diff --git a/packages/uni_app/assets/images/current_account.png b/packages/uni_app/assets/images/current_account.png new file mode 100644 index 000000000..57cd7b88b Binary files /dev/null and b/packages/uni_app/assets/images/current_account.png differ diff --git a/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj b/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj index 9f321be0b..3c1df5698 100644 --- a/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj @@ -360,10 +360,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -702,6 +706,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = uni_dev; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 9D8RT43WU8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -709,7 +714,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = pt.up.fe.ni.uni.dev; + PRODUCT_BUNDLE_IDENTIFIER = pt.up.fe.ni.uni.dev.g; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; diff --git a/packages/uni_app/ios/Runner/Info.plist b/packages/uni_app/ios/Runner/Info.plist index e3c79a662..86cd095bc 100644 --- a/packages/uni_app/ios/Runner/Info.plist +++ b/packages/uni_app/ios/Runner/Info.plist @@ -55,6 +55,11 @@ LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSCalendarsUsageDescription Exportar exames e eventos para o calendário NSPhotoLibraryUsageDescription diff --git a/packages/uni_app/lib/controller/fetchers/fees_fetcher.dart b/packages/uni_app/lib/controller/fetchers/fees_fetcher.dart index bd87a5ebc..1a52259b0 100644 --- a/packages/uni_app/lib/controller/fetchers/fees_fetcher.dart +++ b/packages/uni_app/lib/controller/fetchers/fees_fetcher.dart @@ -1,6 +1,8 @@ import 'package:http/http.dart'; import 'package:uni/controller/fetchers/session_dependant_fetcher.dart'; import 'package:uni/controller/networking/network_router.dart'; +import 'package:uni/controller/parsers/parser_current_account.dart'; +import 'package:uni/model/entities/current_account.dart'; import 'package:uni/session/flows/base/session.dart'; class FeesFetcher implements SessionDependantFetcher { @@ -19,4 +21,17 @@ class FeesFetcher implements SessionDependantFetcher { final query = {'pct_cod': session.username}; return NetworkRouter.getWithCookies(url, query, session); } + + Future<(List, List)> extractCurrentAccount( + Session session, + CurrentAccountParser parser, + ) async { + final response = await getUserFeesResponse(session); + + final unpaid = parser.parseUnpaid(response); + + final accountStatement = parser.parseAccountStatement(response); + + return (unpaid, accountStatement); + } } diff --git a/packages/uni_app/lib/controller/parsers/parser_current_account.dart b/packages/uni_app/lib/controller/parsers/parser_current_account.dart new file mode 100644 index 000000000..512201abb --- /dev/null +++ b/packages/uni_app/lib/controller/parsers/parser_current_account.dart @@ -0,0 +1,137 @@ +import 'package:html/dom.dart'; +import 'package:html/parser.dart'; +import 'package:http/http.dart'; +import 'package:uni/model/entities/current_account.dart'; + +class CurrentAccountParser { + static const tabNames = { + 'unpaid': ['Despesas não saldadas', 'Unpaid expenses'], + //'certificate': ['Certidão', 'Certidão'], + //'latePayment': ['Juros de mora Propinas', 'Juros de mora Propinas'], + //'tuitionFees': ['Propinas', 'Tuition fees'], + //'schoolInsurance': ['Seguro Escolar', 'Seguro Escolar'], + 'accountStatement': ['Extrato Geral', 'Account Statement'], + }; + + String? findTableId(Document document, List names) { + final tabs = document.querySelectorAll( + '#GPAG_CCORRENTE_GERAL_CONTA_CORRENTE_VIEW ul li a', + ); + for (final tab in tabs) { + if (names.contains(tab.text.trim())) { + return tab.attributes['href']; + } + } + return null; + } + + List parseUnpaid(Response response) { + final List data = []; + final document = parse(response.body); + + final tableId = findTableId(document, tabNames['unpaid']!); + + if (tableId != null) { + final tab = document.querySelector(tableId); + final rows = tab?.querySelectorAll('tr').skip(1) ?? []; + + for (final row in rows) { + final cells = row.querySelectorAll('td'); + final description = cells[2].text.trim(); + + final date = DateTime.parse(cells[3].text.trim()); + + final deadline = cells[4].text.trim().isEmpty + ? null + : DateTime.parse(cells[4].text.trim()); + final value = parseAmount(cells[5].text.trim()) ?? 0; + final amountDue = parseAmount(cells[7].text.trim()) ?? 0; + + final anchor = cells[8].querySelector('a'); + final relativeLink = anchor?.attributes['href']; + + String? paymentLink; + if (relativeLink != null) { + paymentLink = 'https://sigarra.up.pt/feup/pt/$relativeLink'; + } + + final interest = cells[9].text.trim(); + double? interestOnLatePayment; + + if (interest.isNotEmpty) { + final parts = interest.split('+'); + interestOnLatePayment = parts + .map((p) => parseAmount(p) ?? 0.0) + .reduce((a, b) => a + b); + } + + data.add( + Unpaid( + description: description, + date: date, + deadline: deadline, + value: value, + amountDue: amountDue, + interestOnLatePayment: interestOnLatePayment, + paymentLink: paymentLink, + ), + ); + } + } + return data; + } + + List parseAccountStatement(Response response) { + final document = parse(response.body); + final List data = []; + + final tableId = findTableId(document, tabNames['accountStatement']!); + + if (tableId != null) { + final tab = document.querySelector(tableId); + final rows = tab?.querySelectorAll('tbody tr') ?? []; + + for (final row in rows) { + final cells = row.querySelectorAll('td'); + final description = cells[0].text.trim(); + + final date = DateTime.parse(cells[1].text.trim()); + final credit = parseAmount(cells[3].text.trim()); + + if (credit != null) { + data.add( + AccountStatement( + description: description, + date: date, + credit: credit, + ), + ); + } + } + } + + return data; + } + + String parseStatus(Element cell) { + final img = cell.querySelector('img'); + final alt = img?.attributes['alt'] ?? ''; + + return switch (alt) { + 'Pago' => 'Paid', + 'Não pago mas prazo ainda não foi excedido' => 'unpaid', + 'Não pago mas prazo foi excedido' => 'overdue', + _ => 'unknown', + }; + } + + double? parseAmount(String text) { + final cleaned = text + .replaceAll('€', '') + .replaceAll('\u00a0', '') + .replaceAll(' ', '') + .replaceAll(',', ''); + + return double.tryParse(cleaned); + } +} diff --git a/packages/uni_app/lib/generated/intl/messages_en.dart b/packages/uni_app/lib/generated/intl/messages_en.dart index ec226d08b..b3b493f06 100644 --- a/packages/uni_app/lib/generated/intl/messages_en.dart +++ b/packages/uni_app/lib/generated/intl/messages_en.dart @@ -28,7 +28,7 @@ class MessageLookup extends MessageLookupByLibrary { "${Intl.plural(time, zero: 'Refreshed ${time} minutes ago', one: 'Refreshed ${time} minute ago', other: 'Refreshed ${time} minutes ago')}"; static m3(title) => - "${Intl.select(title, {'horario': 'Schedule', 'exames': 'Exams', 'area': 'Personal Area', 'cadeiras': 'Course Units', 'autocarros': 'Buses', 'locais': 'Places', 'restaurantes': 'Restaurants', 'calendario': 'Calendar', 'biblioteca': 'Library', 'percurso_academico': 'Academic Path', 'mapa': 'Map', 'faculdade': 'Faculty', 'other': 'Other'})}"; + "${Intl.select(title, {'horario': 'Schedule', 'exames': 'Exams', 'area': 'Personal Area', 'cadeiras': 'Course Units', 'autocarros': 'Buses', 'locais': 'Places', 'restaurantes': 'Restaurants', 'calendario': 'Calendar', 'biblioteca': 'Library', 'percurso_academico': 'Academic Path', 'mapa': 'Map', 'faculdade': 'Faculty', 'conta_corrente': 'Current Account', 'other': 'Other'})}"; static m4(period) => "${Intl.select(period, {'lunch': 'Lunch', 'dinner': 'Dinner', 'other': 'Other'})}"; @@ -71,6 +71,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "average": MessageLookupByLibrary.simpleMessage("Average"), "balance": MessageLookupByLibrary.simpleMessage("Balance"), + "balance_description": MessageLookupByLibrary.simpleMessage( + "Your total outstanding balance", + ), "banner_info": MessageLookupByLibrary.simpleMessage( "We collect anonymous usage data to help improve your experience. You can opt out anytime in the settings.", ), @@ -146,6 +149,10 @@ class MessageLookup extends MessageLookupByLibrary { "course_class": MessageLookupByLibrary.simpleMessage("Classes"), "course_info": MessageLookupByLibrary.simpleMessage("Info"), "courses": MessageLookupByLibrary.simpleMessage("Courses"), + "current_account": MessageLookupByLibrary.simpleMessage("Current Account"), + "current_account_description": MessageLookupByLibrary.simpleMessage( + "Track your fees, due dates and payment history.", + ), "current_state": MessageLookupByLibrary.simpleMessage("Current state: "), "current_year": MessageLookupByLibrary.simpleMessage( "Current academic year: ", @@ -173,6 +180,7 @@ class MessageLookup extends MessageLookupByLibrary { "drag_and_drop": MessageLookupByLibrary.simpleMessage( "Drag and drop elements", ), + "due_in": MessageLookupByLibrary.simpleMessage("Due in"), "ects": MessageLookupByLibrary.simpleMessage("ECTS performed: "), "edit_homepage": MessageLookupByLibrary.simpleMessage("Edit"), "edit_off": MessageLookupByLibrary.simpleMessage("Edit"), @@ -199,6 +207,9 @@ class MessageLookup extends MessageLookupByLibrary { "failed_upload": MessageLookupByLibrary.simpleMessage("Failed to upload"), "favorite_filter": MessageLookupByLibrary.simpleMessage("Favorites"), "fee_date": MessageLookupByLibrary.simpleMessage("Deadline"), + "fee_date_description": MessageLookupByLibrary.simpleMessage( + "Due date for your next payment", + ), "fee_notification": MessageLookupByLibrary.simpleMessage("Fee deadline"), "feedback_description": MessageLookupByLibrary.simpleMessage( "Report an issue or suggest an improvement", @@ -211,6 +222,7 @@ class MessageLookup extends MessageLookupByLibrary { "floors": MessageLookupByLibrary.simpleMessage("Floors"), "forgot_password": MessageLookupByLibrary.simpleMessage("Forgot password?"), "frequency": MessageLookupByLibrary.simpleMessage("Eligibility for exams"), + "general_history": MessageLookupByLibrary.simpleMessage("General History"), "generate_reference": MessageLookupByLibrary.simpleMessage( "Generate reference", ), @@ -226,6 +238,9 @@ class MessageLookup extends MessageLookupByLibrary { "increment": MessageLookupByLibrary.simpleMessage("Increment 1,00€"), "instructor": MessageLookupByLibrary.simpleMessage("Instructor"), "instructors": MessageLookupByLibrary.simpleMessage("Instructors"), + "interest_on_late_payments": MessageLookupByLibrary.simpleMessage( + "Interest on late payments", + ), "internet_status_exception": MessageLookupByLibrary.simpleMessage( "Check your internet connection", ), @@ -305,6 +320,9 @@ class MessageLookup extends MessageLookupByLibrary { "no_courses_description": MessageLookupByLibrary.simpleMessage( "Try to refresh the page", ), + "no_current_account_info": MessageLookupByLibrary.simpleMessage( + "Try refreshing the page or check back later.", + ), "no_data": MessageLookupByLibrary.simpleMessage( "There is no data to show at this time", ), @@ -326,6 +344,12 @@ class MessageLookup extends MessageLookupByLibrary { "no_files_label": MessageLookupByLibrary.simpleMessage( "You have nothing to see!", ), + "no_history_label": MessageLookupByLibrary.simpleMessage( + "Nothing here yet", + ), + "no_history_sublabel": MessageLookupByLibrary.simpleMessage( + "Your payment history will appear here.", + ), "no_info": MessageLookupByLibrary.simpleMessage( "There is no information to display", ), @@ -349,12 +373,17 @@ class MessageLookup extends MessageLookupByLibrary { ), "no_name_course": MessageLookupByLibrary.simpleMessage("Unnamed course"), "no_news": MessageLookupByLibrary.simpleMessage("No news to display"), + "no_pending_label": MessageLookupByLibrary.simpleMessage("All caught up!"), + "no_pending_sublabel": MessageLookupByLibrary.simpleMessage( + "You have no pending payments.", + ), "no_places_info": MessageLookupByLibrary.simpleMessage( "There is no information available about places", ), "no_print_info": MessageLookupByLibrary.simpleMessage( "No print balance information", ), + "no_records": MessageLookupByLibrary.simpleMessage("No records"), "no_references": MessageLookupByLibrary.simpleMessage( "There are no references to pay", ), @@ -374,6 +403,12 @@ class MessageLookup extends MessageLookupByLibrary { "no_trips": MessageLookupByLibrary.simpleMessage( "No trips found at the moment", ), + "no_tuition_fees_label": MessageLookupByLibrary.simpleMessage( + "No tuition fees found", + ), + "no_tuition_fees_sublabel": MessageLookupByLibrary.simpleMessage( + "Your tuition fee records will appear here.", + ), "notifications": MessageLookupByLibrary.simpleMessage("Notifications"), "now": MessageLookupByLibrary.simpleMessage("Now"), "occurrence_type": MessageLookupByLibrary.simpleMessage( @@ -384,6 +419,7 @@ class MessageLookup extends MessageLookupByLibrary { "Error opening the file", ), "other_links": MessageLookupByLibrary.simpleMessage("Other links"), + "overview": MessageLookupByLibrary.simpleMessage("Overview"), "pass_change_request": MessageLookupByLibrary.simpleMessage( "For security reasons, passwords must be changed periodically.", ), @@ -397,6 +433,7 @@ class MessageLookup extends MessageLookupByLibrary { "pendent_references": MessageLookupByLibrary.simpleMessage( "Pending references", ), + "pending": MessageLookupByLibrary.simpleMessage("Pending"), "permission_denied": MessageLookupByLibrary.simpleMessage( "Permission denied", ), @@ -406,6 +443,9 @@ class MessageLookup extends MessageLookupByLibrary { "press_again": MessageLookupByLibrary.simpleMessage("Press again to exit"), "print": MessageLookupByLibrary.simpleMessage("Print"), "print_balance": MessageLookupByLibrary.simpleMessage("Print balance"), + "print_balance_description": MessageLookupByLibrary.simpleMessage( + "Funds for university printing services", + ), "prints": MessageLookupByLibrary.simpleMessage("Prints"), "problem_id": MessageLookupByLibrary.simpleMessage( "Brief identification of the problem", @@ -468,10 +508,12 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("Theme"), "title": MessageLookupByLibrary.simpleMessage("Title"), "tomorrows_meals": MessageLookupByLibrary.simpleMessage("Tomorrow\'s Menu"), + "transactions": MessageLookupByLibrary.simpleMessage("Transactions"), "try_again": MessageLookupByLibrary.simpleMessage("Try again"), "try_different_login": MessageLookupByLibrary.simpleMessage( "Having trouble signing in?", ), + "tuition_fees": MessageLookupByLibrary.simpleMessage("Tuition Fees"), "uc_info": MessageLookupByLibrary.simpleMessage("Open UC page"), "ucs": MessageLookupByLibrary.simpleMessage("UCS"), "unable_to_load_data": MessageLookupByLibrary.simpleMessage( @@ -479,6 +521,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "unavailable": MessageLookupByLibrary.simpleMessage("Unavailable"), "until": MessageLookupByLibrary.simpleMessage("Until"), + "upcoming_due": MessageLookupByLibrary.simpleMessage("Upcoming Due"), "valid_email": MessageLookupByLibrary.simpleMessage( "Please enter a valid email", ), diff --git a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart index db7600f51..bd6fc72cb 100644 --- a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart +++ b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart @@ -28,7 +28,7 @@ class MessageLookup extends MessageLookupByLibrary { "${Intl.plural(time, zero: 'Atualizado há ${time} minutos', one: 'Atualizado há ${time} minuto', other: 'Atualizado há ${time} minutos')}"; static m3(title) => - "${Intl.select(title, {'horario': 'Horário', 'exames': 'Exames', 'area': 'Área Pessoal', 'cadeiras': 'Cadeiras', 'autocarros': 'Autocarros', 'locais': 'Locais', 'restaurantes': 'Restaurantes', 'calendario': 'Calendário', 'biblioteca': 'Biblioteca', 'percurso_academico': 'Percurso Académico', 'mapa': 'Mapa', 'faculdade': 'Faculdade', 'other': 'Outros'})}"; + "${Intl.select(title, {'horario': 'Horário', 'exames': 'Exames', 'area': 'Área Pessoal', 'cadeiras': 'Cadeiras', 'autocarros': 'Autocarros', 'locais': 'Locais', 'restaurantes': 'Restaurantes', 'calendario': 'Calendário', 'biblioteca': 'Biblioteca', 'percurso_academico': 'Percurso Académico', 'mapa': 'Mapa', 'faculdade': 'Faculdade', 'conta_corrente': 'Conta Corrente', 'other': 'Outros'})}"; static m4(period) => "${Intl.select(period, {'lunch': 'Almoço', 'dinner': 'Jantar', 'other': 'Other'})}"; @@ -75,6 +75,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "average": MessageLookupByLibrary.simpleMessage("Média"), "balance": MessageLookupByLibrary.simpleMessage("Saldo"), + "balance_description": MessageLookupByLibrary.simpleMessage( + "O teu saldo total em dívida", + ), "banner_info": MessageLookupByLibrary.simpleMessage( "Recolhemos dados anónimos de utilização para ajudar a melhorar a sua experiência. Pode desativar esta opção a qualquer momento nas definições", ), @@ -154,6 +157,10 @@ class MessageLookup extends MessageLookupByLibrary { "course_class": MessageLookupByLibrary.simpleMessage("Turmas"), "course_info": MessageLookupByLibrary.simpleMessage("Ficha"), "courses": MessageLookupByLibrary.simpleMessage("Cursos"), + "current_account": MessageLookupByLibrary.simpleMessage("Conta Corrente"), + "current_account_description": MessageLookupByLibrary.simpleMessage( + "Acompanha as tuas propinas, prazos e histórico de pagamentos.", + ), "current_state": MessageLookupByLibrary.simpleMessage("Estado atual: "), "current_year": MessageLookupByLibrary.simpleMessage( "Ano curricular atual: ", @@ -179,6 +186,7 @@ class MessageLookup extends MessageLookupByLibrary { "drag_and_drop": MessageLookupByLibrary.simpleMessage( "Arrasta e solta os elementos", ), + "due_in": MessageLookupByLibrary.simpleMessage("Vence em"), "ects": MessageLookupByLibrary.simpleMessage("ECTS realizados: "), "edit_homepage": MessageLookupByLibrary.simpleMessage("Editar"), "edit_off": MessageLookupByLibrary.simpleMessage("Editar"), @@ -207,6 +215,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "favorite_filter": MessageLookupByLibrary.simpleMessage("Favoritos"), "fee_date": MessageLookupByLibrary.simpleMessage("Data limite"), + "fee_date_description": MessageLookupByLibrary.simpleMessage( + "Data limite para o próximo pagamento", + ), "fee_notification": MessageLookupByLibrary.simpleMessage( "Data limite de propina", ), @@ -223,6 +234,7 @@ class MessageLookup extends MessageLookupByLibrary { "Esqueceu a palavra-passe?", ), "frequency": MessageLookupByLibrary.simpleMessage("Obtenção de Frequência"), + "general_history": MessageLookupByLibrary.simpleMessage("Histórico Geral"), "generate_reference": MessageLookupByLibrary.simpleMessage( "Gerar referência", ), @@ -238,6 +250,9 @@ class MessageLookup extends MessageLookupByLibrary { "increment": MessageLookupByLibrary.simpleMessage("Incrementar 1,00€"), "instructor": MessageLookupByLibrary.simpleMessage("Docente"), "instructors": MessageLookupByLibrary.simpleMessage("Docentes"), + "interest_on_late_payments": MessageLookupByLibrary.simpleMessage( + "Juros de mora", + ), "internet_status_exception": MessageLookupByLibrary.simpleMessage( "Verifique sua conexão com a internet", ), @@ -321,6 +336,9 @@ class MessageLookup extends MessageLookupByLibrary { "no_courses_description": MessageLookupByLibrary.simpleMessage( "Tenta refrescar a página", ), + "no_current_account_info": MessageLookupByLibrary.simpleMessage( + "Tenta atualizar a página ou volta mais tarde.", + ), "no_data": MessageLookupByLibrary.simpleMessage( "Não há dados a mostrar neste momento", ), @@ -346,6 +364,10 @@ class MessageLookup extends MessageLookupByLibrary { "no_files_label": MessageLookupByLibrary.simpleMessage( "Não tens nada para ver!", ), + "no_history_label": MessageLookupByLibrary.simpleMessage("Sem movimentos"), + "no_history_sublabel": MessageLookupByLibrary.simpleMessage( + "O teu histórico de pagamentos aparecerá aqui.", + ), "no_info": MessageLookupByLibrary.simpleMessage( "Não existem informações para apresentar", ), @@ -371,12 +393,17 @@ class MessageLookup extends MessageLookupByLibrary { "no_news": MessageLookupByLibrary.simpleMessage( "Não há notícias para apresentar", ), + "no_pending_label": MessageLookupByLibrary.simpleMessage("Tudo em dia!"), + "no_pending_sublabel": MessageLookupByLibrary.simpleMessage( + "Não tens pagamentos pendentes.", + ), "no_places_info": MessageLookupByLibrary.simpleMessage( "Não há informação disponível sobre locais", ), "no_print_info": MessageLookupByLibrary.simpleMessage( "Sem informação de saldo", ), + "no_records": MessageLookupByLibrary.simpleMessage("Sem registos"), "no_references": MessageLookupByLibrary.simpleMessage( "Não existem referências a pagar", ), @@ -396,6 +423,12 @@ class MessageLookup extends MessageLookupByLibrary { "no_trips": MessageLookupByLibrary.simpleMessage( "Não há viagens planeadas de momento", ), + "no_tuition_fees_label": MessageLookupByLibrary.simpleMessage( + "Sem propinas encontradas", + ), + "no_tuition_fees_sublabel": MessageLookupByLibrary.simpleMessage( + "Os teus registos de propinas aparecerão aqui.", + ), "notifications": MessageLookupByLibrary.simpleMessage("Notificações"), "now": MessageLookupByLibrary.simpleMessage("Agora"), "occurrence_type": MessageLookupByLibrary.simpleMessage( @@ -406,6 +439,7 @@ class MessageLookup extends MessageLookupByLibrary { "Erro ao abrir o ficheiro", ), "other_links": MessageLookupByLibrary.simpleMessage("Outros links"), + "overview": MessageLookupByLibrary.simpleMessage("Visão Geral"), "pass_change_request": MessageLookupByLibrary.simpleMessage( "Por razões de segurança, as palavras-passe têm de ser alteradas periodicamente.", ), @@ -419,6 +453,7 @@ class MessageLookup extends MessageLookupByLibrary { "pendent_references": MessageLookupByLibrary.simpleMessage( "Referências pendentes", ), + "pending": MessageLookupByLibrary.simpleMessage("Pendentes"), "permission_denied": MessageLookupByLibrary.simpleMessage("Sem permissão"), "personal_assistance": MessageLookupByLibrary.simpleMessage( "Atendimento presencial", @@ -428,6 +463,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "print": MessageLookupByLibrary.simpleMessage("Impressão"), "print_balance": MessageLookupByLibrary.simpleMessage("Saldo impressões"), + "print_balance_description": MessageLookupByLibrary.simpleMessage( + "Saldo para serviços de impressão da UP", + ), "prints": MessageLookupByLibrary.simpleMessage("Impressões"), "problem_id": MessageLookupByLibrary.simpleMessage( "Breve identificação do problema", @@ -496,10 +534,12 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("Tema"), "title": MessageLookupByLibrary.simpleMessage("Título"), "tomorrows_meals": MessageLookupByLibrary.simpleMessage("Menu de Amanhã"), + "transactions": MessageLookupByLibrary.simpleMessage("Transações"), "try_again": MessageLookupByLibrary.simpleMessage("Tentar de novo"), "try_different_login": MessageLookupByLibrary.simpleMessage( "Problemas ao iniciar sessão?", ), + "tuition_fees": MessageLookupByLibrary.simpleMessage("Propinas"), "uc_info": MessageLookupByLibrary.simpleMessage("Abrir página da UC"), "ucs": MessageLookupByLibrary.simpleMessage("UCS"), "unable_to_load_data": MessageLookupByLibrary.simpleMessage( @@ -507,6 +547,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "unavailable": MessageLookupByLibrary.simpleMessage("Indisponível"), "until": MessageLookupByLibrary.simpleMessage("Até"), + "upcoming_due": MessageLookupByLibrary.simpleMessage("Próximo Vencimento"), "valid_email": MessageLookupByLibrary.simpleMessage( "Por favor insere um email válido", ), diff --git a/packages/uni_app/lib/generated/l10n.dart b/packages/uni_app/lib/generated/l10n.dart index 0081a27f5..0c326f4bf 100644 --- a/packages/uni_app/lib/generated/l10n.dart +++ b/packages/uni_app/lib/generated/l10n.dart @@ -792,7 +792,7 @@ class S { ); } - /// `{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} mapa{Map} faculdade{Faculty} other{Other}}` + /// `{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} mapa{Map} faculdade{Faculty} conta_corrente{Current Account} other{Other}}` String nav_title(Object title) { return Intl.select( title, @@ -809,6 +809,7 @@ class S { 'percurso_academico': 'Academic Path', 'mapa': 'Map', 'faculdade': 'Faculty', + 'conta_corrente': 'Current Account', 'other': 'Other', }, name: 'nav_title', @@ -2030,6 +2031,196 @@ class S { ); } + /// `Current Account` + String get current_account { + return Intl.message( + 'Current Account', + name: 'current_account', + desc: '', + args: [], + ); + } + + /// `Track your fees, due dates and payment history.` + String get current_account_description { + return Intl.message( + 'Track your fees, due dates and payment history.', + name: 'current_account_description', + desc: '', + args: [], + ); + } + + /// `Overview` + String get overview { + return Intl.message('Overview', name: 'overview', desc: '', args: []); + } + + /// `Upcoming Due` + String get upcoming_due { + return Intl.message( + 'Upcoming Due', + name: 'upcoming_due', + desc: '', + args: [], + ); + } + + /// `Transactions` + String get transactions { + return Intl.message( + 'Transactions', + name: 'transactions', + desc: '', + args: [], + ); + } + + /// `Pending` + String get pending { + return Intl.message('Pending', name: 'pending', desc: '', args: []); + } + + /// `Tuition Fees` + String get tuition_fees { + return Intl.message( + 'Tuition Fees', + name: 'tuition_fees', + desc: '', + args: [], + ); + } + + /// `General History` + String get general_history { + return Intl.message( + 'General History', + name: 'general_history', + desc: '', + args: [], + ); + } + + /// `Your total outstanding balance` + String get balance_description { + return Intl.message( + 'Your total outstanding balance', + name: 'balance_description', + desc: '', + args: [], + ); + } + + /// `Due date for your next payment` + String get fee_date_description { + return Intl.message( + 'Due date for your next payment', + name: 'fee_date_description', + desc: '', + args: [], + ); + } + + /// `Funds for university printing services` + String get print_balance_description { + return Intl.message( + 'Funds for university printing services', + name: 'print_balance_description', + desc: '', + args: [], + ); + } + + /// `Interest on late payments` + String get interest_on_late_payments { + return Intl.message( + 'Interest on late payments', + name: 'interest_on_late_payments', + desc: '', + args: [], + ); + } + + /// `Due in` + String get due_in { + return Intl.message('Due in', name: 'due_in', desc: '', args: []); + } + + /// `No records` + String get no_records { + return Intl.message('No records', name: 'no_records', desc: '', args: []); + } + + /// `Try refreshing the page or check back later.` + String get no_current_account_info { + return Intl.message( + 'Try refreshing the page or check back later.', + name: 'no_current_account_info', + desc: '', + args: [], + ); + } + + /// `All caught up!` + String get no_pending_label { + return Intl.message( + 'All caught up!', + name: 'no_pending_label', + desc: '', + args: [], + ); + } + + /// `You have no pending payments.` + String get no_pending_sublabel { + return Intl.message( + 'You have no pending payments.', + name: 'no_pending_sublabel', + desc: '', + args: [], + ); + } + + /// `Nothing here yet` + String get no_history_label { + return Intl.message( + 'Nothing here yet', + name: 'no_history_label', + desc: '', + args: [], + ); + } + + /// `Your payment history will appear here.` + String get no_history_sublabel { + return Intl.message( + 'Your payment history will appear here.', + name: 'no_history_sublabel', + desc: '', + args: [], + ); + } + + /// `No tuition fees found` + String get no_tuition_fees_label { + return Intl.message( + 'No tuition fees found', + name: 'no_tuition_fees_label', + desc: '', + args: [], + ); + } + + /// `Your tuition fee records will appear here.` + String get no_tuition_fees_sublabel { + return Intl.message( + 'Your tuition fee records will appear here.', + name: 'no_tuition_fees_sublabel', + desc: '', + args: [], + ); + } + /// `Edit` String get edit_homepage { return Intl.message('Edit', name: 'edit_homepage', desc: '', args: []); diff --git a/packages/uni_app/lib/l10n/intl_en.arb b/packages/uni_app/lib/l10n/intl_en.arb index 41597c5d2..ed78f60a2 100644 --- a/packages/uni_app/lib/l10n/intl_en.arb +++ b/packages/uni_app/lib/l10n/intl_en.arb @@ -192,7 +192,7 @@ "@min_value_reference": {}, "multimedia_center": "Multimedia center", "@multimedia_center": {}, - "nav_title": "{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} mapa{Map} faculdade{Faculty} other{Other}}", + "nav_title": "{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} mapa{Map} faculdade{Faculty} conta_corrente{Current Account} other{Other}}", "@nav_title": {}, "news": "News", "@news": {}, @@ -484,6 +484,48 @@ "@no_restaurants_available": {}, "no_restaurants_available_sublabel": "Bring your lunchbox from home.", "@no_restaurants_available_sublabel": {}, + "current_account": "Current Account", + "@current_account": {}, + "current_account_description": "Track your fees, due dates and payment history.", + "@current_account_description": {}, + "overview": "Overview", + "@overview": {}, + "upcoming_due": "Upcoming Due", + "@upcoming_due": {}, + "transactions": "Transactions", + "@transactions": {}, + "pending": "Pending", + "@pending": {}, + "tuition_fees": "Tuition Fees", + "@tuition_fees": {}, + "general_history": "General History", + "@general_history": {}, + "balance_description": "Your total outstanding balance", + "@balance_description": {}, + "fee_date_description": "Due date for your next payment", + "@fee_date_description": {}, + "print_balance_description": "Funds for university printing services", + "@print_balance_description": {}, + "interest_on_late_payments": "Interest on late payments", + "@interest_on_late_payments": {}, + "due_in": "Due in", + "@due_in": {}, + "no_records": "No records", + "@no_records": {}, + "no_current_account_info": "Try refreshing the page or check back later.", + "@no_current_account_info": {}, + "no_pending_label": "All caught up!", + "@no_pending_label": {}, + "no_pending_sublabel": "You have no pending payments.", + "@no_pending_sublabel": {}, + "no_history_label": "Nothing here yet", + "@no_history_label": {}, + "no_history_sublabel": "Your payment history will appear here.", + "@no_history_sublabel": {}, + "no_tuition_fees_label": "No tuition fees found", + "@no_tuition_fees_label": {}, + "no_tuition_fees_sublabel": "Your tuition fee records will appear here.", + "@no_tuition_fees_sublabel": {}, "edit_homepage": "Edit", "@edit_homepage": {} } \ No newline at end of file diff --git a/packages/uni_app/lib/l10n/intl_pt_PT.arb b/packages/uni_app/lib/l10n/intl_pt_PT.arb index 40825cf64..3ce896d82 100644 --- a/packages/uni_app/lib/l10n/intl_pt_PT.arb +++ b/packages/uni_app/lib/l10n/intl_pt_PT.arb @@ -200,7 +200,7 @@ "@min_value_reference": {}, "multimedia_center": "Centro de multimédia", "@multimedia_center": {}, - "nav_title": "{title, select, horario{Horário} exames{Exames} area{Área Pessoal} cadeiras{Cadeiras} autocarros{Autocarros} locais{Locais} restaurantes{Restaurantes} calendario{Calendário} biblioteca{Biblioteca} percurso_academico{Percurso Académico} mapa{Mapa} faculdade{Faculdade} other{Outros}}", + "nav_title": "{title, select, horario{Horário} exames{Exames} area{Área Pessoal} cadeiras{Cadeiras} autocarros{Autocarros} locais{Locais} restaurantes{Restaurantes} calendario{Calendário} biblioteca{Biblioteca} percurso_academico{Percurso Académico} mapa{Mapa} faculdade{Faculdade} conta_corrente{Conta Corrente} other{Outros}}", "@nav_title": {}, "news": "Notícias", "@news": {}, @@ -484,6 +484,48 @@ "@no_restaurants_available": {}, "no_restaurants_available_sublabel": "Traz a tua marmita de casa.", "@no_restaurants_available_sublabel": {}, + "current_account": "Conta Corrente", + "@current_account": {}, + "current_account_description": "Acompanha as tuas propinas, prazos e histórico de pagamentos.", + "@current_account_description": {}, + "overview": "Visão Geral", + "@overview": {}, + "upcoming_due": "Próximo Vencimento", + "@upcoming_due": {}, + "transactions": "Transações", + "@transactions": {}, + "pending": "Pendentes", + "@pending": {}, + "tuition_fees": "Propinas", + "@tuition_fees": {}, + "general_history": "Histórico Geral", + "@general_history": {}, + "balance_description": "O teu saldo total em dívida", + "@balance_description": {}, + "fee_date_description": "Data limite para o próximo pagamento", + "@fee_date_description": {}, + "print_balance_description": "Saldo para serviços de impressão da UP", + "@print_balance_description": {}, + "interest_on_late_payments": "Juros de mora", + "@interest_on_late_payments": {}, + "due_in": "Vence em", + "@due_in": {}, + "no_records": "Sem registos", + "@no_records": {}, + "no_current_account_info": "Tenta atualizar a página ou volta mais tarde.", + "@no_current_account_info": {}, + "no_pending_label": "Tudo em dia!", + "@no_pending_label": {}, + "no_pending_sublabel": "Não tens pagamentos pendentes.", + "@no_pending_sublabel": {}, + "no_history_label": "Sem movimentos", + "@no_history_label": {}, + "no_history_sublabel": "O teu histórico de pagamentos aparecerá aqui.", + "@no_history_sublabel": {}, + "no_tuition_fees_label": "Sem propinas encontradas", + "@no_tuition_fees_label": {}, + "no_tuition_fees_sublabel": "Os teus registos de propinas aparecerão aqui.", + "@no_tuition_fees_sublabel": {}, "edit_homepage": "Editar", "@edit_homepage": {} } \ No newline at end of file diff --git a/packages/uni_app/lib/main.dart b/packages/uni_app/lib/main.dart index fbefafce8..a301c1b43 100644 --- a/packages/uni_app/lib/main.dart +++ b/packages/uni_app/lib/main.dart @@ -27,6 +27,7 @@ import 'package:uni/view/academic_path/academic_path.dart'; import 'package:uni/view/bug_report/bug_report.dart'; import 'package:uni/view/calendar/calendar.dart'; import 'package:uni/view/course_unit_info/course_unit_info.dart'; +import 'package:uni/view/current_account/current_account.dart'; import 'package:uni/view/faculty/faculty.dart'; import 'package:uni/view/home/edit_home.dart'; import 'package:uni/view/home/home.dart'; @@ -250,6 +251,11 @@ class ApplicationState extends ConsumerState { page: CourseUnitDetailPageView(courseUnit!), settings: settings, ), + '/${NavigationItem.navCurrentAccount.route}': () => + PageTransition.makePageTransition( + page: const CurrentAccountPageView(), + settings: settings, + ), }; final builder = transitionFunctions[settings.name]; diff --git a/packages/uni_app/lib/model/entities/current_account.dart b/packages/uni_app/lib/model/entities/current_account.dart new file mode 100644 index 000000000..1f682c09d --- /dev/null +++ b/packages/uni_app/lib/model/entities/current_account.dart @@ -0,0 +1,41 @@ +class Unpaid { + Unpaid({ + required this.description, + required this.date, + this.deadline, + required this.value, + required this.amountDue, + this.interestOnLatePayment, + this.paymentLink, + }); + + final String description; + final DateTime date; + final DateTime? deadline; + final double value; + final double amountDue; + final double? interestOnLatePayment; + final String? paymentLink; + + @override + String toString() { + return 'Unpaid(description: $description, date: $date, deadline: $deadline, value: $value, amountDue: $amountDue, interestOnLatePayment: $interestOnLatePayment)'; + } +} + +class AccountStatement { + AccountStatement({ + required this.description, + required this.date, + required this.credit, + }); + + final String description; + final DateTime date; + final double credit; + + @override + String toString() { + return 'AccountStatement(description: $description, date: $date, credit:$credit)'; + } +} diff --git a/packages/uni_app/lib/model/providers/riverpod/current_account_provider.dart b/packages/uni_app/lib/model/providers/riverpod/current_account_provider.dart new file mode 100644 index 000000000..0cb828240 --- /dev/null +++ b/packages/uni_app/lib/model/providers/riverpod/current_account_provider.dart @@ -0,0 +1,12 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uni/controller/fetchers/fees_fetcher.dart'; +import 'package:uni/controller/parsers/parser_current_account.dart'; +import 'package:uni/model/providers/riverpod/session_provider.dart'; + +final currentAccountProvider = FutureProvider.autoDispose((ref) async { + final session = await ref.watch(sessionProvider.future); + final fetcher = FeesFetcher(); + final parser = CurrentAccountParser(); + final result = await fetcher.extractCurrentAccount(session!, parser); + return result; +}); diff --git a/packages/uni_app/lib/utils/navigation_items.dart b/packages/uni_app/lib/utils/navigation_items.dart index e70a8dd2b..48dd8a953 100644 --- a/packages/uni_app/lib/utils/navigation_items.dart +++ b/packages/uni_app/lib/utils/navigation_items.dart @@ -16,7 +16,8 @@ enum NavigationItem { navLogin('login'), navBugreport('bug_report'), navSplash('splash'), - navAboutus('sobre_nos'); + navAboutus('sobre_nos'), + navCurrentAccount('conta_corrente'); const NavigationItem(this.route, {this.faculties}); diff --git a/packages/uni_app/lib/view/current_account/current_account.dart b/packages/uni_app/lib/view/current_account/current_account.dart new file mode 100644 index 000000000..b5d2565e0 --- /dev/null +++ b/packages/uni_app/lib/view/current_account/current_account.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/current_account.dart'; +import 'package:uni/model/providers/riverpod/current_account_provider.dart'; +import 'package:uni/model/providers/riverpod/profile_provider.dart'; +import 'package:uni/utils/navigation_items.dart'; +import 'package:uni/view/current_account/widgets/account_overview.dart'; +import 'package:uni/view/current_account/widgets/current_account_shimmers.dart'; +import 'package:uni/view/current_account/widgets/no_current_account.dart'; +import 'package:uni/view/current_account/widgets/transaction.dart'; +import 'package:uni/view/current_account/widgets/transaction_filter_menu.dart'; +import 'package:uni/view/widgets/pages_layouts/secondary/secondary.dart'; + +class CurrentAccountPageView extends ConsumerStatefulWidget { + const CurrentAccountPageView({super.key}); + + @override + ConsumerState createState() => + CurrentAccountPageViewState(); +} + +class CurrentAccountPageViewState + extends SecondaryPageViewState { + String _selectedFilter = 'Pending'; + + @override + String? getTitle() => + S.of(context).nav_title(NavigationItem.navCurrentAccount.route); + + @override + Future onRefresh() async {} + + List _filterTuition( + List unpaid, + List statement, + ) { + final List combined = + [ + ...unpaid.where( + (e) => e.description.toLowerCase().contains('propina'), + ), + ...statement.where( + (e) => e.description.toLowerCase().contains('propina'), + ), + ]..sort((a, b) { + final dateA = (a is Unpaid) + ? (a.deadline ?? a.date) + : (a as AccountStatement).date; + final dateB = (b is Unpaid) + ? (b.deadline ?? b.date) + : (b as AccountStatement).date; + return dateB.compareTo(dateA); + }); + + return combined; + } + + Widget _buildListView(List items) { + if (items.isEmpty) { + return const Center(child: Text('Sem registos para este filtro.')); + } + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + itemCount: items.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final item = items[index]; + + if (item is Unpaid) { + return Transaction( + description: item.description, + date: item.date, + deadline: item.deadline, + value: item.amountDue, + interestOnLatePayment: item.interestOnLatePayment, + paymentLink: item.paymentLink, + isUnpaid: true, + ); + } else if (item is AccountStatement) { + return Transaction( + description: item.description, + date: item.date, + value: item.credit, + ); + } + return const SizedBox.shrink(); + }, + ); + } + + @override + Widget getBody(BuildContext context) { + final currentAccount = ref.watch(currentAccountProvider); + final accountOverview = ref.watch(profileProvider); + + if (currentAccount.isLoading || accountOverview.isLoading) { + return const CurrentAccountShimmers(); + } + + if (currentAccount.hasError || accountOverview.hasError) { + return LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + height: constraints.maxHeight, + padding: const EdgeInsets.only(bottom: 120), + child: Center( + child: CurrentAccountNoInfo( + label: S.of(context).no_info, + sublabel: S.of(context).no_current_account_info, + ), + ), + ), + ), + ); + } + + final unpaid = currentAccount.value!.$1; + final history = currentAccount.value!.$2; + + final nextItem = unpaid.isNotEmpty ? unpaid.first : null; + + final p = accountOverview.value; + + List currentList; + + switch (_selectedFilter) { + case 'Pending': + currentList = unpaid; + case 'Tuition Fees': + currentList = _filterTuition(unpaid, history); + case 'General History': + currentList = history; + default: + currentList = []; + } + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 20), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + S.of(context).overview, + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + const SizedBox(height: 8), + AccountOverview( + feesBalance: p!.feesBalance, + feesLimit: p.feesLimit, + printBalance: p.printBalance, + ), + const SizedBox(height: 8), + if (nextItem != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + S.of(context).upcoming_due, + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Transaction( + description: nextItem.description, + date: nextItem.date, + deadline: nextItem.deadline, + value: nextItem.amountDue, + interestOnLatePayment: nextItem.interestOnLatePayment, + paymentLink: nextItem.paymentLink, + isUnpaid: true, + ), + ), + ], + const SizedBox(height: 22), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + S.of(context).transactions, + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + Container( + margin: const EdgeInsets.only(right: 20), + child: TransactionFilterMenu( + items: const ['Pending', 'Tuition Fees', 'General History'], + selectedValue: _selectedFilter, + onSelectionChanged: (newValue) { + setState(() { + _selectedFilter = newValue; + }); + }, + ), + ), + ], + ), + const SizedBox(height: 8), + if (currentList.isEmpty) + switch (_selectedFilter) { + 'Pending' => Transform.scale( + scale: 0.8, + child: CurrentAccountNoInfo( + label: S.of(context).no_pending_label, + sublabel: S.of(context).no_pending_sublabel, + ), + ), + 'Tuition Fees' => Transform.scale( + scale: 0.8, + child: CurrentAccountNoInfo( + label: S.of(context).no_tuition_fees_label, + sublabel: S.of(context).no_tuition_fees_sublabel, + ), + ), + 'General History' => Transform.scale( + scale: 0.8, + child: CurrentAccountNoInfo( + label: S.of(context).no_history_label, + sublabel: S.of(context).no_history_sublabel, + ), + ), + _ => Transform.scale( + scale: 0.8, + child: CurrentAccountNoInfo( + label: S.of(context).no_pending_label, + sublabel: S.of(context).no_pending_sublabel, + ), + ), + } + else + _buildListView(currentList), + ], + ); + } +} diff --git a/packages/uni_app/lib/view/current_account/widgets/account_overview.dart b/packages/uni_app/lib/view/current_account/widgets/account_overview.dart new file mode 100644 index 000000000..e0ec77b56 --- /dev/null +++ b/packages/uni_app/lib/view/current_account/widgets/account_overview.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni_ui/cards/generic_card.dart'; +import 'package:uni_ui/cards/profile_list_tile.dart'; +import 'package:uni_ui/icons.dart'; + +class AccountOverview extends StatelessWidget { + const AccountOverview({ + super.key, + required this.feesBalance, + this.feesLimit, + required this.printBalance, + }); + + final String feesBalance; + final DateTime? feesLimit; + final String printBalance; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + GenericCard( + tooltip: S.of(context).balance, + margin: const EdgeInsets.only(bottom: 14, right: 20, left: 20), + padding: EdgeInsets.zero, + + child: ProfileListTile( + icon: UniIcons.piggyBank, + title: S.of(context).balance, + subtitle: S.of(context).balance_description, + trailing: Text( + feesBalance, + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + ), + GenericCard( + tooltip: S.of(context).fee_date, + margin: const EdgeInsets.only(bottom: 14, right: 20, left: 20), + padding: EdgeInsets.zero, + + child: ProfileListTile( + icon: UniIcons.calendarDots, + title: S.of(context).fee_date, + subtitle: S.of(context).fee_date_description, + trailing: Text( + feesLimit != null + ? DateFormat('yyyy-MM-dd').format(feesLimit!) + : S.of(context).no_date, + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + ), + GenericCard( + tooltip: S.of(context).print_balance, + margin: const EdgeInsets.only(bottom: 14, right: 20, left: 20), + padding: EdgeInsets.zero, + child: ProfileListTile( + icon: UniIcons.printer, + title: S.of(context).print_balance, + subtitle: S.of(context).print_balance_description, + trailing: Text( + printBalance, + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + ), + ], + ); + } +} diff --git a/packages/uni_app/lib/view/current_account/widgets/current_account_shimmers.dart b/packages/uni_app/lib/view/current_account/widgets/current_account_shimmers.dart new file mode 100644 index 000000000..a9818fc36 --- /dev/null +++ b/packages/uni_app/lib/view/current_account/widgets/current_account_shimmers.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class CurrentAccountShimmers extends StatelessWidget { + const CurrentAccountShimmers({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 30, bottom: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadiusGeometry.circular(20), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 40, + decoration: const BoxDecoration(color: Colors.white), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadiusGeometry.circular(20), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 60, + decoration: const BoxDecoration(color: Colors.white), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadiusGeometry.circular(20), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 70, + decoration: const BoxDecoration(color: Colors.white), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadiusGeometry.circular(20), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 70, + decoration: const BoxDecoration(color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 14), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadiusGeometry.circular(20), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 40, + decoration: const BoxDecoration(color: Colors.white), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadiusGeometry.circular(20), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 100, + decoration: const BoxDecoration(color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 14), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadiusGeometry.circular(20), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 40, + decoration: const BoxDecoration(color: Colors.white), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadiusGeometry.circular(20), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 150, + decoration: const BoxDecoration(color: Colors.white), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/packages/uni_app/lib/view/current_account/widgets/no_current_account.dart b/packages/uni_app/lib/view/current_account/widgets/no_current_account.dart new file mode 100644 index 000000000..14ef1525b --- /dev/null +++ b/packages/uni_app/lib/view/current_account/widgets/no_current_account.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:uni/view/widgets/expanded_image_label.dart'; + +class CurrentAccountNoInfo extends StatelessWidget { + const CurrentAccountNoInfo({ + super.key, + required this.label, + required this.sublabel, + }); + + final String label; + final String sublabel; + + @override + Widget build(BuildContext context) { + return ImageLabel( + imagePath: 'assets/images/current_account.png', + label: label, + labelTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.primary, + ), + sublabel: sublabel, + sublabelTextStyle: Theme.of(context).textTheme.bodyLarge, + ); + } +} diff --git a/packages/uni_app/lib/view/current_account/widgets/payment_webview.dart b/packages/uni_app/lib/view/current_account/widgets/payment_webview.dart new file mode 100644 index 000000000..1fcce999a --- /dev/null +++ b/packages/uni_app/lib/view/current_account/widgets/payment_webview.dart @@ -0,0 +1,90 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:uni/session/flows/base/session.dart'; + +class PaymentWebView extends StatefulWidget { + const PaymentWebView({super.key, required this.url, required this.session}); + + final String url; + final Session session; + + @override + State createState() => _PaymentWebViewState(); +} + +class _PaymentWebViewState extends State { + bool isLoading = true; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + margin: const EdgeInsets.all(8), + height: 4, + width: 40, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + Expanded( + child: Stack( + children: [ + InAppWebView( + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + initialSettings: InAppWebViewSettings(), + gestureRecognizers: const { + Factory( + VerticalDragGestureRecognizer.new, + ), + }, + onWebViewCreated: (controller) async { + final cookieManager = CookieManager.instance(); + const baseUrl = 'https://sigarra.up.pt'; + + for (final cookie in widget.session.cookies) { + await cookieManager.setCookie( + url: WebUri(baseUrl), + name: cookie.name, + value: cookie.value, + domain: cookie.domain ?? 'sigarra.up.pt', + path: cookie.path ?? '/', + isSecure: cookie.secure, + isHttpOnly: cookie.httpOnly, + ); + } + }, + onLoadStop: (controller, url) async { + final urlString = url?.toString() ?? ''; + + if (urlString.contains('https://www.up')) { + setState(() { + isLoading = false; + }); + return; + } + + await controller.evaluateJavascript( + source: """ + const btn = document.getElementById('botao-mb'); + if (btn) btn.click(); + """, + ); + }, + ), + + if (isLoading) + Container( + color: Colors.white, + child: const Center(child: CircularProgressIndicator()), + ), + ], + ), + ), + ], + ); + } +} diff --git a/packages/uni_app/lib/view/current_account/widgets/transaction.dart b/packages/uni_app/lib/view/current_account/widgets/transaction.dart new file mode 100644 index 000000000..372ba70af --- /dev/null +++ b/packages/uni_app/lib/view/current_account/widgets/transaction.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/providers/riverpod/session_provider.dart'; +import 'package:uni/view/current_account/widgets/payment_webview.dart'; +import 'package:uni_ui/icons.dart'; +import 'package:uni_ui/theme.dart'; + +enum PaymentStatus { paid, pending, overdue } + +class Transaction extends ConsumerWidget { + const Transaction({ + super.key, + required this.description, + required this.date, + this.deadline, + required this.value, + this.interestOnLatePayment, + this.isUnpaid = false, + this.paymentLink, + }); + + final String description; + final DateTime date; + final DateTime? deadline; + final double value; + final double? interestOnLatePayment; + final bool isUnpaid; + final String? paymentLink; + + PaymentStatus get status { + if (deadline == null) { + return isUnpaid ? PaymentStatus.pending : PaymentStatus.paid; + } + + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + + if (deadline!.isBefore(today)) { + return PaymentStatus.overdue; + } + return PaymentStatus.pending; + } + + Color _dotColor(BuildContext context) => switch (status) { + PaymentStatus.paid => BadgeColors.pl, + PaymentStatus.pending => BadgeColors.ee, + PaymentStatus.overdue => BadgeColors.er, + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final sessionAsync = ref.watch(sessionProvider); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: ShapeDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + shadows: [BoxShadow(color: Colors.black.withValues(alpha: 0.03))], + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: paymentLink != null + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: _dotColor(context), + borderRadius: BorderRadius.circular(5), + ), + ), + const SizedBox(height: 8), + Text( + description, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + status == PaymentStatus.paid || deadline == null + ? "${date.day.toString().padLeft(2, '0')}-${date.month.toString().padLeft(2, '0')}-${date.year}" + : "${S.of(context).due_in} ${deadline!.day.toString().padLeft(2, '0')}-${deadline!.month.toString().padLeft(2, '0')}-${deadline!.year}", + ), + ], + ), + ), + const SizedBox(width: 24), + IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (paymentLink != null) + Align( + alignment: Alignment.centerRight, + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: Icon( + UniIcons.arrowSquareOut, + color: Theme.of(context).colorScheme.primary, + size: 18, + ), + onPressed: () { + sessionAsync.whenData((session) { + if (session != null) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + height: MediaQuery.sizeOf(context).height * 0.9, + child: PaymentWebView( + url: paymentLink!, + session: session, + ), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).failed_login), + ), + ); + } + }); + }, + visualDensity: VisualDensity.compact, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${(value / 100).toStringAsFixed(2)}€', + style: Theme.of(context).textTheme.headlineSmall, + ), + if (status == PaymentStatus.overdue && + interestOnLatePayment != null) ...[ + Text( + '+ ${(interestOnLatePayment! / 100).toStringAsFixed(2)}€', + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.primary, + ), + ), + Text( + S.of(context).interest_on_late_payments, + style: TextStyle( + fontSize: 8, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/uni_app/lib/view/current_account/widgets/transaction_filter_menu.dart b/packages/uni_app/lib/view/current_account/widgets/transaction_filter_menu.dart new file mode 100644 index 000000000..0907502ea --- /dev/null +++ b/packages/uni_app/lib/view/current_account/widgets/transaction_filter_menu.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni_ui/modal/modal.dart'; + +class TransactionFilterMenu extends StatefulWidget { + const TransactionFilterMenu({ + super.key, + required this.items, + required this.selectedValue, + required this.onSelectionChanged, + }); + + final List items; + final String selectedValue; + final void Function(String) onSelectionChanged; + + @override + State createState() => _TransactionFilterMenuState(); +} + +class _TransactionFilterMenuState extends State { + late String currentSelected; + + @override + void initState() { + super.initState(); + currentSelected = widget.selectedValue; + } + + @override + void didUpdateWidget(TransactionFilterMenu oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.selectedValue != widget.selectedValue) { + currentSelected = widget.selectedValue; + } + } + + void _showFilterDialog(BuildContext context) { + String dialogSelected = currentSelected; + + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return ModalDialog( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Text( + S.of(context).transactions, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + const Divider(height: 1), + const SizedBox(height: 8), + + ...widget.items.map((keyLabel) { + final isSelected = dialogSelected == keyLabel; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 1), + child: ListTile( + dense: true, + visualDensity: const VisualDensity(vertical: -2), + selected: isSelected, + selectedTileColor: Theme.of( + context, + ).colorScheme.primary.withAlpha(20), + title: Text( + _getFilterLabel(context, keyLabel), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + color: isSelected + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + trailing: isSelected + ? Icon( + Icons.check, + color: Theme.of(context).colorScheme.primary, + ) + : null, + onTap: () { + setModalState(() { + dialogSelected = keyLabel; + }); + }, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + ), + ), + ); + }), + + const SizedBox(height: 16), + + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + S.of(context).cancel, + style: const TextStyle(fontSize: 12), + ), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: () { + setState(() { + currentSelected = dialogSelected; + }); + widget.onSelectionChanged(currentSelected); + Navigator.of(context).pop(); + }, + child: Text( + S.of(context).apply, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ), + ], + ), + ], + ); + }, + ); + }, + ); + } + + String _getFilterLabel(BuildContext context, String key) { + switch (key) { + case 'Pending': + return S.of(context).pending; + case 'Tuition Fees': + return S.of(context).tuition_fees; + case 'General History': + return S.of(context).general_history; + default: + return key; + } + } + + @override + Widget build(BuildContext context) { + return TextButton( + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + shape: const StadiumBorder(), + ), + onPressed: () => _showFilterDialog(context), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _getFilterLabel(context, currentSelected), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(width: 4), + const Icon(Icons.keyboard_arrow_down, size: 16), + ], + ), + ); + } +} diff --git a/packages/uni_app/lib/view/profile/profile.dart b/packages/uni_app/lib/view/profile/profile.dart index f9606c7fb..6cf120aa3 100644 --- a/packages/uni_app/lib/view/profile/profile.dart +++ b/packages/uni_app/lib/view/profile/profile.dart @@ -1,13 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uni/generated/l10n.dart'; import 'package:uni/model/entities/profile.dart'; import 'package:uni/model/providers/riverpod/default_consumer.dart'; import 'package:uni/model/providers/riverpod/profile_provider.dart'; +import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/profile/profile_shimmer.dart'; -import 'package:uni/view/profile/widgets/profile_info.dart'; import 'package:uni/view/profile/widgets/profile_overview.dart'; import 'package:uni/view/profile/widgets/settings.dart'; import 'package:uni/view/widgets/pages_layouts/secondary/secondary.dart'; +import 'package:uni_ui/cards/generic_card.dart'; +import 'package:uni_ui/cards/profile_list_tile.dart'; +import 'package:uni_ui/icons.dart'; class ProfilePageView extends ConsumerStatefulWidget { const ProfilePageView({super.key}); @@ -24,17 +28,35 @@ class ProfilePageViewState extends SecondaryPageViewState { children: [ DefaultConsumer( provider: profileProvider, - builder: (context, ref, profile) => Column( - children: [ - ProfileOverview(profile: profile), - const ProfileInfo(), - ], - ), + builder: (context, ref, profile) => ProfileOverview(profile: profile), hasContent: (profile) => profile.courses.isNotEmpty, loadingWidget: const ProfileCardShimmer(), nullContentWidget: Container(), ), - + GenericCard( + tooltip: S.of(context).current_account, + margin: const EdgeInsets.only( + top: 20, + left: 20, + right: 20, + bottom: 10, + ), + child: ProfileListTile( + icon: UniIcons.bank, + title: S.of(context).current_account, + subtitle: S.of(context).current_account_description, + trailing: UniIcon( + UniIcons.caretRight, + color: Theme.of(context).colorScheme.primary, + ), + onTap: () { + Navigator.pushNamed( + context, + '/${NavigationItem.navCurrentAccount.route}', + ); + }, + ), + ), const Settings(), ], ); diff --git a/packages/uni_app/pubspec.yaml b/packages/uni_app/pubspec.yaml index ed70214da..6f36f2ff1 100644 --- a/packages/uni_app/pubspec.yaml +++ b/packages/uni_app/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: sdk: flutter flutter_cache_manager: ^3.4.1 flutter_dotenv: ^6.0.0 + flutter_inappwebview: ^6.0.0 flutter_local_notifications: ^20.0.0 flutter_localizations: sdk: flutter @@ -87,6 +88,7 @@ dependencies: path: workmanager ref: "4ce065" + dev_dependencies: build_runner: ^2.7.1 custom_lint: ^0.8.1 diff --git a/packages/uni_app/windows/flutter/generated_plugins.cmake b/packages/uni_app/windows/flutter/generated_plugins.cmake index ef31d9dc3..4004491be 100644 --- a/packages/uni_app/windows/flutter/generated_plugins.cmake +++ b/packages/uni_app/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST battery_plus connectivity_plus file_selector_windows + flutter_inappwebview_windows flutter_secure_storage_windows objectbox_flutter_libs sentry_flutter diff --git a/packages/uni_ui/lib/icons.dart b/packages/uni_ui/lib/icons.dart index 662e5c79a..271752405 100644 --- a/packages/uni_ui/lib/icons.dart +++ b/packages/uni_ui/lib/icons.dart @@ -94,6 +94,11 @@ class UniIcons { static const courseUnit = PhosphorIconsDuotone.chalkboardTeacher; static const warning = PhosphorIconsDuotone.warningOctagon; + + // current account icons + static const piggyBank = PhosphorIconsDuotone.piggyBank; + static const calendarDots = PhosphorIconsDuotone.calendarDots; + static const bank = PhosphorIconsDuotone.bank; } // The same as default Icon class from material.dart but allowing to use PhosphorIcons duotone icons diff --git a/pubspec.lock b/pubspec.lock index ed88702b7..7f10d028d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -575,6 +575,70 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: e30fba942e3debea7b7e6cdd4f0f59ce89dd403a9865193e3221293b6d1544c6 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" flutter_launcher_icons: dependency: transitive description: @@ -1026,10 +1090,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -1720,26 +1784,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.15" timelines: dependency: transitive description: