From 5332df66d012e93b80e253d4a98bea9b3a74be45 Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Sat, 2 Mar 2024 17:41:38 +0100 Subject: [PATCH 01/16] Add webview for ticket --- android/app/build.gradle | 20 +- android/build.gradle | 16 -- android/settings.gradle | 31 ++- lib/pages/feed/feed_page.dart | 2 +- lib/pages/feed/widgets/video_player.dart | 4 +- lib/pages/mensa/mensa_page.dart | 6 +- lib/pages/wallet/widgets/wallet.dart | 59 +----- lib/utils/constants.dart | 6 +- pubspec.lock | 232 ++++++++++++----------- 9 files changed, 171 insertions(+), 205 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6feb7c36..cbd9ea8f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,10 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" + id "com.google.gms.google-services" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,10 +13,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { @@ -27,13 +30,6 @@ if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } -apply plugin: 'com.android.application' -// START: FlutterFire Configuration -apply plugin: 'com.google.gms.google-services' -// END: FlutterFire Configuration -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdk 34 @@ -81,7 +77,7 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.20" implementation('androidx.appcompat:appcompat:1.6.1') implementation("androidx.appcompat:appcompat-resources:1.6.1") } diff --git a/android/build.gradle b/android/build.gradle index 4f656811..bc157bd1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,19 +1,3 @@ -buildscript { - ext.kotlin_version = '1.9.20' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - // START: FlutterFire Configuration - classpath 'com.google.gms:google-services:4.3.14' - // END: FlutterFire Configuration - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf..43f1da8d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.20" apply false + id "com.google.gms.google-services" version "4.4.0" apply false +} + +include ":app" diff --git a/lib/pages/feed/feed_page.dart b/lib/pages/feed/feed_page.dart index 11f466f4..9a23d970 100644 --- a/lib/pages/feed/feed_page.dart +++ b/lib/pages/feed/feed_page.dart @@ -1,5 +1,4 @@ import 'dart:io' show Platform; -import 'package:campus_app/utils/widgets/scroll_to_top_button.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -20,6 +19,7 @@ import 'package:campus_app/utils/pages/feed_utils.dart'; import 'package:campus_app/utils/widgets/campus_icon_button.dart'; import 'package:campus_app/utils/widgets/campus_segmented_control.dart'; import 'package:campus_app/utils/widgets/campus_search_bar.dart'; +import 'package:campus_app/utils/widgets/scroll_to_top_button.dart'; class FeedPage extends StatefulWidget { final GlobalKey mainNavigatorKey; diff --git a/lib/pages/feed/widgets/video_player.dart b/lib/pages/feed/widgets/video_player.dart index 28ff685d..1bd4aa4b 100644 --- a/lib/pages/feed/widgets/video_player.dart +++ b/lib/pages/feed/widgets/video_player.dart @@ -30,7 +30,7 @@ class FeedVideoPlayer extends StatefulWidget { class _FeedVideoPlayerState extends State { /// The controller object to handle video player - late VideoPlayerController _videoPlayerController; + late CachedVideoPlayerController _videoPlayerController; late CustomVideoPlayerController _customVideoPlayerController; // Show replay instead of pause / play button @@ -39,7 +39,7 @@ class _FeedVideoPlayerState extends State { @override void initState() { super.initState(); - _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.url))..initialize(); + _videoPlayerController = CachedVideoPlayerController.network(widget.url)..initialize(); _customVideoPlayerController = CustomVideoPlayerController( context: context, videoPlayerController: _videoPlayerController, diff --git a/lib/pages/mensa/mensa_page.dart b/lib/pages/mensa/mensa_page.dart index 5baff9a4..426db1e0 100644 --- a/lib/pages/mensa/mensa_page.dart +++ b/lib/pages/mensa/mensa_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io' show Platform; -import 'package:campus_app/utils/widgets/scroll_to_top_button.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -10,13 +9,14 @@ import 'package:campus_app/core/injection.dart'; import 'package:campus_app/core/failures.dart'; import 'package:campus_app/pages/mensa/dish_entity.dart'; import 'package:campus_app/pages/mensa/mensa_usecases.dart'; -import 'package:campus_app/utils/pages/mensa_utils.dart'; import 'package:campus_app/pages/home/widgets/page_navigation_animation.dart'; -import 'package:campus_app/utils/widgets/campus_button.dart'; import 'package:campus_app/pages/mensa/widgets/day_selection.dart'; import 'package:campus_app/pages/mensa/widgets/expandable_restaurant.dart'; import 'package:campus_app/pages/mensa/widgets/preferences_popup.dart'; import 'package:campus_app/pages/mensa/widgets/allergenes_popup.dart'; +import 'package:campus_app/utils/widgets/campus_button.dart'; +import 'package:campus_app/utils/widgets/scroll_to_top_button.dart'; +import 'package:campus_app/utils/pages/mensa_utils.dart'; class MensaPage extends StatefulWidget { final GlobalKey mainNavigatorKey; diff --git a/lib/pages/wallet/widgets/wallet.dart b/lib/pages/wallet/widgets/wallet.dart index 36af6de4..d8ce5a05 100644 --- a/lib/pages/wallet/widgets/wallet.dart +++ b/lib/pages/wallet/widgets/wallet.dart @@ -2,22 +2,22 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; -import 'package:campus_app/core/settings.dart'; -import 'package:campus_app/pages/wallet/ticket_fullscreen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; + import 'package:pdf_image_renderer/pdf_image_renderer.dart'; import 'package:pdfx/pdfx.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:syncfusion_flutter_pdf/pdf.dart' as sync_pdf; import 'package:path_provider/path_provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:screen_brightness/screen_brightness.dart'; -import 'package:fluttertoast/fluttertoast.dart'; +import 'package:campus_app/core/settings.dart'; import 'package:campus_app/core/themes.dart'; +import 'package:campus_app/pages/wallet/ticket_fullscreen.dart'; import 'package:campus_app/pages/wallet/widgets/stacked_card_carousel.dart'; +import 'package:campus_app/pages/more/in_app_web_view_page.dart'; import 'package:campus_app/utils/widgets/custom_button.dart'; +import 'package:campus_app/utils/constants.dart'; class CampusWallet extends StatelessWidget { const CampusWallet({Key? key}) : super(key: key); @@ -133,54 +133,7 @@ class _BogestraTicketState extends State with AutomaticKeepAlive } Future addTicket() async { - FilePickerResult? result; - - try { - result = await FilePicker.platform.pickFiles(); - } catch (e) { - debugPrint('Access files permission not granted.'); - } - - if (result != null) { - final File file = File(result.files.single.path!); - - final String fileType = file.path.substring(file.path.lastIndexOf('.')); - - if (fileType != '.pdf') { - await Fluttertoast.showToast(msg: 'Ungültiges Ticket!', timeInSecForIosWeb: 3, gravity: ToastGravity.TOP); - return; - } - - // Load the pdf file - final sync_pdf.PdfDocument document = sync_pdf.PdfDocument(inputBytes: await file.readAsBytes()); - - // Get the pdf text - final String pdfText = sync_pdf.PdfTextExtractor(document).extractText(startPageIndex: 0); - - // Remove the pdf file from memory for efficiency reasons - document.dispose(); - - // Check if the pdf file is a valid ticket - if (!pdfText.contains('Ticket')) { - await Fluttertoast.showToast(msg: 'Ungültiges Ticket!', timeInSecForIosWeb: 3, gravity: ToastGravity.TOP); - return; - } - - // Save the picked pdf file - unawaited(saveTicketPDF(file)); - - // Parse the picked pdf - final Image semesterTicketImage = await renderSemesterTicket(file.path); - final Image qrCodeImage = await renderQRCode(file.path); - - setState(() { - scanned = true; - this.semesterTicketImage = semesterTicketImage; - this.qrCodeImage = qrCodeImage; - }); - } else { - // User canceled the picker - } + Navigator.push(context, MaterialPageRoute(builder: (context) => InAppWebViewPage(url: rideTicketing))); } @override diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 89cd92f9..b03e4c9a 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -3,6 +3,8 @@ import 'package:campus_app/env/env.dart'; const String appWordpressHost = 'https://app.asta-bochum.de'; +const String appwrite = 'https://api-app.asta-bochum.de/v1'; + const String astaEvents = 'https://asta-bochum.de/wp-json/tribe/events/v1/events'; const String astaFeed = 'https://asta-bochum.de/wp-json/wp/v2/posts?per_page=20'; const String astaFavicon = 'https://asta-bochum.de/wp-content/themes/rt_notio/custom/images/favicon.ico'; @@ -10,12 +12,12 @@ const String appEvents = 'https://app.asta-bochum.de/wp-json/tribe/events/v1/eve const String appFeed = 'https://app.asta-bochum.de/wp-json/wp/v2/posts'; const String rubNewsfeed = 'https://news.rub.de/newsfeed'; // there is no non-german -const String appwrite = 'https://api-app.asta-bochum.de/v1'; - const String mensaData = 'https://api-app.asta-bochum.de/get_meal'; const String osrmBackend = 'https://osrm.app.asta-bochum.de'; +const String rideTicketing = 'https://abo.ride-ticketing.de/app/login?partnerId=61b1cbf4604e623aef325ef0e4226cea'; + // See: https://codewithandrea.com/articles/flutter-api-keys-dart-define-env-files/ final String mensaApiKey = Env.mensaApiKey; final String firebaseAndroidApiKey = Env.firebaseAndroidApiKey; diff --git a/pubspec.lock b/pubspec.lock index 380205a1..6d616838 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" animations: dependency: "direct main" description: @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: appinio_video_player - sha256: f39a7fdebda31f72970a79a4263ceb049f2958dba9e87403235fec32d5a3ff58 + sha256: "43b5a269d461a8ec37394ee12c6e3461e8348b9127f9928e49b84adb90c5cf86" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" appwrite: dependency: "direct main" description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.8.1" + version: "8.9.1" cached_network_image: dependency: "direct main" description: @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cached_video_player: + dependency: transitive + description: + name: cached_video_player + sha256: "13c25fc1af3bb239da83d9e965d119463a67a782fd9af3714ed86a1182ded20c" + url: "https://pub.dev" + source: hosted + version: "2.0.4" characters: dependency: transitive description: @@ -261,10 +269,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: dependency: transitive description: @@ -293,10 +301,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "25b4624c231844a7a70a3817a729a6190a751ef1c07ded256e126a3b72261444" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" dartz: dependency: "direct main" description: @@ -317,10 +325,10 @@ packages: dependency: transitive description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -333,10 +341,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" dio_cookie_manager: dependency: "direct main" description: @@ -381,10 +389,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -498,10 +506,10 @@ packages: dependency: transitive description: name: flutter_inappwebview_android - sha256: fd4db51e46f49b140d83a3206851432c54ea920b381137c0ba82d0cf59be1dee + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 url: "https://pub.dev" source: hosted - version: "1.0.12" + version: "1.0.13" flutter_inappwebview_internal_annotations: dependency: transitive description: @@ -562,10 +570,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535" + sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 url: "https://pub.dev" source: hosted - version: "16.3.0" + version: "16.3.2" flutter_local_notifications_linux: dependency: transitive description: @@ -591,10 +599,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "9cdb5d9665dab5d098dc50feab74301c2c228cd02ca25c9b546ab572cebcd6af" + sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.10" flutter_nfc_kit: dependency: "direct main" description: @@ -673,10 +681,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -723,10 +731,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: d0b88dc35a7f97fd91fec0cf8f165abd97a57977968d8fc02ba0bc92e14ba07e + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 url: "https://pub.dev" source: hosted - version: "7.6.6" + version: "7.6.7" glob: dependency: transitive description: @@ -803,10 +811,10 @@ packages: dependency: transitive description: name: image - sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.1.7" intl: dependency: "direct main" description: @@ -847,6 +855,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -875,42 +907,42 @@ packages: dependency: "direct main" description: name: lottie - sha256: "1f0ce68112072d66ea271a9841994fa8d16442e23d8cf8996c9fa74174e58b4e" + sha256: ce2bb2605753915080e4ee47f036a64228c88dc7f56f7bc1dbe912d75b55b1e2 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mockito: dependency: "direct dev" description: @@ -987,10 +1019,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1060,10 +1092,10 @@ packages: description: path: "packages/pdfx" ref: HEAD - resolved-ref: d637108a2a6e3e97a70304f00f1eda9511fb4f92 + resolved-ref: f4a395280bc7097388280e6862c918c51390c67b url: "https://github.com/ScerIO/packages.flutter" source: git - version: "2.5.0" + version: "2.6.0" petitparser: dependency: transitive description: @@ -1116,10 +1148,10 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1204,26 +1236,26 @@ packages: dependency: "direct main" description: name: sentry - sha256: "89e426587b0879e53c46a0aae0eb312696d9d2d803ba14b252a65cc24b1416a2" + sha256: d2ee9c850d876d285f22e2e662f400ec2438df9939fe4acd5d780df9841794ce url: "https://pub.dev" source: hosted - version: "7.14.0" + version: "7.16.1" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: fd089ee4e75a927be037c56815a0a54af5a519f52b803a5ffecb589bb36e2401 + sha256: "5b428c189c825f16fb14e9166529043f06b965d5b59bfc3a1415e39c082398c0" url: "https://pub.dev" source: hosted - version: "7.14.0" + version: "7.16.1" share_plus: dependency: "direct main" description: name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: @@ -1313,18 +1345,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" url: "https://pub.dev" source: hosted - version: "2.5.0+2" + version: "2.5.3" stack_trace: dependency: transitive description: @@ -1473,26 +1505,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96 + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.2.3" + version: "6.2.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_linux: dependency: transitive description: @@ -1513,18 +1545,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -1545,26 +1577,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1573,46 +1605,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - video_player: - dependency: transitive - description: - name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 - url: "https://pub.dev" - source: hosted - version: "2.8.2" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" - url: "https://pub.dev" - source: hosted - version: "2.4.11" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" - url: "https://pub.dev" - source: hosted - version: "2.5.6" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + sha256: "318a6d20577e1c78cf0bf40670883cc571ea860c72a4f7426d7dacce4bdd4343" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" + sha256: fb3bbeaf0302cb0c31340ebd6075487939aa1fe3b379d1a8784ef852b679940e url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.0.15" video_thumbnail: dependency: "direct main" description: @@ -1629,6 +1637,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" watcher: dependency: transitive description: @@ -1641,18 +1657,18 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" win32: dependency: transitive description: @@ -1702,5 +1718,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" From e5470c191fc442416c4e2ec532f8d0f83cce201b Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Sun, 3 Mar 2024 22:09:56 +0100 Subject: [PATCH 02/16] Ticket webview --- .../wallet/widgets/ticket_web_view_page.dart | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 lib/pages/wallet/widgets/ticket_web_view_page.dart diff --git a/lib/pages/wallet/widgets/ticket_web_view_page.dart b/lib/pages/wallet/widgets/ticket_web_view_page.dart new file mode 100644 index 00000000..2a5c0bd6 --- /dev/null +++ b/lib/pages/wallet/widgets/ticket_web_view_page.dart @@ -0,0 +1,115 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +import 'package:campus_app/core/themes.dart'; +import 'package:campus_app/main.dart'; +import 'package:campus_app/utils/widgets/campus_icon_button.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +/// This page shows an [InAppWebView] in order to display external +/// websites from the helpful ressources that are not yet natively implemented +class TicketWebViewPage extends StatefulWidget { + /// The url that should be opened + final String url; + + const TicketWebViewPage({ + Key? key, + required this.url, + }) : super(key: key); + + @override + State createState() => _TicketWebViewPageState(); +} + +class _TicketWebViewPageState extends State { + InAppWebViewController? webViewController; + late PullToRefreshController pullToRefreshController; + + InAppWebViewSettings settings = InAppWebViewSettings( + mediaPlaybackRequiresUserGesture: false, + verticalScrollBarEnabled: false, + horizontalScrollBarEnabled: false, + allowsInlineMediaPlayback: true, + useHybridComposition: false, + ); + + @override + void initState() { + super.initState(); + + pullToRefreshController = PullToRefreshController( + settings: PullToRefreshSettings(color: Colors.black), + onRefresh: () async { + if (Platform.isAndroid) { + await webViewController?.reload(); + } else if (Platform.isIOS) { + await webViewController?.loadUrl(urlRequest: URLRequest(url: await webViewController?.getUrl())); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvoked: (didPop) async { + if (didPop) return; + final navigator = Navigator.of(context); + if (await webViewController!.canGoBack()) { + await webViewController?.goBack(); + } else { + if (homeKey.currentState != null) { + homeKey.currentState!.setSwipeDisabled(); + } + navigator.pop(); + } + }, + child: VisibilityDetector( + onVisibilityChanged: (info) { + final bool isVisible = info.visibleFraction > 0; + + if (isVisible) { + if (homeKey.currentState != null) { + homeKey.currentState!.setSwipeDisabled(disableSwipe: true); + } + } + }, + key: const Key('visibility-key'), + child: Scaffold( + backgroundColor: Provider.of(context).currentThemeData.colorScheme.background, + body: SafeArea( + child: Stack( + children: [ + InAppWebView( + gestureRecognizers: >{} + ..add(const Factory(VerticalDragGestureRecognizer.new)), + pullToRefreshController: pullToRefreshController, + initialSettings: settings, + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + onWebViewCreated: (controller) { + webViewController = controller; + }, + ), + // Back button + Padding( + padding: const EdgeInsets.only(top: 20, left: 20, right: 20), + child: CampusIconButton( + iconPath: 'assets/img/icons/arrow-left.svg', + onTap: () { + Navigator.maybePop(context); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} From 960a11a7c41f64a0a476c480dc2f38d360735ad9 Mon Sep 17 00:00:00 2001 From: henry-herrmann Date: Sun, 3 Mar 2024 22:17:02 +0100 Subject: [PATCH 03/16] Update pods & iOS project --- ios/Podfile.lock | 58 +++++++++++--------- ios/Runner.xcodeproj/project.pbxproj | 12 ++--- pubspec.lock | 80 +++++++++------------------- 3 files changed, 62 insertions(+), 88 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b3dea7f0..cc4b8359 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,8 @@ PODS: + - cached_video_player (0.0.2): + - Flutter + - KTVHTTPCache (~> 2.0.0) + - CocoaAsyncSocket (7.6.5) - cupertino_http (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -90,9 +94,6 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - GoogleDataTransport (9.3.0): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) @@ -114,6 +115,10 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/UserDefaults (7.12.0): - GoogleUtilities/Logger + - KTVCocoaHTTPServer (1.0.0): + - CocoaAsyncSocket + - KTVHTTPCache (2.0.1): + - KTVCocoaHTTPServer - libwebp (1.3.2): - libwebp/demux (= 1.3.2) - libwebp/mux (= 1.3.2) @@ -147,32 +152,30 @@ PODS: - SDWebImage (5.18.5): - SDWebImage/Core (= 5.18.5) - SDWebImage/Core (5.18.5) - - Sentry/HybridSDK (8.17.2): - - SentryPrivate (= 8.17.2) + - Sentry/HybridSDK (8.20.0): + - SentryPrivate (= 8.20.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.17.2) - - SentryPrivate (8.17.2) + - Sentry/HybridSDK (= 8.20.0) + - SentryPrivate (8.20.0) - share_plus (0.0.1): - Flutter - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) + - FlutterMacOS - SwiftyGif (5.4.4) - Toast (4.0.0) - uni_links (0.0.1): - Flutter - url_launcher_ios (0.0.1): - Flutter - - video_player_avfoundation (0.0.1): - - Flutter - - FlutterMacOS - video_thumbnail (0.0.1): - Flutter - libwebp DEPENDENCIES: + - cached_video_player (from `.symlinks/plugins/cached_video_player/ios`) - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -193,14 +196,14 @@ DEPENDENCIES: - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) SPEC REPOS: trunk: + - CocoaAsyncSocket - DKImagePickerController - DKPhotoGallery - Firebase @@ -208,9 +211,10 @@ SPEC REPOS: - FirebaseCoreInternal - FirebaseInstallations - FirebaseMessaging - - FMDB - GoogleDataTransport - GoogleUtilities + - KTVCocoaHTTPServer + - KTVHTTPCache - libwebp - nanopb - OrderedSet @@ -222,6 +226,8 @@ SPEC REPOS: - Toast EXTERNAL SOURCES: + cached_video_player: + :path: ".symlinks/plugins/cached_video_player/ios" cupertino_http: :path: ".symlinks/plugins/cupertino_http/ios" device_info_plus: @@ -263,17 +269,17 @@ EXTERNAL SOURCES: share_plus: :path: ".symlinks/plugins/share_plus/ios" sqflite: - :path: ".symlinks/plugins/sqflite/ios" + :path: ".symlinks/plugins/sqflite/darwin" uni_links: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - video_player_avfoundation: - :path: ".symlinks/plugins/video_player_avfoundation/darwin" video_thumbnail: :path: ".symlinks/plugins/video_thumbnail/ios" SPEC CHECKSUMS: + cached_video_player: 9f225ed1c46bda0fe287b8288742329d441e2cc5 + CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 cupertino_http: 5f8b1161107fe6c8d94a0c618735a033d93fa7db device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac @@ -288,15 +294,16 @@ SPEC CHECKSUMS: FirebaseMessaging: 9bc34a98d2e0237e1b121915120d4d48ddcf301e Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 - flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_nfc_kit: 965c98c3fa68f5609f1cc89abb968fe1b8ffdbaa flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_web_auth_2: a1bc00762c408a8f80b72a538cd7ff5b601c3e71 fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + KTVCocoaHTTPServer: df8d7b861e603ff8037e9b2138aca2563a6b768d + KTVHTTPCache: 588c3eb16f6bd1e6fde1e230dabfb7bd4e490a4d libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c @@ -307,18 +314,17 @@ SPEC CHECKSUMS: PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 SDWebImage: 7ac2b7ddc5e8484c79aa90fc4e30b149d6a2c88f - Sentry: 64a9f9c3637af913adcf53deced05bbe452d1410 - sentry_flutter: 57912cf425e09398bdf47f38842a1fcb9836f1be - SentryPrivate: 024c6fed507ac39ae98e6d087034160f942920d5 + Sentry: a8d7b373b9f9868442b02a0c425192f693103cbf + sentry_flutter: 03e7660857a8cdb236e71456a7e8447b65c8a788 + SentryPrivate: 006b24af16828441f70e2ab6adf241bd0a8ad130 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 - video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 + url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5ffa50cb..930ae11d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -369,7 +369,7 @@ CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KAT5FS743J; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HCY5D33537; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Campus App"; @@ -382,7 +382,7 @@ PRODUCT_BUNDLE_IDENTIFIER = de.astaBochum.campusApp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Apple Distribution Profile"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Distribution_Profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -508,7 +508,7 @@ CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KAT5FS743J; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HCY5D33537; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Campus App"; @@ -521,7 +521,7 @@ PRODUCT_BUNDLE_IDENTIFIER = de.astaBochum.campusApp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Apple Distribution Profile"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Distribution_Profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -541,7 +541,7 @@ CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KAT5FS743J; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HCY5D33537; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Campus App"; @@ -554,7 +554,7 @@ PRODUCT_BUNDLE_IDENTIFIER = de.astaBochum.campusApp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Apple Distribution Profile"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Distribution_Profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/pubspec.lock b/pubspec.lock index 6d616838..bdea3620 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "64.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.2.0" animations: dependency: "direct main" description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.3+8" crypto: dependency: transitive description: @@ -389,10 +389,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.0" file: dependency: transitive description: @@ -855,30 +855,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" lints: dependency: transitive description: @@ -915,26 +891,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.10.0" mime: dependency: transitive description: @@ -1019,10 +995,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_parsing: dependency: transitive description: @@ -1553,10 +1529,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.3" url_launcher_windows: dependency: transitive description: @@ -1637,14 +1613,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0+2" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 - url: "https://pub.dev" - source: hosted - version: "13.0.0" watcher: dependency: transitive description: @@ -1657,18 +1625,18 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.3.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.0" win32: dependency: transitive description: @@ -1718,5 +1686,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.2.3 <4.0.0" + flutter: ">=3.16.6" From 333cde650b04af9fd3e2784b933f1e9e27ba263a Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Wed, 13 Mar 2024 15:01:40 +0100 Subject: [PATCH 04/16] Convert ticket web view page into a datasource using a headless web view --- lib/env/env.g.dart | 864 ++++++++++-------- .../wallet/ticket/ticket_datasource.dart | 135 +++ .../wallet/widgets/ticket_web_view_page.dart | 115 --- lib/pages/wallet/widgets/wallet.dart | 46 +- lib/utils/pages/main_utils.dart | 1 - pubspec.lock | 86 +- pubspec.yaml | 1 + .../calendar_datasource_test.mocks.dart | 10 +- .../calendar_repository_test.mocks.dart | 14 +- .../calendar_usecases_test.mocks.dart | 4 +- .../mensa/mensa_datasource_test.mocks.dart | 10 +- .../mensa/mensa_repository_test.mocks.dart | 4 +- .../mensa/mensa_usecases_test.mocks.dart | 4 +- .../news/news_repository_test.mocks.dart | 14 +- test/pages/news/news_usecases_test.mocks.dart | 4 +- .../news/rubnews_datasource_test.mocks.dart | 10 +- 16 files changed, 782 insertions(+), 540 deletions(-) create mode 100644 lib/pages/wallet/ticket/ticket_datasource.dart delete mode 100644 lib/pages/wallet/widgets/ticket_web_view_page.dart diff --git a/lib/env/env.g.dart b/lib/env/env.g.dart index 994346aa..04a4f670 100644 --- a/lib/env/env.g.dart +++ b/lib/env/env.g.dart @@ -6,374 +6,524 @@ part of 'env.dart'; // EnviedGenerator // ************************************************************************** -class _Env { - static const List _enviedkeymensaApiKey = [ - 499615877, - 2599506896, - 3439962914, - 3304992344, - 4181183304, - 378115746, - 1405070898, - 3236357814, - 2515773368, - 2433840638, - 3024590752, - 107037633, - 1285716260, - 2414403300, - 3691118631, - 3723146539, - 2034370242, - 1164921063, - 1538208806, - 1323409084, - 1441418503, - 1175265975, - 934925641, - 2464522931, - 4249069102, - 4117353468, - 1016945719, - 2041707029, - 2882093233, - 3982794431, - 2803932485, - 421321559, +// coverage:ignore-file +// ignore_for_file: type=lint +final class _Env { + static const List _enviedkeymensaApiKey = [ + 2478872583, + 2132561946, + 3750844218, + 1716735487, + 2971617009, + 3548924964, + 37865034, + 3433624950, + 2527023148, + 3839587069, + 1179952306, + 1789833538, + 1609289118, + 2669206434, + 1753807060, + 3162320412, + 1034763632, + 2352393270, + 1017302866, + 1480300283, + 57959766, + 1930292261, + 3254286715, + 669070198, + 2349228522, + 829661861, + 2271098589, + 3293035083, + 853472123, + 2434144526, + 1144761493, + 2042665095, ]; - static const List _envieddatamensaApiKey = [ - 499615943, - 2599506869, - 3439962947, - 3304992298, - 4181183277, - 378115792, - 1405070866, - 3236357851, - 2515773405, - 2433840528, - 3024590803, - 107037600, - 1285716347, - 2414403223, - 3691118658, - 3723146568, - 2034370224, - 1164920962, - 1538208850, - 1323409123, - 1441418598, - 1175265991, - 934925600, - 2464522988, - 4249069125, - 4117353369, - 1016945742, - 2041707082, - 2882093184, - 3982794380, - 2803932534, - 421321568, + + static const List _envieddatamensaApiKey = [ + 2478872645, + 2132562047, + 3750844251, + 1716735373, + 2971616916, + 3548925014, + 37865066, + 3433624859, + 2527023177, + 3839586963, + 1179952321, + 1789833507, + 1609289153, + 2669206481, + 1753807025, + 3162320511, + 1034763522, + 2352393299, + 1017302822, + 1480300196, + 57959735, + 1930292309, + 3254286610, + 669070121, + 2349228417, + 829661888, + 2271098532, + 3293035028, + 853472074, + 2434144573, + 1144761510, + 2042665136, ]; - static final String mensaApiKey = String.fromCharCodes( - List.generate(_envieddatamensaApiKey.length, (i) => i, growable: false) - .map((i) => _envieddatamensaApiKey[i] ^ _enviedkeymensaApiKey[i]) - .toList(growable: false), - ); - static const List _enviedkeyfirebaseAndroidApiKey = [ - 1912014174, - 1213452383, - 1015020074, - 3621854356, - 1793492138, - 2065598005, - 676543619, - 1102365091, - 2721010071, - 2976267952, - 546776359, - 1299487310, - 1975715727, - 2418406963, - 4044334181, - 177876799, - 4116100918, - 2821189044, - 829015196, - 362364936, - 1574090904, - 3752374453, - 2334501907, - 2970900318, - 3108347875, - 2278580407, - 3773042871, - 3632499665, - 3491373822, - 1061225023, - 2913491971, - 1226447574, - 908496605, - 2832246967, - 4090720556, - 1532599614, - 1280752820, - 3997084342, - 1026718960, + + static final String mensaApiKey = String.fromCharCodes(List.generate( + _envieddatamensaApiKey.length, + (int i) => i, + growable: false, + ).map((int i) => _envieddatamensaApiKey[i] ^ _enviedkeymensaApiKey[i])); + + static const List _enviedkeyfirebaseAndroidApiKey = [ + 1369112136, + 4029571444, + 3264223869, + 1351668967, + 471020085, + 3945981759, + 517383963, + 1975313234, + 1053960370, + 3588833899, + 3356976369, + 4030755356, + 3629860963, + 1124861821, + 1016355974, + 1382618155, + 2397003379, + 3104515804, + 826348540, + 1125850651, + 1172961691, + 3468879278, + 2467412073, + 1028958808, + 3115206262, + 1533626503, + 3274281873, + 2781504856, + 1929828016, + 76794923, + 4011019275, + 810629213, + 1508314797, + 2192643563, + 44299158, + 1738098949, + 3663046590, + 4157792558, + 1270653908, ]; - static const List _envieddatafirebaseAndroidApiKey = [ - 1912014111, - 1213452310, - 1015020112, - 3621854453, - 1793492217, - 2065598028, - 676543681, - 1102365140, - 2721010132, - 2976268023, - 546776417, - 1299487350, - 1975715807, - 2418407005, - 4044334102, - 177876846, - 4116100867, - 2821189070, - 829015240, - 362364994, - 1574090963, - 3752374464, - 2334501950, - 2970900278, - 3108347862, - 2278580358, - 3773042816, - 3632499635, - 3491373717, - 1061224970, - 2913492059, - 1226447504, - 908496542, - 2832246990, - 4090720618, - 1532599627, - 1280752849, - 3997084378, - 1026718871, + + static const List _envieddatafirebaseAndroidApiKey = [ + 1369112073, + 4029571389, + 3264223751, + 1351668870, + 471020134, + 3945981766, + 517384025, + 1975313189, + 1053960433, + 3588833836, + 3356976311, + 4030755364, + 3629860915, + 1124861715, + 1016356085, + 1382618234, + 2397003334, + 3104515750, + 826348456, + 1125850705, + 1172961744, + 3468879323, + 2467412036, + 1028958768, + 3115206211, + 1533626550, + 3274281894, + 2781504826, + 1929828059, + 76794910, + 4011019347, + 810629147, + 1508314862, + 2192643474, + 44299216, + 1738099056, + 3663046619, + 4157792578, + 1270653875, ]; + static final String firebaseAndroidApiKey = String.fromCharCodes( - List.generate(_envieddatafirebaseAndroidApiKey.length, (i) => i, growable: false) - .map((i) => _envieddatafirebaseAndroidApiKey[i] ^ _enviedkeyfirebaseAndroidApiKey[i]) - .toList(growable: false), - ); - static const List _enviedkeyfirebaseIosApiKey = [ - 3234232084, - 1340439386, - 504245731, - 1332635209, - 459529247, - 4112214202, - 2661407361, - 537776507, - 299129246, - 1669987453, - 2454531307, - 1071578478, - 1879354564, - 344486939, - 3630042050, - 3067937494, - 2783809509, - 2775937044, - 2315735456, - 897082356, - 3508831622, - 1903605102, - 2315543371, - 1975602613, - 4251954959, - 13850945, - 2428281642, - 300797010, - 3155678803, - 1363143135, - 3186457102, - 3130858887, - 1489881246, - 3222425423, - 2066800751, - 3942141724, - 2520324321, - 3044178090, - 1218444269, + List.generate( + _envieddatafirebaseAndroidApiKey.length, + (int i) => i, + growable: false, + ).map((int i) => + _envieddatafirebaseAndroidApiKey[i] ^ + _enviedkeyfirebaseAndroidApiKey[i])); + + static const List _enviedkeyfirebaseIosApiKey = [ + 2506337707, + 2717631155, + 539259100, + 829110730, + 2367959603, + 3039928276, + 4191457059, + 923406485, + 2621276653, + 3333526944, + 2213211507, + 3632003430, + 550266588, + 311635769, + 394648262, + 2793546101, + 863246209, + 78055949, + 3874252684, + 322751879, + 4276002628, + 2287877577, + 141212620, + 2772268903, + 3013514611, + 2290127764, + 3410601825, + 4105952212, + 1705725650, + 333864281, + 87719712, + 2429871858, + 3378982779, + 4156516983, + 2177793786, + 2187546319, + 1526161990, + 3146431255, + 202399793, ]; - static const List _envieddatafirebaseIosApiKey = [ - 3234232149, - 1340439315, - 504245657, - 1332635176, - 459529292, - 4112214211, - 2661407424, - 537776420, - 299129301, - 1669987382, - 2454531242, - 1071578390, - 1879354550, - 344487021, - 3630042000, - 3067937410, - 2783809408, - 2775937136, - 2315735545, - 897082316, - 3508831729, - 1903605000, - 2315543420, - 1975602683, - 4251955044, - 13850902, - 2428281726, - 300797031, - 3155678732, - 1363143151, - 3186457209, - 3130858973, - 1489881290, - 3222425385, - 2066800663, - 3942141814, - 2520324242, - 3044178117, - 1218444186, + + static const List _envieddatafirebaseIosApiKey = [ + 2506337770, + 2717631226, + 539259046, + 829110699, + 2367959648, + 3039928237, + 4191457122, + 923406538, + 2621276582, + 3333527019, + 2213211442, + 3632003358, + 550266542, + 311635791, + 394648212, + 2793546017, + 863246308, + 78056041, + 3874252757, + 322751935, + 4276002611, + 2287877551, + 141212667, + 2772268841, + 3013514520, + 2290127811, + 3410601781, + 4105952225, + 1705725581, + 333864297, + 87719767, + 2429871784, + 3378982703, + 4156516881, + 2177793666, + 2187546277, + 1526161973, + 3146431352, + 202399814, ]; + static final String firebaseIosApiKey = String.fromCharCodes( - List.generate(_envieddatafirebaseIosApiKey.length, (i) => i, growable: false) - .map((i) => _envieddatafirebaseIosApiKey[i] ^ _enviedkeyfirebaseIosApiKey[i]) - .toList(growable: false), - ); - static const List _enviedkeyappwriteCreateUserKey = [ - 3271707763, - 308433720, - 560061483, - 4127080807, - 2709287822, - 2849756213, - 4288764808, - 3389663825, - 4001298595, - 2446538789, - 4095981034, - 2295357155, - 4108824960, - 196859161, - 2237808182, - 2118835925, - 3511952480, - 3791717322, - 661354679, - 3966738612, - 1345167718, - 2187471285, - 3244348462, - 3637492468, - 2284228931, - 3530441806, - 4022870979, - 4209048067, - 2688771853, - 3412324427, - 3621237640, - 1687738613, - 4075994995, + List.generate( + _envieddatafirebaseIosApiKey.length, + (int i) => i, + growable: false, + ).map((int i) => + _envieddatafirebaseIosApiKey[i] ^ _enviedkeyfirebaseIosApiKey[i])); + + static const List _enviedkeyappwriteCreateUserKey = [ + 1025363171, + 3199901565, + 1117772124, + 2267096286, + 3458665533, + 3505928106, + 1560126977, + 1487931165, + 4131610370, + 2714925865, + 2090137170, + 2019772343, + 3101872387, + 1829696148, + 4197033618, + 4076555066, + 2360831126, + 481873414, + 3551757944, + 1003230722, + 346876229, + 950867569, + 651068530, + 3256805128, + 3018621299, + 3087914558, + 3184780552, + 1724971719, + 405645922, + 1665344215, + 2929460018, + 2325394729, + 4045366481, + 1344022204, + 2240973660, + 4042938449, + 788353664, + 2570176115, + 3496361631, + 3183086243, + 3493746911, + 603929716, + 2163637738, + 2049614588, + 1270764652, + 1624989428, + 2047853320, ]; - static const List _envieddataappwriteCreateUserKey = [ - 3271707701, - 308433753, - 560061529, - 4127080720, - 2709287871, - 2849756226, - 4288764922, - 3389663802, - 4001298670, - 2446538853, - 4095981011, - 2295357106, - 4108825002, - 196859213, - 2237808254, - 2118835949, - 3511952391, - 3791717260, - 661354702, - 3966738564, - 1345167663, - 2187471330, - 3244348506, - 3637492370, - 2284228977, - 3530441780, - 4022870938, - 4209048175, - 2688771934, - 3412324387, - 3621237712, - 1687738573, - 4075994948, + + static const List _envieddataappwriteCreateUserKey = [ + 1025363156, + 3199901451, + 1117772062, + 2267096238, + 3458665482, + 3505928093, + 1560127030, + 1487931248, + 4131610481, + 2714925854, + 2090137108, + 2019772377, + 3101872459, + 1829696252, + 4197033691, + 4076555090, + 2360831194, + 481873462, + 3551757901, + 1003230784, + 346876173, + 950867515, + 651068432, + 3256805201, + 3018621232, + 3087914505, + 3184780615, + 1724971664, + 405645915, + 1665344147, + 2929460063, + 2325394713, + 4045366417, + 1344022222, + 2240973610, + 4042938468, + 788353733, + 2570176021, + 3496361646, + 3183086292, + 3493746861, + 603929631, + 2163637671, + 2049614524, + 1270764629, + 1624989349, + 2047853346, ]; + static final String appwriteCreateUserKey = String.fromCharCodes( - List.generate(_envieddataappwriteCreateUserKey.length, (i) => i, growable: false) - .map((i) => _envieddataappwriteCreateUserKey[i] ^ _enviedkeyappwriteCreateUserKey[i]) - .toList(growable: false), - ); - static const List _enviedkeysentryDsn = [ - 3387281780, - 2534848782, - 853729727, - 1823172842, - 154591104, - 826679487, - 3360855262, - 3336960630, - 178613407, - 2902580785, - 708094396, - 2106589013, - 835719203, - 4252166697, - 503462322, - 3250200742, - 1740706821, - 4247036713, - 3813601400, + List.generate( + _envieddataappwriteCreateUserKey.length, + (int i) => i, + growable: false, + ).map((int i) => + _envieddataappwriteCreateUserKey[i] ^ + _enviedkeyappwriteCreateUserKey[i])); + + static const List _enviedkeysentryDsn = [ + 2093328549, + 2395802232, + 3689159578, + 1300107928, + 3457125591, + 753724482, + 722895077, + 656818493, + 3913599158, + 376812285, + 3932443728, + 4234049025, + 3958444475, + 1205587358, + 3607620029, + 2595966066, + 4286516767, + 2053601588, + 2206081614, + 4188996412, + 3739776189, + 2244230283, + 546021754, + 1799267250, + 1731643136, + 2679774739, + 888392791, + 3849848579, + 1752597810, + 848212169, + 15579502, + 3774201693, + 3191836906, + 2230678639, + 2290254202, + 3038727798, + 590668032, + 2866989375, + 3850014967, + 4009837300, + 3288420008, + 2340165528, + 3334502811, + 3810202287, + 652887321, + 2249348196, + 4145870016, + 3610794821, + 1954487445, + 2448115544, + 434025565, + 771180480, + 4037655367, + 1289923839, + 544702042, + 3855182399, + 3421035046, + 1883884298, + 1821176784, + 3849888774, + 4086241362, + 2005885374, + 4151536256, + 4196683549, + 1552656471, + 880436121, + 2374532573, + 1353233488, ]; - static const List _envieddatasentryDsn = [ - 3387281692, - 2534848890, - 853729739, - 1823172762, - 154591219, - 826679429, - 3360855281, - 3336960601, - 178613498, - 2902580809, - 708094429, - 2106588984, - 835719251, - 4252166725, - 503462359, - 3250200712, - 1740706918, - 4247036742, - 3813601301, + + static const List _envieddatasentryDsn = [ + 2093328589, + 2395802124, + 3689159662, + 1300108008, + 3457125540, + 753724536, + 722895050, + 656818450, + 3913599107, + 376812187, + 3932443750, + 4234049081, + 3958444431, + 1205587452, + 3607620059, + 2595965969, + 4286516862, + 2053601543, + 2206081581, + 4188996365, + 3739776132, + 2244230323, + 546021704, + 1799267286, + 1731643238, + 2679774752, + 888392803, + 3849848627, + 1752597760, + 848212209, + 15579483, + 3774201710, + 3191836809, + 2230678617, + 2290254152, + 3038727744, + 590668082, + 2866989322, + 3850014914, + 4009837251, + 3288420072, + 2340165611, + 3334502910, + 3810202305, + 652887405, + 2249348118, + 4145870009, + 3610794859, + 1954487540, + 2448115496, + 434025517, + 771180526, + 4037655334, + 1289923724, + 544701998, + 3855182430, + 3421035019, + 1883884392, + 1821176767, + 3849888869, + 4086241338, + 2005885387, + 4151536365, + 4196683571, + 1552656435, + 880436220, + 2374532594, + 1353233506, ]; - static final String sentryDsn = String.fromCharCodes( - List.generate(_envieddatasentryDsn.length, (i) => i, growable: false) - .map((i) => _envieddatasentryDsn[i] ^ _enviedkeysentryDsn[i]) - .toList(growable: false), - ); + + static final String sentryDsn = String.fromCharCodes(List.generate( + _envieddatasentryDsn.length, + (int i) => i, + growable: false, + ).map((int i) => _envieddatasentryDsn[i] ^ _enviedkeysentryDsn[i])); } diff --git a/lib/pages/wallet/ticket/ticket_datasource.dart b/lib/pages/wallet/ticket/ticket_datasource.dart new file mode 100644 index 00000000..505b58b2 --- /dev/null +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -0,0 +1,135 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +import 'package:campus_app/utils/constants.dart'; + +class TicketDataSource { + Future> getTicket() { + final Completer> completer = Completer>(); + + final Map ticket = { + 'barcode': '', + 'valid_from': '', + 'valid_till': '', + 'owner': '', + 'birthdate': '', + }; + + HeadlessInAppWebView? headlessWebView; + headlessWebView = HeadlessInAppWebView( + initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), + onWebViewCreated: (controller) { + controller.addJavaScriptHandler( + handlerName: 'barcode', + callback: (args) { + if (args.isNotEmpty && args[0] is String) { + final String image = List.from(args)[0].split(',')[1]; + + ticket['barcode'] = image; + } + }, + ); + + controller.addJavaScriptHandler( + handlerName: 'ticket_details', + callback: (args) { + if (args.isEmpty && args.length != 4) return; + + ticket['valid_from'] = args[0]; + ticket['valid_till'] = args[1]; + ticket['owner'] = args[2]; + ticket['birthdate'] = args[3]; + + completer.complete(ticket); + headlessWebView!.dispose(); + }, + ); + + controller.addJavaScriptHandler( + handlerName: 'error', + callback: (args) { + debugPrint('An error occurred. Error: $args'); + + completer.completeError(args); + headlessWebView!.dispose(); + }, + ); + + controller.addJavaScriptHandler( + handlerName: 'dispose', + callback: (args) { + headlessWebView!.dispose(); + }, + ); + }, + onLoadStop: (controller, url) async { + if (url.toString().startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && + url.toString().endsWith('s1')) { + await controller.evaluateJavascript( + source: """ + document.getElementById('username').value=""; + document.getElementById('password').value=""; + setTimeout(function(){ + document.getElementById('shibbutton').click(); + }, 500); + """, + ); + } else if (url.toString().startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && + url.toString().endsWith('s2')) { + await controller.evaluateJavascript( + source: """ + setTimeout(function(){ + document.getElementById('consentbutton_2').click(); + }, 500); + """, + ); + } else { + await controller.evaluateJavascript( + source: ''' + setTimeout(function(){ + if(!document.URL.startsWith("https://abo.ride-ticketing.de/app/subscription")) { + window.flutter_inappwebview.callHandler('error', "Ride ticketing not opened."); + return; + } + document.getElementsByClassName("abo-card-wrapper")[0].click(); + }, 1000); + ''', + ); + await controller.evaluateJavascript( + source: ''' + setTimeout(function(){ + if(!document.URL.startsWith("https://abo.ride-ticketing.de/app/ticket")) { + window.flutter_inappwebview.callHandler('test', "Could not open ticket page."); + return; + } + window.flutter_inappwebview.callHandler('barcode', document.getElementsByClassName("barcode")[0].src); + + const ticket_details = document.getElementsByClassName("value-column"); + const arr = []; + + for(const detail of ticket_details) { + arr.push(detail.innerText); + } + + window.flutter_inappwebview.callHandler('ticket_details', arr); + }, 1500); + ''', + ); + + // Fallback to ensure that the headless web view is always disposed, even if the ticket cannot be fetched. + await controller.evaluateJavascript( + source: ''' + setTimeout(function(){ + window.flutter_inappwebview.callHandler('dispose', ''); + }, 10000); + ''', + ); + } + }, + ); + + return completer.future; + } +} diff --git a/lib/pages/wallet/widgets/ticket_web_view_page.dart b/lib/pages/wallet/widgets/ticket_web_view_page.dart deleted file mode 100644 index 2a5c0bd6..00000000 --- a/lib/pages/wallet/widgets/ticket_web_view_page.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; - -import 'package:campus_app/core/themes.dart'; -import 'package:campus_app/main.dart'; -import 'package:campus_app/utils/widgets/campus_icon_button.dart'; -import 'package:visibility_detector/visibility_detector.dart'; - -/// This page shows an [InAppWebView] in order to display external -/// websites from the helpful ressources that are not yet natively implemented -class TicketWebViewPage extends StatefulWidget { - /// The url that should be opened - final String url; - - const TicketWebViewPage({ - Key? key, - required this.url, - }) : super(key: key); - - @override - State createState() => _TicketWebViewPageState(); -} - -class _TicketWebViewPageState extends State { - InAppWebViewController? webViewController; - late PullToRefreshController pullToRefreshController; - - InAppWebViewSettings settings = InAppWebViewSettings( - mediaPlaybackRequiresUserGesture: false, - verticalScrollBarEnabled: false, - horizontalScrollBarEnabled: false, - allowsInlineMediaPlayback: true, - useHybridComposition: false, - ); - - @override - void initState() { - super.initState(); - - pullToRefreshController = PullToRefreshController( - settings: PullToRefreshSettings(color: Colors.black), - onRefresh: () async { - if (Platform.isAndroid) { - await webViewController?.reload(); - } else if (Platform.isIOS) { - await webViewController?.loadUrl(urlRequest: URLRequest(url: await webViewController?.getUrl())); - } - }, - ); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: false, - onPopInvoked: (didPop) async { - if (didPop) return; - final navigator = Navigator.of(context); - if (await webViewController!.canGoBack()) { - await webViewController?.goBack(); - } else { - if (homeKey.currentState != null) { - homeKey.currentState!.setSwipeDisabled(); - } - navigator.pop(); - } - }, - child: VisibilityDetector( - onVisibilityChanged: (info) { - final bool isVisible = info.visibleFraction > 0; - - if (isVisible) { - if (homeKey.currentState != null) { - homeKey.currentState!.setSwipeDisabled(disableSwipe: true); - } - } - }, - key: const Key('visibility-key'), - child: Scaffold( - backgroundColor: Provider.of(context).currentThemeData.colorScheme.background, - body: SafeArea( - child: Stack( - children: [ - InAppWebView( - gestureRecognizers: >{} - ..add(const Factory(VerticalDragGestureRecognizer.new)), - pullToRefreshController: pullToRefreshController, - initialSettings: settings, - initialUrlRequest: URLRequest(url: WebUri(widget.url)), - onWebViewCreated: (controller) { - webViewController = controller; - }, - ), - // Back button - Padding( - padding: const EdgeInsets.only(top: 20, left: 20, right: 20), - child: CampusIconButton( - iconPath: 'assets/img/icons/arrow-left.svg', - onTap: () { - Navigator.maybePop(context); - }, - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/wallet/widgets/wallet.dart b/lib/pages/wallet/widgets/wallet.dart index d8ce5a05..a050b674 100644 --- a/lib/pages/wallet/widgets/wallet.dart +++ b/lib/pages/wallet/widgets/wallet.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -6,18 +7,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pdf_image_renderer/pdf_image_renderer.dart'; -import 'package:pdfx/pdfx.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import 'package:image/image.dart' as IMG; import 'package:campus_app/core/settings.dart'; import 'package:campus_app/core/themes.dart'; import 'package:campus_app/pages/wallet/ticket_fullscreen.dart'; import 'package:campus_app/pages/wallet/widgets/stacked_card_carousel.dart'; -import 'package:campus_app/pages/more/in_app_web_view_page.dart'; import 'package:campus_app/utils/widgets/custom_button.dart'; -import 'package:campus_app/utils/constants.dart'; class CampusWallet extends StatelessWidget { const CampusWallet({Key? key}) : super(key: key); @@ -50,7 +49,6 @@ class _BogestraTicketState extends State with AutomaticKeepAlive bool scanned = false; String scannedValue = ''; - late Image semesterTicketImage; late Image qrCodeImage; bool showQrCode = false; @@ -64,29 +62,29 @@ class _BogestraTicketState extends State with AutomaticKeepAlive final String directoryPath = saveDirectory.path; // Define the image files - final File ticketFile = File('$directoryPath/ticket.pdf'); + final File ticketFile = File('$directoryPath/ticket.png'); // If the images were parsed and saved in the past, they're loaded final bool tickedSaved = ticketFile.existsSync(); if (tickedSaved) { - final Image semesterTicketImage = await renderSemesterTicket(ticketFile.path); - final Image qrCodeImage = await renderQRCode(ticketFile.path); + final Image qrCodeImage = await renderQRCode(ticketFile); setState(() { scanned = true; - this.semesterTicketImage = semesterTicketImage; this.qrCodeImage = qrCodeImage; }); } } /// Saves a loaded semester ticket and its corresponding aztec-code - Future saveTicketPDF(File ticketPdf) async { + Future saveTicketPDF(String code) async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; // Save the given pdf file to the apps directory - await ticketPdf.copy('$directoryPath/ticket.pdf'); + final File file = File('$directoryPath/ticket.png'); + + await file.writeAsBytes(base64Decode(code)); } Future renderSemesterTicket(String path) async { @@ -115,26 +113,18 @@ class _BogestraTicketState extends State with AutomaticKeepAlive return Image(image: MemoryImage(bytes)); } - Future renderQRCode(String path) async { - final document = await PdfDocument.openFile(path); - final page = await document.getPage(1); - final pageImage = await page.render( - width: page.width * 2.4, - height: page.height * 2.4, - cropRect: Rect.fromLTWH(174, 250, page.width - 325, 269), - ); - await page.close(); + Future renderQRCode(File ticketFile) async { + Uint8List resizedData = ticketFile.readAsBytesSync(); + final IMG.Image img = IMG.decodeImage(resizedData)!; + final IMG.Image resized = IMG.copyResize(img, width: 250, height: 250); + resizedData = IMG.encodePng(resized); - if (pageImage == null) { - return Image(image: MemoryImage(Uint8List.fromList([0]))); - } - - return Image(image: MemoryImage(pageImage.bytes)); + return Image( + image: MemoryImage(resizedData), + ); } - Future addTicket() async { - Navigator.push(context, MaterialPageRoute(builder: (context) => InAppWebViewPage(url: rideTicketing))); - } + Future addTicket() async {} @override bool get wantKeepAlive => true; @@ -177,7 +167,7 @@ class _BogestraTicketState extends State with AutomaticKeepAlive } }, onLongPress: addTicket, - child: showQrCode ? qrCodeImage : semesterTicketImage, + child: qrCodeImage, ) : CustomButton( tapHandler: addTicket, diff --git a/lib/utils/pages/main_utils.dart b/lib/utils/pages/main_utils.dart index 01cffb7c..3d7b131b 100644 --- a/lib/utils/pages/main_utils.dart +++ b/lib/utils/pages/main_utils.dart @@ -430,7 +430,6 @@ class MainUtils { Future initializeFirebase(BuildContext context) async { // Initialize Firebase await Firebase.initializeApp( - name: 'campus_app', options: DefaultFirebaseOptions.currentPlatform, ); diff --git a/pubspec.lock b/pubspec.lock index bdea3620..c419ee93 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" animations: dependency: "direct main" description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: dependency: transitive description: @@ -301,10 +301,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "25b4624c231844a7a70a3817a729a6190a751ef1c07ded256e126a3b72261444" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.6" dartz: dependency: "direct main" description: @@ -389,10 +389,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -808,7 +808,7 @@ packages: source: hosted version: "4.0.2" image: - dependency: transitive + dependency: "direct main" description: name: image sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" @@ -855,6 +855,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -891,26 +915,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -995,10 +1019,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1529,10 +1553,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -1613,6 +1637,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" watcher: dependency: transitive description: @@ -1625,18 +1657,18 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" win32: dependency: transitive description: @@ -1686,5 +1718,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.3 <4.0.0" - flutter: ">=3.16.6" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 67e30142..1984076b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: sentry_flutter: ^7.14.0 sentry: ^7.14.0 video_thumbnail: ^0.5.3 + image: ^4.1.7 dev_dependencies: flutter_test: diff --git a/test/pages/calendar/calendar_datasource_test.mocks.dart b/test/pages/calendar/calendar_datasource_test.mocks.dart index acace95b..1f4fda77 100644 --- a/test/pages/calendar/calendar_datasource_test.mocks.dart +++ b/test/pages/calendar/calendar_datasource_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/calendar/calendar_datasource_test.dart. // Do not manually edit this file. @@ -14,11 +14,14 @@ import 'package:dio/src/response.dart' as _i6; import 'package:dio/src/transformer.dart' as _i4; import 'package:hive/hive.dart' as _i10; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -801,7 +804,10 @@ class MockBox extends _i1.Mock implements _i10.Box { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: '', + returnValue: _i11.dummyValue( + this, + Invocation.getter(#name), + ), ) as String); @override diff --git a/test/pages/calendar/calendar_repository_test.mocks.dart b/test/pages/calendar/calendar_repository_test.mocks.dart index ef178bf6..140ff6dd 100644 --- a/test/pages/calendar/calendar_repository_test.mocks.dart +++ b/test/pages/calendar/calendar_repository_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/calendar/calendar_repository_test.dart. // Do not manually edit this file. @@ -15,6 +15,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -106,6 +108,16 @@ class MockCalendarDatasource extends _i1.Mock returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future clearEventEntityCache() => (super.noSuchMethod( + Invocation.method( + #clearEventEntityCache, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override List<_i6.Event> readEventsFromCache({ bool? saved = false, diff --git a/test/pages/calendar/calendar_usecases_test.mocks.dart b/test/pages/calendar/calendar_usecases_test.mocks.dart index 422c352b..36aef855 100644 --- a/test/pages/calendar/calendar_usecases_test.mocks.dart +++ b/test/pages/calendar/calendar_usecases_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/calendar/calendar_usecases_test.dart. // Do not manually edit this file. @@ -16,6 +16,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/pages/mensa/mensa_datasource_test.mocks.dart b/test/pages/mensa/mensa_datasource_test.mocks.dart index dcdfb3df..96157f6b 100644 --- a/test/pages/mensa/mensa_datasource_test.mocks.dart +++ b/test/pages/mensa/mensa_datasource_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/mensa/mensa_datasource_test.dart. // Do not manually edit this file. @@ -14,11 +14,14 @@ import 'package:dio/src/response.dart' as _i6; import 'package:dio/src/transformer.dart' as _i4; import 'package:hive/hive.dart' as _i10; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -801,7 +804,10 @@ class MockBox extends _i1.Mock implements _i10.Box { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: '', + returnValue: _i11.dummyValue( + this, + Invocation.getter(#name), + ), ) as String); @override diff --git a/test/pages/mensa/mensa_repository_test.mocks.dart b/test/pages/mensa/mensa_repository_test.mocks.dart index b934fafe..aebb00e1 100644 --- a/test/pages/mensa/mensa_repository_test.mocks.dart +++ b/test/pages/mensa/mensa_repository_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/mensa/mensa_repository_test.dart. // Do not manually edit this file. @@ -15,6 +15,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/pages/mensa/mensa_usecases_test.mocks.dart b/test/pages/mensa/mensa_usecases_test.mocks.dart index 9cfa3816..2eaae348 100644 --- a/test/pages/mensa/mensa_usecases_test.mocks.dart +++ b/test/pages/mensa/mensa_usecases_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/mensa/mensa_usecases_test.dart. // Do not manually edit this file. @@ -16,6 +16,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/pages/news/news_repository_test.mocks.dart b/test/pages/news/news_repository_test.mocks.dart index 296d868f..ff0c5c8c 100644 --- a/test/pages/news/news_repository_test.mocks.dart +++ b/test/pages/news/news_repository_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/news/news_repository_test.dart. // Do not manually edit this file. @@ -16,6 +16,8 @@ import 'package:xml/xml.dart' as _i4; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -134,6 +136,16 @@ class MockNewsDatasource extends _i1.Mock implements _i5.NewsDatasource { returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); + @override + _i6.Future clearNewsEntityCache() => (super.noSuchMethod( + Invocation.method( + #clearNewsEntityCache, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override List<_i7.NewsEntity> readNewsEntitiesFromCach() => (super.noSuchMethod( Invocation.method( diff --git a/test/pages/news/news_usecases_test.mocks.dart b/test/pages/news/news_usecases_test.mocks.dart index 90925002..cf7fbbed 100644 --- a/test/pages/news/news_usecases_test.mocks.dart +++ b/test/pages/news/news_usecases_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/news/news_usecases_test.dart. // Do not manually edit this file. @@ -16,6 +16,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/pages/news/rubnews_datasource_test.mocks.dart b/test/pages/news/rubnews_datasource_test.mocks.dart index 7f2aab48..d852a4fd 100644 --- a/test/pages/news/rubnews_datasource_test.mocks.dart +++ b/test/pages/news/rubnews_datasource_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in campus_app/test/pages/news/rubnews_datasource_test.dart. // Do not manually edit this file. @@ -14,11 +14,14 @@ import 'package:dio/src/response.dart' as _i6; import 'package:dio/src/transformer.dart' as _i4; import 'package:hive/hive.dart' as _i10; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -801,7 +804,10 @@ class MockBox extends _i1.Mock implements _i10.Box { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: '', + returnValue: _i11.dummyValue( + this, + Invocation.getter(#name), + ), ) as String); @override From aa5ac4f7c32b5ce8fe7d5d063c233f08d3c4e667 Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Wed, 20 Mar 2024 08:53:54 +0100 Subject: [PATCH 05/16] Add ticket datasourc & repository --- lib/core/injection.dart | 16 ++++-- lib/pages/more/privacy_policy_page.dart | 2 + .../wallet/ticket/ticket_datasource.dart | 18 ++++++- .../wallet/ticket/ticket_repository.dart | 51 +++++++++++++++++++ test/pages/wallet/ticket_repository_test.dart | 17 +++++++ 5 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 lib/pages/wallet/ticket/ticket_repository.dart create mode 100644 test/pages/wallet/ticket_repository_test.dart diff --git a/lib/core/injection.dart b/lib/core/injection.dart index 5789b503..8fc7f21b 100644 --- a/lib/core/injection.dart +++ b/lib/core/injection.dart @@ -1,10 +1,8 @@ -import 'dart:io'; - import 'package:appwrite/appwrite.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; import 'package:campus_app/utils/pages/main_utils.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; -import 'package:dio/io.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; @@ -60,14 +58,22 @@ Future init() async { ); sl.registerLazySingleton(() { - final Client client = Client().setEndpoint(appwrite).setProject('campus_app'); - return BackendRepository(client: client); + return TicketDataSource( + secureStorage: const FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ), + ); }); //! //! Repositories //! + sl.registerLazySingleton(() { + final Client client = Client().setEndpoint(appwrite).setProject('campus_app'); + return BackendRepository(client: client); + }); + sl.registerSingletonWithDependencies( () => NewsRepository(newsDatasource: sl()), dependsOn: [NewsDatasource], diff --git a/lib/pages/more/privacy_policy_page.dart b/lib/pages/more/privacy_policy_page.dart index 6400be11..f2410f25 100644 --- a/lib/pages/more/privacy_policy_page.dart +++ b/lib/pages/more/privacy_policy_page.dart @@ -104,6 +104,8 @@ class PrivacyPolicyPage extends StatelessWidget { Bei der Nutzung des Raumfinders der Campus App, werden die aktuellen Standortdaten des Endgerätes verabeitet und bis zum Schließen der App zwischengespeichert.
Eine Einwilligung wird hierfür vom Betriebssystem eingeholt und kann jederzeit durch den Nutzer widerrufen werden.

+ Bei der Nutzung der Funktion zum Anzeigen des Semestertickets werden die RUB Logindaten (LoginID, Passwort) lokal auf dem Gerät gespeichert, dies ist zum Abrufen des Tickets unabdingbar. Eine Übermittlung dieser Daten über das Internet ist ausgeschlossen und jegliche Nutzung dieser erfolgt auf dem lokalen Endgeräte. +

Zweck der Datenverarbeitung


Die Verarbeitung der personenbezogenen Daten der Nutzer ist für den ordnungsgemäßen Betrieb der App erforderlich.
Dabei ist es unerlässlich, dass beispielsweise beim Abrufen der Speisepläne durch einen Server des AStAs personenbezogene Daten erhoben werden.
diff --git a/lib/pages/wallet/ticket/ticket_datasource.dart b/lib/pages/wallet/ticket/ticket_datasource.dart index 505b58b2..cbdacfad 100644 --- a/lib/pages/wallet/ticket/ticket_datasource.dart +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -2,11 +2,18 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:campus_app/utils/constants.dart'; class TicketDataSource { - Future> getTicket() { + final FlutterSecureStorage secureStorage; + + TicketDataSource({ + required this.secureStorage, + }); + + Future> getTicket() async { final Completer> completer = Completer>(); final Map ticket = { @@ -17,6 +24,13 @@ class TicketDataSource { 'birthdate': '', }; + final String? loginId = await secureStorage.read(key: 'loginId'); + final String? password = await secureStorage.read(key: 'password'); + + if (loginId == null || password == null) { + return ticket; + } + HeadlessInAppWebView? headlessWebView; headlessWebView = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), @@ -69,7 +83,7 @@ class TicketDataSource { url.toString().endsWith('s1')) { await controller.evaluateJavascript( source: """ - document.getElementById('username').value=""; + document.getElementById('username').value="${await secureStorage.read(key: 'loginID')}"; document.getElementById('password').value=""; setTimeout(function(){ document.getElementById('shibbutton').click(); diff --git a/lib/pages/wallet/ticket/ticket_repository.dart b/lib/pages/wallet/ticket/ticket_repository.dart new file mode 100644 index 00000000..a8dd461e --- /dev/null +++ b/lib/pages/wallet/ticket/ticket_repository.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:image/image.dart' as img; + +import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; + +class TicketRepository { + final TicketDataSource ticketDataSource; + + TicketRepository({ + required this.ticketDataSource, + }); + + Future loadTicket() async { + Map? ticket; + + try { + ticket = await ticketDataSource.getTicket(); + } catch (e) { + debugPrint(e.toString()); + return; + } + + if (ticket['barcode'] == null) { + return; + } + + final Directory saveDirectory = await getApplicationDocumentsDirectory(); + final String directoryPath = saveDirectory.path; + + // Save the given pdf file to the apps directory + final File file = File('$directoryPath/ticket.png'); + + await file.writeAsBytes(base64Decode(ticket['barcode'])); + } + + Future renderQRCode(File ticketFile) async { + Uint8List resizedData = ticketFile.readAsBytesSync(); + final img.Image image = img.decodeImage(resizedData)!; + final img.Image resized = img.copyResize(image, width: 250, height: 250); + resizedData = img.encodePng(resized); + + return Image( + image: MemoryImage(resizedData), + ); + } +} diff --git a/test/pages/wallet/ticket_repository_test.dart b/test/pages/wallet/ticket_repository_test.dart new file mode 100644 index 00000000..a009f0bb --- /dev/null +++ b/test/pages/wallet/ticket_repository_test.dart @@ -0,0 +1,17 @@ +import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +@GenerateMocks([TicketDataSource]) +void main() { + //late TicketDataSource mockTicketDatasource; + + setUp(() { + //mockTicketDatasource = MockTicketDatasource(); + }); + + group('[getRemoteNewsfeed]', () { + test('Should return news list on successfully web request', () async {}); + }); +} From 18b71a4ed43ab161c096217c6376553a658183ef Mon Sep 17 00:00:00 2001 From: henry-herrmann Date: Thu, 21 Mar 2024 12:13:46 +0100 Subject: [PATCH 06/16] Ticket usecases & UI implementation --- ios/Runner.xcodeproj/project.pbxproj | 6 +- lib/core/exceptions.dart | 4 + lib/core/injection.dart | 20 ++- .../wallet/ticket/ticket_datasource.dart | 75 +++++++- .../wallet/ticket/ticket_repository.dart | 67 ++++++-- lib/pages/wallet/ticket/ticket_usecases.dart | 36 ++++ lib/pages/wallet/ticket/ticket_web_view.dart | 141 +++++++++++++++ .../wallet/widgets/ticket_login_screen.dart | 161 ++++++++++++++++++ lib/pages/wallet/widgets/wallet.dart | 78 ++------- lib/utils/widgets/campus_textfield.dart | 22 ++- 10 files changed, 508 insertions(+), 102 deletions(-) create mode 100644 lib/pages/wallet/ticket/ticket_usecases.dart create mode 100644 lib/pages/wallet/ticket/ticket_web_view.dart create mode 100644 lib/pages/wallet/widgets/ticket_login_screen.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 930ae11d..96d4f080 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -348,7 +348,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -436,7 +436,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -485,7 +485,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/core/exceptions.dart b/lib/core/exceptions.dart index 6bcda6d5..784f2bf8 100644 --- a/lib/core/exceptions.dart +++ b/lib/core/exceptions.dart @@ -10,6 +10,10 @@ class EmptyResponseException implements Exception {} /// RUB login credentials incorrect class InvalidLoginIDAndPasswordException implements Exception {} +class MissingCredentialsException implements Exception {} + +class TicketNotFoundException implements Exception {} + /// 2FA token is not correct class Invalid2FATokenException implements Exception {} diff --git a/lib/core/injection.dart b/lib/core/injection.dart index 8fc7f21b..4744a0b0 100644 --- a/lib/core/injection.dart +++ b/lib/core/injection.dart @@ -1,5 +1,7 @@ import 'package:appwrite/appwrite.dart'; import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_usecases.dart'; import 'package:campus_app/utils/pages/main_utils.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; @@ -59,9 +61,7 @@ Future init() async { sl.registerLazySingleton(() { return TicketDataSource( - secureStorage: const FlutterSecureStorage( - aOptions: AndroidOptions(encryptedSharedPreferences: true), - ), + secureStorage: sl(), ); }); @@ -89,6 +89,10 @@ Future init() async { dependsOn: [MensaDataSource], ); + sl.registerLazySingleton( + () => TicketRepository(ticketDataSource: sl()), + ); + //! //! Usecases //! @@ -108,6 +112,10 @@ Future init() async { dependsOn: [MensaRepository], ); + sl.registerLazySingleton( + () => TicketUsecases(ticketRepository: sl()), + ); + //! //! Utils //! @@ -130,9 +138,13 @@ Future init() async { //! //sl.registerLazySingleton(http.Client.new); - sl.registerLazySingleton(FlutterSecureStorage.new); sl.registerLazySingleton(Dio.new); sl.registerLazySingleton(CookieJar.new); + sl.registerLazySingleton( + () => const FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ), + ); await sl.allReady(); } diff --git a/lib/pages/wallet/ticket/ticket_datasource.dart b/lib/pages/wallet/ticket/ticket_datasource.dart index cbdacfad..b17d9602 100644 --- a/lib/pages/wallet/ticket/ticket_datasource.dart +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -13,6 +13,53 @@ class TicketDataSource { required this.secureStorage, }); + Future logout() async { + HeadlessInAppWebView? headlessWebView; + headlessWebView = HeadlessInAppWebView( + initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), + initialSettings: InAppWebViewSettings(cacheEnabled: false, clearCache: true), + onWebViewCreated: (controller) { + controller.addJavaScriptHandler( + handlerName: 'error', + callback: (args) { + debugPrint('An error occurred. Error: $args'); + + headlessWebView!.dispose(); + }, + ); + + controller.addJavaScriptHandler( + handlerName: 'dispose', + callback: (args) { + headlessWebView!.dispose(); + }, + ); + }, + onLoadStop: (controller, url) async { + print(url.toString() == rideTicketing); + if (url.toString() == rideTicketing) { + await controller.evaluateJavascript( + source: ''' + setTimeout(function(){ + document.getElementsByClassName("abo-card-wrapper")[0].click(); + }, 500); + ''', + ); + } + // Fallback to ensure that the headless web view is always disposed, even if the ticket cannot be fetched. + await controller.evaluateJavascript( + source: ''' + setTimeout(function(){ + window.flutter_inappwebview.callHandler('dispose', ''); + }, 10000); + ''', + ); + }, + ); + + await headlessWebView.run(); + } + Future> getTicket() async { final Completer> completer = Completer>(); @@ -28,12 +75,13 @@ class TicketDataSource { final String? password = await secureStorage.read(key: 'password'); if (loginId == null || password == null) { - return ticket; + completer.completeError('No login credentials found.'); } HeadlessInAppWebView? headlessWebView; headlessWebView = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), + initialSettings: InAppWebViewSettings(cacheEnabled: false, clearCache: true), onWebViewCreated: (controller) { controller.addJavaScriptHandler( handlerName: 'barcode', @@ -49,12 +97,16 @@ class TicketDataSource { controller.addJavaScriptHandler( handlerName: 'ticket_details', callback: (args) { - if (args.isEmpty && args.length != 4) return; + if (args.isEmpty || List.of(args)[0] is! List || List.of(args[0]).length != 4) { + completer.completeError('Invalid ticket details'); + } - ticket['valid_from'] = args[0]; - ticket['valid_till'] = args[1]; - ticket['owner'] = args[2]; - ticket['birthdate'] = args[3]; + final List arguments = List.of(args)[0]; + + ticket['valid_from'] = arguments[0]; + ticket['valid_till'] = arguments[1]; + ticket['owner'] = arguments[2]; + ticket['birthdate'] = arguments[3]; completer.complete(ticket); headlessWebView!.dispose(); @@ -83,8 +135,8 @@ class TicketDataSource { url.toString().endsWith('s1')) { await controller.evaluateJavascript( source: """ - document.getElementById('username').value="${await secureStorage.read(key: 'loginID')}"; - document.getElementById('password').value=""; + document.getElementById('username').value="${loginId!}"; + document.getElementById('password').value="${password!}"; setTimeout(function(){ document.getElementById('shibbutton').click(); }, 500); @@ -94,6 +146,9 @@ class TicketDataSource { url.toString().endsWith('s2')) { await controller.evaluateJavascript( source: """ + if(document.getElementsByClassName("form-error").length == 1) { + window.flutter_inappwebview.callHandler('error', "Invalid credentials."); + } setTimeout(function(){ document.getElementById('consentbutton_2').click(); }, 500); @@ -115,7 +170,7 @@ class TicketDataSource { source: ''' setTimeout(function(){ if(!document.URL.startsWith("https://abo.ride-ticketing.de/app/ticket")) { - window.flutter_inappwebview.callHandler('test', "Could not open ticket page."); + window.flutter_inappwebview.callHandler('error', "Could not open ticket page."); return; } window.flutter_inappwebview.callHandler('barcode', document.getElementsByClassName("barcode")[0].src); @@ -144,6 +199,8 @@ class TicketDataSource { }, ); + await headlessWebView.run(); + return completer.future; } } diff --git a/lib/pages/wallet/ticket/ticket_repository.dart b/lib/pages/wallet/ticket/ticket_repository.dart index a8dd461e..a16d5d8d 100644 --- a/lib/pages/wallet/ticket/ticket_repository.dart +++ b/lib/pages/wallet/ticket/ticket_repository.dart @@ -1,10 +1,8 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; -import 'package:flutter/material.dart'; +import 'package:campus_app/core/exceptions.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:image/image.dart' as img; import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; @@ -21,31 +19,64 @@ class TicketRepository { try { ticket = await ticketDataSource.getTicket(); } catch (e) { - debugPrint(e.toString()); - return; + if (e == 'No login credentials found.') { + throw MissingCredentialsException(); + } else if (e == 'Invalid credentials.') { + throw InvalidLoginIDAndPasswordException(); + } else if (e == 'Could not open ticket page.') { + await deleteTicketQRCode(); + throw TicketNotFoundException(); + } } - - if (ticket['barcode'] == null) { - return; + if (ticket == null) { + throw TicketNotFoundException(); } + await saveTicketQRCode(ticket['barcode']); + } + + Future saveTicketQRCode(String code) async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; - // Save the given pdf file to the apps directory + // Save the given png file to the app directory final File file = File('$directoryPath/ticket.png'); - await file.writeAsBytes(base64Decode(ticket['barcode'])); + await file.writeAsBytes(base64Decode(code)); + } + + Future deleteTicketQRCode() async { + final Directory saveDirectory = await getApplicationDocumentsDirectory(); + final String directoryPath = saveDirectory.path; + + // Define the image file + final File ticketFile = File('$directoryPath/ticket.png'); + + if (await ticketFileExists()) { + ticketFile.deleteSync(); + } + } + + Future getTicketFile() async { + final Directory saveDirectory = await getApplicationDocumentsDirectory(); + final String directoryPath = saveDirectory.path; + + // Define the image files + final File ticketFile = File('$directoryPath/ticket.png'); + + return ticketFile; } - Future renderQRCode(File ticketFile) async { - Uint8List resizedData = ticketFile.readAsBytesSync(); - final img.Image image = img.decodeImage(resizedData)!; - final img.Image resized = img.copyResize(image, width: 250, height: 250); - resizedData = img.encodePng(resized); + Future ticketFileExists() async { + final Directory saveDirectory = await getApplicationDocumentsDirectory(); + final String directoryPath = saveDirectory.path; + + // Define the image file + final File ticketFile = File('$directoryPath/ticket.png'); + + // If the images were parsed and saved in the past, they're loaded + final bool ticketSaved = ticketFile.existsSync(); - return Image( - image: MemoryImage(resizedData), - ); + return ticketSaved; } } diff --git a/lib/pages/wallet/ticket/ticket_usecases.dart b/lib/pages/wallet/ticket/ticket_usecases.dart new file mode 100644 index 00000000..dc70eb9f --- /dev/null +++ b/lib/pages/wallet/ticket/ticket_usecases.dart @@ -0,0 +1,36 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:image/image.dart' as img; + +import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; + +class TicketUsecases { + final TicketRepository ticketRepository; + + TicketUsecases({ + required this.ticketRepository, + }); + + Future renderQRCode() async { + try { + await ticketRepository.loadTicket(); + } catch (e) { + debugPrint(e.toString()); + return null; + } + if (await ticketRepository.ticketFileExists() == false) return null; + + final File ticketFile = await ticketRepository.getTicketFile(); + + Uint8List resizedData = ticketFile.readAsBytesSync(); + final img.Image image = img.decodeImage(resizedData)!; + final img.Image resized = img.copyResize(image, width: 250, height: 250); + resizedData = img.encodePng(resized); + + return Image( + image: MemoryImage(resizedData), + ); + } +} diff --git a/lib/pages/wallet/ticket/ticket_web_view.dart b/lib/pages/wallet/ticket/ticket_web_view.dart new file mode 100644 index 00000000..fe9cf813 --- /dev/null +++ b/lib/pages/wallet/ticket/ticket_web_view.dart @@ -0,0 +1,141 @@ +import 'dart:io'; +import 'package:campus_app/utils/constants.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +import 'package:campus_app/core/themes.dart'; +import 'package:campus_app/main.dart'; +import 'package:campus_app/utils/widgets/campus_icon_button.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +/// This page shows an [InAppWebView] in order to display external +/// websites from the helpful ressources that are not yet natively implemented +class TicketWebViewPage extends StatefulWidget { + /// The url that should be opened + + const TicketWebViewPage({ + Key? key, + }) : super(key: key); + + @override + State createState() => _TicketWebViewPageState(); +} + +class _TicketWebViewPageState extends State { + InAppWebViewController? webViewController; + late PullToRefreshController pullToRefreshController; + + InAppWebViewSettings settings = InAppWebViewSettings( + mediaPlaybackRequiresUserGesture: false, + verticalScrollBarEnabled: false, + horizontalScrollBarEnabled: false, + allowsInlineMediaPlayback: true, + useHybridComposition: false, + ); + + @override + void initState() { + super.initState(); + + pullToRefreshController = PullToRefreshController( + settings: PullToRefreshSettings(color: Colors.black), + onRefresh: () async { + if (Platform.isAndroid) { + await webViewController?.reload(); + } else if (Platform.isIOS) { + await webViewController?.loadUrl(urlRequest: URLRequest(url: await webViewController?.getUrl())); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvoked: (didPop) async { + if (didPop) return; + final navigator = Navigator.of(context); + if (await webViewController!.canGoBack()) { + await webViewController?.goBack(); + } else { + if (homeKey.currentState != null) { + homeKey.currentState!.setSwipeDisabled(); + } + navigator.pop(); + } + }, + child: VisibilityDetector( + onVisibilityChanged: (info) { + final bool isVisible = info.visibleFraction > 0; + + if (isVisible) { + if (homeKey.currentState != null) { + homeKey.currentState!.setSwipeDisabled(disableSwipe: true); + } + } + }, + key: const Key('visibility-key'), + child: Scaffold( + backgroundColor: Provider.of(context).currentThemeData.colorScheme.background, + body: SafeArea( + child: Stack( + children: [ + InAppWebView( + gestureRecognizers: >{} + ..add(const Factory(VerticalDragGestureRecognizer.new)), + pullToRefreshController: pullToRefreshController, + initialSettings: settings, + initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), + onWebViewCreated: (controller) { + controller.addJavaScriptHandler( + handlerName: 'error', + callback: (args) { + debugPrint('An error occurred. Error: $args'); + + //headlessWebView!.dispose(); + }, + ); + + controller.addJavaScriptHandler( + handlerName: 'dispose', + callback: (args) { + //headlessWebView!.dispose(); + print(args); + }, + ); + }, + onLoadStop: (controller, url) async { + if (url.toString() == rideTicketing) { + await controller.evaluateJavascript( + source: ''' + setTimeout(function(){ + window.flutter_inappwebview.callHandler('dispose', Object.getOwnPropertyNames(document.getElementsByTagName("lib-icon-button")[0])); + document.getElementsByTagName("lib-profile-icon-button")[0].__zone_symbol__loginIconClickfalse(); + } , 500); + ''', + ); + } + }, + ), + // Back button + Padding( + padding: const EdgeInsets.only(top: 20, left: 20, right: 20), + child: CampusIconButton( + iconPath: 'assets/img/icons/arrow-left.svg', + onTap: () { + Navigator.maybePop(context); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet/widgets/ticket_login_screen.dart b/lib/pages/wallet/widgets/ticket_login_screen.dart new file mode 100644 index 00000000..612f2f02 --- /dev/null +++ b/lib/pages/wallet/widgets/ticket_login_screen.dart @@ -0,0 +1,161 @@ +import 'package:campus_app/core/exceptions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:provider/provider.dart'; + +import 'package:campus_app/core/injection.dart'; +import 'package:campus_app/core/themes.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; +import 'package:campus_app/utils/widgets/campus_icon_button.dart'; +import 'package:campus_app/utils/widgets/campus_textfield.dart'; +import 'package:campus_app/utils/widgets/campus_button.dart'; + +class TicketLoginScreen extends StatefulWidget { + const TicketLoginScreen({super.key}); + + @override + State createState() => _TicketLoginScreenState(); +} + +class _TicketLoginScreenState extends State { + final TicketRepository ticketRepository = sl(); + final FlutterSecureStorage secureStorage = sl(); + + final TextEditingController usernameController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final TextEditingController submitButtonController = TextEditingController(); + + bool showErrorMessage = false; + String errorMessage = ''; + + bool loading = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Provider.of(context).currentThemeData.colorScheme.background, + body: Padding( + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Back button + Padding( + padding: const EdgeInsets.only(bottom: 12, left: 20, right: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CampusIconButton( + iconPath: 'assets/img/icons/arrow-left.svg', + onTap: () { + Navigator.pop(context); + }, + ), + ], + ), + ), + const Padding(padding: EdgeInsets.only(top: 10)), + Center( + child: Column( + children: [ + Image.asset( + 'assets/img/icons/rub-link.png', + color: Provider.of(context).currentTheme == AppThemes.light + ? Colors.black + : Colors.white, + width: 80, + filterQuality: FilterQuality.high, + ), + const Padding(padding: EdgeInsets.only(top: 25)), + CampusTextField( + textFieldController: usernameController, + textFieldText: 'RUB LoginID', + ), + const Padding(padding: EdgeInsets.only(top: 10)), + CampusTextField( + textFieldController: passwordController, + obscuredInput: true, + textFieldText: 'RUB Passwort', + ), + const Padding(padding: EdgeInsets.only(top: 15)), + CampusButton( + text: 'Login', + onTap: () async { + final NavigatorState navigator = Navigator.of(context); + + if (usernameController.text.isEmpty || passwordController.text.isEmpty) { + setState(() { + errorMessage = 'Bitte fülle beide Felder aus!'; + showErrorMessage = true; + }); + return; + } + + setState(() { + showErrorMessage = false; + loading = true; + }); + + await secureStorage.write(key: 'loginId', value: usernameController.text); + await secureStorage.write(key: 'password', value: passwordController.text); + + try { + await ticketRepository.loadTicket(); + navigator.pop(); + } catch (e) { + if (e is InvalidLoginIDAndPasswordException) { + setState(() { + errorMessage = 'Falsche LoginID und/oder Passwort!'; + showErrorMessage = true; + }); + } + } + setState(() { + loading = false; + }); + }, + ), + const Padding(padding: EdgeInsets.only(top: 25)), + if (showErrorMessage) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/img/icons/info.svg', + colorFilter: const ColorFilter.mode( + Colors.redAccent, + BlendMode.srcIn, + ), + width: 18, + ), + const Padding( + padding: EdgeInsets.only(left: 3), + ), + Text( + errorMessage, + style: Provider.of(context).currentThemeData.textTheme.labelMedium!.copyWith( + color: Provider.of(context).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1)), + ), + ], + ), + ], + if (loading) ...[ + CircularProgressIndicator( + backgroundColor: Provider.of(context).currentThemeData.cardColor, + color: Provider.of(context).currentThemeData.primaryColor, + strokeWidth: 3, + ), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/wallet/widgets/wallet.dart b/lib/pages/wallet/widgets/wallet.dart index a050b674..c2d6c1fa 100644 --- a/lib/pages/wallet/widgets/wallet.dart +++ b/lib/pages/wallet/widgets/wallet.dart @@ -1,16 +1,15 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; +import 'package:campus_app/core/injection.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_usecases.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_web_view.dart'; +import 'package:campus_app/pages/wallet/widgets/ticket_login_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pdf_image_renderer/pdf_image_renderer.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:screen_brightness/screen_brightness.dart'; -import 'package:image/image.dart' as IMG; import 'package:campus_app/core/settings.dart'; import 'package:campus_app/core/themes.dart'; @@ -53,22 +52,16 @@ class _BogestraTicketState extends State with AutomaticKeepAlive bool showQrCode = false; + TicketUsecases ticketUsecases = sl(); + /// Loads the previously saved image of the semester ticket and /// the corresponding aztec-code Future loadTicket() async { debugPrint('Loading semester ticket'); - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; - - // Define the image files - final File ticketFile = File('$directoryPath/ticket.png'); - - // If the images were parsed and saved in the past, they're loaded - final bool tickedSaved = ticketFile.existsSync(); - if (tickedSaved) { - final Image qrCodeImage = await renderQRCode(ticketFile); + final Image? qrCodeImage = await ticketUsecases.renderQRCode(); + if (qrCodeImage != null) { setState(() { scanned = true; this.qrCodeImage = qrCodeImage; @@ -76,56 +69,15 @@ class _BogestraTicketState extends State with AutomaticKeepAlive } } - /// Saves a loaded semester ticket and its corresponding aztec-code - Future saveTicketPDF(String code) async { - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; - - // Save the given pdf file to the apps directory - final File file = File('$directoryPath/ticket.png'); - - await file.writeAsBytes(base64Decode(code)); - } - - Future renderSemesterTicket(String path) async { - final pdf = PdfImageRendererPdf(path: path); - - await pdf.open(); - await pdf.openPage(pageIndex: 0); - - final size = await pdf.getPageSize(pageIndex: 0); - - final bytes = await pdf.renderPage( - x: 71, - y: 66, - width: size.width - 353, - height: size.height - 690, - scale: 4, - ); - - await pdf.closePage(pageIndex: 0); - await pdf.close(); - - if (bytes == null) { - return Image(image: MemoryImage(Uint8List.fromList([0]))); - } - - return Image(image: MemoryImage(bytes)); - } - - Future renderQRCode(File ticketFile) async { - Uint8List resizedData = ticketFile.readAsBytesSync(); - final IMG.Image img = IMG.decodeImage(resizedData)!; - final IMG.Image resized = IMG.copyResize(img, width: 250, height: 250); - resizedData = IMG.encodePng(resized); - - return Image( - image: MemoryImage(resizedData), + Future addTicket() async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const TicketWebViewPage(), + ), ); } - Future addTicket() async {} - @override bool get wantKeepAlive => true; diff --git a/lib/utils/widgets/campus_textfield.dart b/lib/utils/widgets/campus_textfield.dart index 4c0ec6d1..90c7f005 100644 --- a/lib/utils/widgets/campus_textfield.dart +++ b/lib/utils/widgets/campus_textfield.dart @@ -76,22 +76,34 @@ class CampusTextFieldState extends State { focusNode: _focusNode, controller: widget.textFieldController, style: Provider.of(context).currentThemeData.textTheme.labelMedium!.copyWith( - color: const Color.fromARGB(255, 129, 129, 129), + color: Provider.of(context).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1), ), - cursorColor: Colors.black, + cursorColor: Provider.of(context).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1), decoration: InputDecoration( filled: true, - fillColor: const Color.fromRGBO(245, 246, 250, 1), + fillColor: Provider.of(context, listen: false).currentTheme == AppThemes.light + ? const Color.fromRGBO(245, 246, 250, 1) + : const Color.fromRGBO(34, 40, 54, 1), hintText: hint, hintStyle: Provider.of(context).currentThemeData.textTheme.labelMedium!.copyWith( - color: const Color.fromARGB(255, 146, 146, 146), + color: Provider.of(context, listen: false).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1), ), contentPadding: widget.type == CampusTextFieldType.normal ? const EdgeInsets.symmetric(horizontal: 12, vertical: 24) : const EdgeInsets.only(left: 65, right: 12, top: 24, bottom: 24), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), - borderSide: const BorderSide(width: 2), + borderSide: BorderSide( + color: Provider.of(context, listen: false).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromARGB(255, 129, 129, 129), + ), ), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), ), From 3129d86ea5b0e1ee805d90ac2f7835593876f7ea Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Mon, 25 Mar 2024 23:00:14 +0100 Subject: [PATCH 07/16] Ticket datasource changes & wallet UI changes --- assets/img/bogestra-logo.svg | 24 +- lib/pages/more/privacy_policy_page.dart | 2 +- .../wallet/ticket/ticket_datasource.dart | 240 +++++++----------- .../wallet/ticket/ticket_repository.dart | 67 +++-- lib/pages/wallet/ticket/ticket_usecases.dart | 33 ++- lib/pages/wallet/ticket/ticket_web_view.dart | 141 ---------- .../wallet/widgets/ticket_login_screen.dart | 51 +++- lib/pages/wallet/widgets/wallet.dart | 124 ++++++++- lib/utils/constants.dart | 3 +- 9 files changed, 340 insertions(+), 345 deletions(-) delete mode 100644 lib/pages/wallet/ticket/ticket_web_view.dart diff --git a/assets/img/bogestra-logo.svg b/assets/img/bogestra-logo.svg index 0dadcf93..153d615a 100644 --- a/assets/img/bogestra-logo.svg +++ b/assets/img/bogestra-logo.svg @@ -2,45 +2,41 @@ - - - - - - - - - - + - diff --git a/lib/pages/more/privacy_policy_page.dart b/lib/pages/more/privacy_policy_page.dart index f2410f25..42e8a174 100644 --- a/lib/pages/more/privacy_policy_page.dart +++ b/lib/pages/more/privacy_policy_page.dart @@ -104,7 +104,7 @@ class PrivacyPolicyPage extends StatelessWidget { Bei der Nutzung des Raumfinders der Campus App, werden die aktuellen Standortdaten des Endgerätes verabeitet und bis zum Schließen der App zwischengespeichert.
Eine Einwilligung wird hierfür vom Betriebssystem eingeholt und kann jederzeit durch den Nutzer widerrufen werden.

- Bei der Nutzung der Funktion zum Anzeigen des Semestertickets werden die RUB Logindaten (LoginID, Passwort) lokal auf dem Gerät gespeichert, dies ist zum Abrufen des Tickets unabdingbar. Eine Übermittlung dieser Daten über das Internet ist ausgeschlossen und jegliche Nutzung dieser erfolgt auf dem lokalen Endgeräte. + Bei der Nutzung der Funktion zum Anzeigen des Semestertickets werden die RUB Logindaten (LoginID, Passwort) lokal auf dem Gerät gespeichert, dies ist zum Abrufen des Tickets unabdingbar. Eine Übermittlung der Daten erfolgt nur an den offizielen SSO Provider der RUB.

Zweck der Datenverarbeitung


Die Verarbeitung der personenbezogenen Daten der Nutzer ist für den ordnungsgemäßen Betrieb der App erforderlich.
diff --git a/lib/pages/wallet/ticket/ticket_datasource.dart b/lib/pages/wallet/ticket/ticket_datasource.dart index b17d9602..5062ed2a 100644 --- a/lib/pages/wallet/ticket/ticket_datasource.dart +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -13,60 +13,16 @@ class TicketDataSource { required this.secureStorage, }); - Future logout() async { - HeadlessInAppWebView? headlessWebView; - headlessWebView = HeadlessInAppWebView( - initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), - initialSettings: InAppWebViewSettings(cacheEnabled: false, clearCache: true), - onWebViewCreated: (controller) { - controller.addJavaScriptHandler( - handlerName: 'error', - callback: (args) { - debugPrint('An error occurred. Error: $args'); - - headlessWebView!.dispose(); - }, - ); - - controller.addJavaScriptHandler( - handlerName: 'dispose', - callback: (args) { - headlessWebView!.dispose(); - }, - ); - }, - onLoadStop: (controller, url) async { - print(url.toString() == rideTicketing); - if (url.toString() == rideTicketing) { - await controller.evaluateJavascript( - source: ''' - setTimeout(function(){ - document.getElementsByClassName("abo-card-wrapper")[0].click(); - }, 500); - ''', - ); - } - // Fallback to ensure that the headless web view is always disposed, even if the ticket cannot be fetched. - await controller.evaluateJavascript( - source: ''' - setTimeout(function(){ - window.flutter_inappwebview.callHandler('dispose', ''); - }, 10000); - ''', - ); - }, - ); - - await headlessWebView.run(); - } + Future> getRemoteTicket() async { + debugPrint('Loading semester ticket'); - Future> getTicket() async { final Completer> completer = Completer>(); final Map ticket = { 'barcode': '', 'valid_from': '', 'valid_till': '', + 'validity_region': '', 'owner': '', 'birthdate': '', }; @@ -74,78 +30,80 @@ class TicketDataSource { final String? loginId = await secureStorage.read(key: 'loginId'); final String? password = await secureStorage.read(key: 'password'); - if (loginId == null || password == null) { - completer.completeError('No login credentials found.'); - } + if (loginId != null && password != null) { + HeadlessInAppWebView? headlessWebView; + headlessWebView = HeadlessInAppWebView( + initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), + initialSettings: InAppWebViewSettings(cacheEnabled: false, clearCache: true), + onWebViewCreated: (controller) { + controller.addJavaScriptHandler( + handlerName: 'barcode', + callback: (args) { + if (args.isNotEmpty && args[0] is String) { + final String image = List.from(args)[0].split(',')[1]; + + ticket['barcode'] = image; + } + }, + ); - HeadlessInAppWebView? headlessWebView; - headlessWebView = HeadlessInAppWebView( - initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), - initialSettings: InAppWebViewSettings(cacheEnabled: false, clearCache: true), - onWebViewCreated: (controller) { - controller.addJavaScriptHandler( - handlerName: 'barcode', - callback: (args) { - if (args.isNotEmpty && args[0] is String) { - final String image = List.from(args)[0].split(',')[1]; - - ticket['barcode'] = image; - } - }, - ); - - controller.addJavaScriptHandler( - handlerName: 'ticket_details', - callback: (args) { - if (args.isEmpty || List.of(args)[0] is! List || List.of(args[0]).length != 4) { - completer.completeError('Invalid ticket details'); - } - - final List arguments = List.of(args)[0]; - - ticket['valid_from'] = arguments[0]; - ticket['valid_till'] = arguments[1]; - ticket['owner'] = arguments[2]; - ticket['birthdate'] = arguments[3]; - - completer.complete(ticket); - headlessWebView!.dispose(); - }, - ); - - controller.addJavaScriptHandler( - handlerName: 'error', - callback: (args) { - debugPrint('An error occurred. Error: $args'); - - completer.completeError(args); - headlessWebView!.dispose(); - }, - ); - - controller.addJavaScriptHandler( - handlerName: 'dispose', - callback: (args) { - headlessWebView!.dispose(); - }, - ); - }, - onLoadStop: (controller, url) async { - if (url.toString().startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && - url.toString().endsWith('s1')) { - await controller.evaluateJavascript( - source: """ - document.getElementById('username').value="${loginId!}"; - document.getElementById('password').value="${password!}"; - setTimeout(function(){ - document.getElementById('shibbutton').click(); - }, 500); - """, + controller.addJavaScriptHandler( + handlerName: 'ticket_details', + callback: (args) { + if (args.isEmpty || List.of(args)[0] is! List || List.of(args[0]).length != 4) { + completer.completeError('Invalid ticket details'); + } + + final List arguments = List.of(args)[0]; + + if (arguments.length == 4) { + ticket['valid_from'] = arguments[0]; + ticket['valid_till'] = arguments[1]; + ticket['owner'] = arguments[2]; + ticket['birthdate'] = arguments[3]; + } else { + ticket['valid_from'] = arguments[0]; + ticket['valid_till'] = arguments[1]; + ticket['validity_region'] = arguments[2]; + ticket['owner'] = arguments[3]; + ticket['birthdate'] = arguments[4]; + } + + debugPrint('Loaded semesterticket.'); + + completer.complete(ticket); + headlessWebView!.dispose(); + }, + ); + + controller.addJavaScriptHandler( + handlerName: 'error', + callback: (args) { + debugPrint('An error occurred. Error: $args'); + + completer.completeError(args[0]); + headlessWebView!.dispose(); + }, ); - } else if (url.toString().startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && - url.toString().endsWith('s2')) { - await controller.evaluateJavascript( - source: """ + }, + onLoadStop: (controller, url) async { + if (url.toString().startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && + url.toString().endsWith('s1')) { + Timer(const Duration(milliseconds: 300), () async { + await controller.evaluateJavascript( + source: """ + document.getElementById('username').value="$loginId"; + document.getElementById('password').value="$password"; + setTimeout(function(){ + document.getElementById('shibbutton').click(); + }, 500); + """, + ); + }); + } else if (url.toString().startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && + url.toString().endsWith('s2')) { + await controller.evaluateJavascript( + source: """ if(document.getElementsByClassName("form-error").length == 1) { window.flutter_inappwebview.callHandler('error', "Invalid credentials."); } @@ -153,26 +111,24 @@ class TicketDataSource { document.getElementById('consentbutton_2').click(); }, 500); """, - ); - } else { - await controller.evaluateJavascript( - source: ''' + ); + } else if (url.toString().startsWith('https://abo.ride-ticketing.de')) { + await controller.evaluateJavascript( + source: ''' setTimeout(function(){ - if(!document.URL.startsWith("https://abo.ride-ticketing.de/app/subscription")) { - window.flutter_inappwebview.callHandler('error', "Ride ticketing not opened."); - return; - } document.getElementsByClassName("abo-card-wrapper")[0].click(); }, 1000); ''', - ); - await controller.evaluateJavascript( - source: ''' + ); + await controller.evaluateJavascript( + source: ''' setTimeout(function(){ if(!document.URL.startsWith("https://abo.ride-ticketing.de/app/ticket")) { window.flutter_inappwebview.callHandler('error', "Could not open ticket page."); return; } + window.flutter_inappwebview.callHandler('stateChange', "ticketPage"); + window.flutter_inappwebview.callHandler('barcode', document.getElementsByClassName("barcode")[0].src); const ticket_details = document.getElementsByClassName("value-column"); @@ -183,23 +139,23 @@ class TicketDataSource { } window.flutter_inappwebview.callHandler('ticket_details', arr); - }, 1500); + }, 2500); ''', - ); + ); + } + }, + ); - // Fallback to ensure that the headless web view is always disposed, even if the ticket cannot be fetched. - await controller.evaluateJavascript( - source: ''' - setTimeout(function(){ - window.flutter_inappwebview.callHandler('dispose', ''); - }, 10000); - ''', - ); - } - }, - ); + await headlessWebView.run(); - await headlessWebView.run(); + Timer(const Duration(seconds: 15), () async { + if (headlessWebView != null) { + await headlessWebView.dispose(); + } + }); + } else { + completer.completeError('No login credentials found.'); + } return completer.future; } diff --git a/lib/pages/wallet/ticket/ticket_repository.dart b/lib/pages/wallet/ticket/ticket_repository.dart index a16d5d8d..fd5a49e0 100644 --- a/lib/pages/wallet/ticket/ticket_repository.dart +++ b/lib/pages/wallet/ticket/ticket_repository.dart @@ -1,9 +1,9 @@ import 'dart:convert'; import 'dart:io'; -import 'package:campus_app/core/exceptions.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:campus_app/core/exceptions.dart'; import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; class TicketRepository { @@ -17,66 +17,93 @@ class TicketRepository { Map? ticket; try { - ticket = await ticketDataSource.getTicket(); + ticket = await ticketDataSource.getRemoteTicket(); } catch (e) { if (e == 'No login credentials found.') { throw MissingCredentialsException(); } else if (e == 'Invalid credentials.') { throw InvalidLoginIDAndPasswordException(); } else if (e == 'Could not open ticket page.') { - await deleteTicketQRCode(); + await deleteTicket(); throw TicketNotFoundException(); } } - if (ticket == null) { + if (ticket == null || ticket['barcode'].toString().isEmpty) { throw TicketNotFoundException(); } - await saveTicketQRCode(ticket['barcode']); + await saveTicket(ticket); } - Future saveTicketQRCode(String code) async { + Future saveTicket(Map ticket) async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; // Save the given png file to the app directory - final File file = File('$directoryPath/ticket.png'); + final File qrCodeFile = File('$directoryPath/ticket.png'); + final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - await file.writeAsBytes(base64Decode(code)); + await qrCodeFile.writeAsBytes(base64Decode(ticket['barcode'])); + await ticketDetailsFile.writeAsString(jsonEncode(ticket)); } - Future deleteTicketQRCode() async { + Future deleteTicket() async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; // Define the image file - final File ticketFile = File('$directoryPath/ticket.png'); + final File qrCodeFile = File('$directoryPath/ticket.png'); + final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - if (await ticketFileExists()) { - ticketFile.deleteSync(); + if (await qrCodeFileExists()) { + await qrCodeFile.delete(); } + + if (await ticketDetailsFileExists()) { + await ticketDetailsFile.delete(); + } + } + + Future getQRCodeFile() async { + final Directory saveDirectory = await getApplicationDocumentsDirectory(); + final String directoryPath = saveDirectory.path; + + // Define the image file + final File qrCodeFile = File('$directoryPath/ticket.png'); + + return qrCodeFile; } - Future getTicketFile() async { + Future getTicketDetailsFile() async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; - // Define the image files - final File ticketFile = File('$directoryPath/ticket.png'); + final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - return ticketFile; + return ticketDetailsFile; } - Future ticketFileExists() async { + Future qrCodeFileExists() async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; // Define the image file - final File ticketFile = File('$directoryPath/ticket.png'); + final File qrCodeFile = File('$directoryPath/ticket.png'); // If the images were parsed and saved in the past, they're loaded - final bool ticketSaved = ticketFile.existsSync(); + final bool exists = qrCodeFile.existsSync(); + + return exists; + } + + Future ticketDetailsFileExists() async { + final Directory saveDirectory = await getApplicationDocumentsDirectory(); + final String directoryPath = saveDirectory.path; + + final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); + + final bool exists = ticketDetailsFile.existsSync(); - return ticketSaved; + return exists; } } diff --git a/lib/pages/wallet/ticket/ticket_usecases.dart b/lib/pages/wallet/ticket/ticket_usecases.dart index dc70eb9f..97641d19 100644 --- a/lib/pages/wallet/ticket/ticket_usecases.dart +++ b/lib/pages/wallet/ticket/ticket_usecases.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -14,23 +15,37 @@ class TicketUsecases { }); Future renderQRCode() async { - try { - await ticketRepository.loadTicket(); - } catch (e) { - debugPrint(e.toString()); - return null; - } - if (await ticketRepository.ticketFileExists() == false) return null; + if (await ticketRepository.qrCodeFileExists() == false) return null; - final File ticketFile = await ticketRepository.getTicketFile(); + final File ticketFile = await ticketRepository.getQRCodeFile(); Uint8List resizedData = ticketFile.readAsBytesSync(); final img.Image image = img.decodeImage(resizedData)!; - final img.Image resized = img.copyResize(image, width: 250, height: 250); + final img.Image resized = img.copyResize(image, width: 200, height: 200); resizedData = img.encodePng(resized); return Image( image: MemoryImage(resizedData), ); } + + Future?> getTicketDetails() async { + if (await ticketRepository.ticketDetailsFileExists() == false) return null; + + final File ticketDetailsFile = await ticketRepository.getTicketDetailsFile(); + + Map? ticketDetails; + + try { + ticketDetails = jsonDecode(await ticketDetailsFile.readAsString()); + } catch (e) { + return null; + } + + if (ticketDetails == null || ticketDetails['owner'] == null) { + return null; + } + + return ticketDetails; + } } diff --git a/lib/pages/wallet/ticket/ticket_web_view.dart b/lib/pages/wallet/ticket/ticket_web_view.dart deleted file mode 100644 index fe9cf813..00000000 --- a/lib/pages/wallet/ticket/ticket_web_view.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'dart:io'; -import 'package:campus_app/utils/constants.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; - -import 'package:campus_app/core/themes.dart'; -import 'package:campus_app/main.dart'; -import 'package:campus_app/utils/widgets/campus_icon_button.dart'; -import 'package:visibility_detector/visibility_detector.dart'; - -/// This page shows an [InAppWebView] in order to display external -/// websites from the helpful ressources that are not yet natively implemented -class TicketWebViewPage extends StatefulWidget { - /// The url that should be opened - - const TicketWebViewPage({ - Key? key, - }) : super(key: key); - - @override - State createState() => _TicketWebViewPageState(); -} - -class _TicketWebViewPageState extends State { - InAppWebViewController? webViewController; - late PullToRefreshController pullToRefreshController; - - InAppWebViewSettings settings = InAppWebViewSettings( - mediaPlaybackRequiresUserGesture: false, - verticalScrollBarEnabled: false, - horizontalScrollBarEnabled: false, - allowsInlineMediaPlayback: true, - useHybridComposition: false, - ); - - @override - void initState() { - super.initState(); - - pullToRefreshController = PullToRefreshController( - settings: PullToRefreshSettings(color: Colors.black), - onRefresh: () async { - if (Platform.isAndroid) { - await webViewController?.reload(); - } else if (Platform.isIOS) { - await webViewController?.loadUrl(urlRequest: URLRequest(url: await webViewController?.getUrl())); - } - }, - ); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: false, - onPopInvoked: (didPop) async { - if (didPop) return; - final navigator = Navigator.of(context); - if (await webViewController!.canGoBack()) { - await webViewController?.goBack(); - } else { - if (homeKey.currentState != null) { - homeKey.currentState!.setSwipeDisabled(); - } - navigator.pop(); - } - }, - child: VisibilityDetector( - onVisibilityChanged: (info) { - final bool isVisible = info.visibleFraction > 0; - - if (isVisible) { - if (homeKey.currentState != null) { - homeKey.currentState!.setSwipeDisabled(disableSwipe: true); - } - } - }, - key: const Key('visibility-key'), - child: Scaffold( - backgroundColor: Provider.of(context).currentThemeData.colorScheme.background, - body: SafeArea( - child: Stack( - children: [ - InAppWebView( - gestureRecognizers: >{} - ..add(const Factory(VerticalDragGestureRecognizer.new)), - pullToRefreshController: pullToRefreshController, - initialSettings: settings, - initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), - onWebViewCreated: (controller) { - controller.addJavaScriptHandler( - handlerName: 'error', - callback: (args) { - debugPrint('An error occurred. Error: $args'); - - //headlessWebView!.dispose(); - }, - ); - - controller.addJavaScriptHandler( - handlerName: 'dispose', - callback: (args) { - //headlessWebView!.dispose(); - print(args); - }, - ); - }, - onLoadStop: (controller, url) async { - if (url.toString() == rideTicketing) { - await controller.evaluateJavascript( - source: ''' - setTimeout(function(){ - window.flutter_inappwebview.callHandler('dispose', Object.getOwnPropertyNames(document.getElementsByTagName("lib-icon-button")[0])); - document.getElementsByTagName("lib-profile-icon-button")[0].__zone_symbol__loginIconClickfalse(); - } , 500); - ''', - ); - } - }, - ), - // Back button - Padding( - padding: const EdgeInsets.only(top: 20, left: 20, right: 20), - child: CampusIconButton( - iconPath: 'assets/img/icons/arrow-left.svg', - onTap: () { - Navigator.maybePop(context); - }, - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/wallet/widgets/ticket_login_screen.dart b/lib/pages/wallet/widgets/ticket_login_screen.dart index 612f2f02..608ecb04 100644 --- a/lib/pages/wallet/widgets/ticket_login_screen.dart +++ b/lib/pages/wallet/widgets/ticket_login_screen.dart @@ -12,7 +12,8 @@ import 'package:campus_app/utils/widgets/campus_textfield.dart'; import 'package:campus_app/utils/widgets/campus_button.dart'; class TicketLoginScreen extends StatefulWidget { - const TicketLoginScreen({super.key}); + final void Function() onTicketLoaded; + const TicketLoginScreen({super.key, required this.onTicketLoaded}); @override State createState() => _TicketLoginScreenState(); @@ -59,6 +60,7 @@ class _TicketLoginScreenState extends State { const Padding(padding: EdgeInsets.only(top: 10)), Center( child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'assets/img/icons/rub-link.png', @@ -68,7 +70,7 @@ class _TicketLoginScreenState extends State { width: 80, filterQuality: FilterQuality.high, ), - const Padding(padding: EdgeInsets.only(top: 25)), + const Padding(padding: EdgeInsets.only(top: 30)), CampusTextField( textFieldController: usernameController, textFieldText: 'RUB LoginID', @@ -98,14 +100,23 @@ class _TicketLoginScreenState extends State { loading = true; }); + final previousLoginId = await secureStorage.read(key: 'loginId'); + final previousPassword = await secureStorage.read(key: 'password'); + await secureStorage.write(key: 'loginId', value: usernameController.text); await secureStorage.write(key: 'password', value: passwordController.text); try { await ticketRepository.loadTicket(); + widget.onTicketLoaded(); navigator.pop(); } catch (e) { if (e is InvalidLoginIDAndPasswordException) { + if (previousLoginId != null && previousPassword != null) { + await secureStorage.write(key: 'loginId', value: previousLoginId); + await secureStorage.write(key: 'password', value: previousPassword); + } + setState(() { errorMessage = 'Falsche LoginID und/oder Passwort!'; showErrorMessage = true; @@ -118,6 +129,38 @@ class _TicketLoginScreenState extends State { }, ), const Padding(padding: EdgeInsets.only(top: 25)), + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/img/icons/info.svg', + colorFilter: ColorFilter.mode( + Provider.of(context).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1), + BlendMode.srcIn, + ), + width: 18, + ), + const Padding( + padding: EdgeInsets.only(left: 5), + ), + SizedBox( + width: 320, + child: Text( + 'Deine Daten werden verschlüsselt auf deinem Gerät gespeichert und nur bei der Anmeldung an die RUB gesendet.', + style: Provider.of(context).currentThemeData.textTheme.labelSmall!.copyWith( + color: Provider.of(context).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1), + ), + overflow: TextOverflow.clip, + ), + ), + ], + ), + ), if (showErrorMessage) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, @@ -131,11 +174,11 @@ class _TicketLoginScreenState extends State { width: 18, ), const Padding( - padding: EdgeInsets.only(left: 3), + padding: EdgeInsets.only(left: 5), ), Text( errorMessage, - style: Provider.of(context).currentThemeData.textTheme.labelMedium!.copyWith( + style: Provider.of(context).currentThemeData.textTheme.labelSmall!.copyWith( color: Provider.of(context).currentTheme == AppThemes.light ? Colors.black : const Color.fromRGBO(184, 186, 191, 1)), diff --git a/lib/pages/wallet/widgets/wallet.dart b/lib/pages/wallet/widgets/wallet.dart index c2d6c1fa..bcb1f5f9 100644 --- a/lib/pages/wallet/widgets/wallet.dart +++ b/lib/pages/wallet/widgets/wallet.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:campus_app/core/injection.dart'; import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; import 'package:campus_app/pages/wallet/ticket/ticket_usecases.dart'; -import 'package:campus_app/pages/wallet/ticket/ticket_web_view.dart'; import 'package:campus_app/pages/wallet/widgets/ticket_login_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -22,16 +21,21 @@ class CampusWallet extends StatelessWidget { @override Widget build(BuildContext context) { - final double initialWalletOffset = (MediaQuery.of(context).size.width - 325) / 2; - const double initialWalletOffsetTablet = (550 - 325) / 2; + final double initialWalletOffset = + (MediaQuery.of(context).size.width - (MediaQuery.of(context).size.width - 70)) / 2; + final double initialWalletOffsetTablet = (MediaQuery.of(context).size.width - 500) / 2; return StackedCardCarousel( cardAlignment: CardAlignment.center, scrollDirection: Axis.horizontal, - initialOffset: MediaQuery.of(context).size.shortestSide < 600 ? initialWalletOffset : initialWalletOffsetTablet, + initialOffset: + MediaQuery.of(context).size.shortestSide < 600 ? initialWalletOffset : initialWalletOffsetTablet + 30, spaceBetweenItems: MediaQuery.of(context).size.shortestSide < 600 ? 400 : 500, - items: const [ - SizedBox(width: 325, height: 217, child: BogestraTicket()), + items: [ + SizedBox( + width: MediaQuery.of(context).size.shortestSide < 600 ? MediaQuery.of(context).size.width - 70 : 330, + height: 217, + child: const BogestraTicket()), ], ); } @@ -49,22 +53,24 @@ class _BogestraTicketState extends State with AutomaticKeepAlive String scannedValue = ''; late Image qrCodeImage; + late Map ticketDetails; bool showQrCode = false; + TicketRepository ticketRepository = sl(); TicketUsecases ticketUsecases = sl(); /// Loads the previously saved image of the semester ticket and /// the corresponding aztec-code - Future loadTicket() async { - debugPrint('Loading semester ticket'); - + Future renderTicket() async { final Image? qrCodeImage = await ticketUsecases.renderQRCode(); + final Map? ticketDetails = await ticketUsecases.getTicketDetails(); - if (qrCodeImage != null) { + if (qrCodeImage != null && ticketDetails != null) { setState(() { scanned = true; this.qrCodeImage = qrCodeImage; + this.ticketDetails = ticketDetails; }); } } @@ -73,7 +79,11 @@ class _BogestraTicketState extends State with AutomaticKeepAlive await Navigator.push( context, MaterialPageRoute( - builder: (context) => const TicketWebViewPage(), + builder: (context) => TicketLoginScreen( + onTicketLoaded: () async { + await renderTicket(); + }, + ), ), ); } @@ -85,7 +95,10 @@ class _BogestraTicketState extends State with AutomaticKeepAlive void initState() { super.initState(); - loadTicket(); + ticketRepository.loadTicket().catchError((error) { + debugPrint('Wallet widget: $error'); + }); + renderTicket(); } @override @@ -119,7 +132,92 @@ class _BogestraTicketState extends State with AutomaticKeepAlive } }, onLongPress: addTicket, - child: qrCodeImage, + child: showQrCode + ? qrCodeImage + : Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 5), + child: SvgPicture.asset( + 'assets/img/bogestra-logo.svg', + height: 60, + width: 30, + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: SizedBox( + width: 130, + height: 130, + child: qrCodeImage, + ), + ), + const Expanded(child: SizedBox()), + Padding( + padding: const EdgeInsets.only(right: 10, left: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Deutschlandsemesterticket', + style: Provider.of(context) + .currentThemeData + .textTheme + .headlineSmall! + .copyWith(color: Colors.black, fontSize: 12.5), + overflow: TextOverflow.ellipsis, + ), + Text( + ticketDetails['owner'], + style: Provider.of(context) + .currentThemeData + .textTheme + .headlineSmall! + .copyWith(color: Colors.black, fontSize: 12.5), + ), + Text( + 'Geburtstag: ${ticketDetails['birthdate']}', + style: Provider.of(context) + .currentThemeData + .textTheme + .bodyMedium! + .copyWith(color: Colors.black, fontSize: 12), + ), + Text( + 'Von: ${ticketDetails['valid_from']}', + style: Provider.of(context) + .currentThemeData + .textTheme + .bodyMedium! + .copyWith(color: Colors.black, fontSize: 12), + ), + Text( + 'Bis: ${ticketDetails['valid_till']}', + style: Provider.of(context) + .currentThemeData + .textTheme + .bodyMedium! + .copyWith(color: Colors.black, fontSize: 12), + ), + if (ticketDetails['validity_region'].toString().isNotEmpty) + Text( + 'Geltungsbereich: ${ticketDetails['validity_region']}', + style: Provider.of(context) + .currentThemeData + .textTheme + .bodyMedium! + .copyWith(color: Colors.black, fontSize: 12), + ), + ], + ), + ), + ], + ), + ], + ), ) : CustomButton( tapHandler: addTicket, diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index b03e4c9a..301f42ae 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -16,7 +16,8 @@ const String mensaData = 'https://api-app.asta-bochum.de/get_meal'; const String osrmBackend = 'https://osrm.app.asta-bochum.de'; -const String rideTicketing = 'https://abo.ride-ticketing.de/app/login?partnerId=61b1cbf4604e623aef325ef0e4226cea'; +const String rideTicketing = + 'https://auth.ride-ticketing.de/auth/realms/ride/protocol/openid-connect/logout?redirect_uri=https%3A%2F%2Fabo.ride-ticketing.de%2Fapp%2Fprofile%3FpartnerId%3D61b1cbf4604e623aef325ef0e4226cea'; // See: https://codewithandrea.com/articles/flutter-api-keys-dart-define-env-files/ final String mensaApiKey = Env.mensaApiKey; From 523d9bb627cfa82f42760f9185d75d009ec6ebf6 Mon Sep 17 00:00:00 2001 From: henry-herrmann Date: Wed, 27 Mar 2024 20:39:52 +0100 Subject: [PATCH 08/16] Add privacy manifest file for iOS --- ios/Runner.xcodeproj/project.pbxproj | 4 ++++ ios/Runner/PrivacyInfo.xcprivacy | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 ios/Runner/PrivacyInfo.xcprivacy diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 96d4f080..4edf3991 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; D9D1457A69A7F122B3FE81D8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0756A62BDDAF9D89B947FE5A /* Pods_Runner.framework */; }; + FB4008602BB4AC74007EAF92 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FB40085F2BB4AC74007EAF92 /* PrivacyInfo.xcprivacy */; }; FBBEF41D2A214655000BABFD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = FBBEF41C2A214655000BABFD /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ @@ -49,6 +50,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F8216C20DF2A8BB04CD1C856 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + FB40085F2BB4AC74007EAF92 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; FBBEF41C2A214655000BABFD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -107,6 +109,7 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + FB40085F2BB4AC74007EAF92 /* PrivacyInfo.xcprivacy */, ); path = Runner; sourceTree = ""; @@ -192,6 +195,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + FB4008602BB4AC74007EAF92 /* PrivacyInfo.xcprivacy in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, FBBEF41D2A214655000BABFD /* GoogleService-Info.plist in Resources */, diff --git a/ios/Runner/PrivacyInfo.xcprivacy b/ios/Runner/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..b0dbd3c1 --- /dev/null +++ b/ios/Runner/PrivacyInfo.xcprivacy @@ -0,0 +1,24 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + From a9adbe00d4e636082d4fbad03a71b7178733984e Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Mon, 1 Apr 2024 16:43:57 +0200 Subject: [PATCH 09/16] UI improvements & code refactoring & inline documentation --- assets/img/icons/error.svg | 1 + lib/core/injection.dart | 16 +- lib/env/env.g.dart | 20 +-- .../wallet/ticket/ticket_datasource.dart | 113 ++++++------- .../wallet/ticket/ticket_repository.dart | 63 ++++---- lib/pages/wallet/ticket/ticket_usecases.dart | 2 + lib/pages/wallet/ticket_fullscreen.dart | 44 +----- .../{widgets => }/ticket_login_screen.dart | 148 ++++++++++-------- lib/pages/wallet/widgets/wallet.dart | 114 +++++++------- lib/utils/pages/wallet_utils.dart | 15 ++ lib/utils/widgets/campus_textfield.dart | 7 + pubspec.yaml | 1 + test/pages/wallet/ticket_repository_test.dart | 17 -- 13 files changed, 281 insertions(+), 280 deletions(-) create mode 100644 assets/img/icons/error.svg rename lib/pages/wallet/{widgets => }/ticket_login_screen.dart (67%) create mode 100644 lib/utils/pages/wallet_utils.dart delete mode 100644 test/pages/wallet/ticket_repository_test.dart diff --git a/assets/img/icons/error.svg b/assets/img/icons/error.svg new file mode 100644 index 00000000..803c8cbe --- /dev/null +++ b/assets/img/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/core/injection.dart b/lib/core/injection.dart index 4744a0b0..bbb059ad 100644 --- a/lib/core/injection.dart +++ b/lib/core/injection.dart @@ -1,8 +1,5 @@ import 'package:appwrite/appwrite.dart'; -import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; -import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; -import 'package:campus_app/pages/wallet/ticket/ticket_usecases.dart'; -import 'package:campus_app/utils/pages/main_utils.dart'; +import 'package:campus_app/utils/pages/wallet_utils.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -17,16 +14,17 @@ import 'package:campus_app/pages/mensa/mensa_datasource.dart'; import 'package:campus_app/pages/mensa/mensa_repository.dart'; import 'package:campus_app/pages/mensa/mensa_usecases.dart'; -//import 'package:campus_app/pages/ecampus/bloc/ecampus_bloc.dart'; -//import 'package:campus_app/pages/ecampus/ticket_datasource.dart'; -//import 'package:campus_app/pages/ecampus/ticket_repository.dart'; import 'package:campus_app/pages/feed/news/news_datasource.dart'; import 'package:campus_app/pages/feed/news/news_repository.dart'; import 'package:campus_app/pages/feed/news/news_usecases.dart'; -import 'package:campus_app/utils/dio_utils.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_usecases.dart'; import 'package:campus_app/utils/pages/calendar_utils.dart'; import 'package:campus_app/utils/pages/feed_utils.dart'; import 'package:campus_app/utils/pages/mensa_utils.dart'; +import 'package:campus_app/utils/pages/main_utils.dart'; +import 'package:campus_app/utils/dio_utils.dart'; import 'package:campus_app/utils/constants.dart'; import 'package:native_dio_adapter/native_dio_adapter.dart'; @@ -130,8 +128,8 @@ Future init() async { sl.registerLazySingleton(CalendarUtils.new); sl.registerLazySingleton(FeedUtils.new); sl.registerLazySingleton(MensaUtils.new); - sl.registerLazySingleton(MainUtils.new); + sl.registerLazySingleton(WalletUtils.new); //! //! External diff --git a/lib/env/env.g.dart b/lib/env/env.g.dart index 04a4f670..42c12a57 100644 --- a/lib/env/env.g.dart +++ b/lib/env/env.g.dart @@ -169,14 +169,11 @@ final class _Env { 1270653875, ]; - static final String firebaseAndroidApiKey = String.fromCharCodes( - List.generate( + static final String firebaseAndroidApiKey = String.fromCharCodes(List.generate( _envieddatafirebaseAndroidApiKey.length, (int i) => i, growable: false, - ).map((int i) => - _envieddatafirebaseAndroidApiKey[i] ^ - _enviedkeyfirebaseAndroidApiKey[i])); + ).map((int i) => _envieddatafirebaseAndroidApiKey[i] ^ _enviedkeyfirebaseAndroidApiKey[i])); static const List _enviedkeyfirebaseIosApiKey = [ 2506337707, @@ -262,13 +259,11 @@ final class _Env { 202399814, ]; - static final String firebaseIosApiKey = String.fromCharCodes( - List.generate( + static final String firebaseIosApiKey = String.fromCharCodes(List.generate( _envieddatafirebaseIosApiKey.length, (int i) => i, growable: false, - ).map((int i) => - _envieddatafirebaseIosApiKey[i] ^ _enviedkeyfirebaseIosApiKey[i])); + ).map((int i) => _envieddatafirebaseIosApiKey[i] ^ _enviedkeyfirebaseIosApiKey[i])); static const List _enviedkeyappwriteCreateUserKey = [ 1025363171, @@ -370,14 +365,11 @@ final class _Env { 2047853346, ]; - static final String appwriteCreateUserKey = String.fromCharCodes( - List.generate( + static final String appwriteCreateUserKey = String.fromCharCodes(List.generate( _envieddataappwriteCreateUserKey.length, (int i) => i, growable: false, - ).map((int i) => - _envieddataappwriteCreateUserKey[i] ^ - _enviedkeyappwriteCreateUserKey[i])); + ).map((int i) => _envieddataappwriteCreateUserKey[i] ^ _enviedkeyappwriteCreateUserKey[i])); static const List _enviedkeysentryDsn = [ 2093328549, diff --git a/lib/pages/wallet/ticket/ticket_datasource.dart b/lib/pages/wallet/ticket/ticket_datasource.dart index 5062ed2a..3270b6ca 100644 --- a/lib/pages/wallet/ticket/ticket_datasource.dart +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -18,6 +18,7 @@ class TicketDataSource { final Completer> completer = Completer>(); + // Define empty ticket final Map ticket = { 'barcode': '', 'valid_from': '', @@ -27,34 +28,31 @@ class TicketDataSource { 'birthdate': '', }; + // Load the user's credentials final String? loginId = await secureStorage.read(key: 'loginId'); final String? password = await secureStorage.read(key: 'password'); + Timer? loginTimer; + if (loginId != null && password != null) { + // Create a headless web view HeadlessInAppWebView? headlessWebView; headlessWebView = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(rideTicketing)), initialSettings: InAppWebViewSettings(cacheEnabled: false, clearCache: true), onWebViewCreated: (controller) { + // Callback handler for the ticket controller.addJavaScriptHandler( - handlerName: 'barcode', - callback: (args) { - if (args.isNotEmpty && args[0] is String) { - final String image = List.from(args)[0].split(',')[1]; - - ticket['barcode'] = image; - } - }, - ); - - controller.addJavaScriptHandler( - handlerName: 'ticket_details', + handlerName: 'ticket', callback: (args) { - if (args.isEmpty || List.of(args)[0] is! List || List.of(args[0]).length != 4) { + if (args.isEmpty || List.of(args)[1] is! List || List.of(args[1]).isEmpty) { completer.completeError('Invalid ticket details'); } - final List arguments = List.of(args)[0]; + final List arguments = List.of(args)[1]; + final String image = List.from(args)[0].toString().split(',')[1]; + + ticket['barcode'] = image; if (arguments.length == 4) { ticket['valid_from'] = arguments[0]; @@ -76,19 +74,26 @@ class TicketDataSource { }, ); + // Error handler controller.addJavaScriptHandler( handlerName: 'error', callback: (args) { debugPrint('An error occurred. Error: $args'); + if (loginTimer != null) { + loginTimer!.cancel(); + } + completer.completeError(args[0]); headlessWebView!.dispose(); }, ); }, - onLoadStop: (controller, url) async { - if (url.toString().startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && - url.toString().endsWith('s1')) { + onLoadStop: (controller, uri) async { + final String url = uri.toString(); + + // Click through the RUB login and extract the ticket from the ticket portal + if (url.startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && url.endsWith('s1')) { Timer(const Duration(milliseconds: 300), () async { await controller.evaluateJavascript( source: """ @@ -96,50 +101,41 @@ class TicketDataSource { document.getElementById('password').value="$password"; setTimeout(function(){ document.getElementById('shibbutton').click(); - }, 500); + }, 100); """, ); }); - } else if (url.toString().startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && - url.toString().endsWith('s2')) { - await controller.evaluateJavascript( - source: """ - if(document.getElementsByClassName("form-error").length == 1) { - window.flutter_inappwebview.callHandler('error', "Invalid credentials."); - } - setTimeout(function(){ + } else if (url.startsWith('https://aai.ruhr-uni-bochum.de/idp/profile/SAML2/POST/SSO') && + url.endsWith('s2')) { + Timer.periodic(const Duration(milliseconds: 100), (ti) async { + loginTimer = ti; + await controller.evaluateJavascript( + source: """ + if(document.getElementsByClassName("form-error").length == 1) { + window.flutter_inappwebview.callHandler('error', "Invalid credentials."); + } document.getElementById('consentbutton_2').click(); - }, 500); - """, - ); - } else if (url.toString().startsWith('https://abo.ride-ticketing.de')) { + """, + ); + }); + } else if (url.startsWith('https://abo.ride-ticketing.de')) { await controller.evaluateJavascript( source: ''' - setTimeout(function(){ + const ticketClickInterval = setInterval(function(){ document.getElementsByClassName("abo-card-wrapper")[0].click(); - }, 1000); - ''', - ); - await controller.evaluateJavascript( - source: ''' - setTimeout(function(){ - if(!document.URL.startsWith("https://abo.ride-ticketing.de/app/ticket")) { - window.flutter_inappwebview.callHandler('error', "Could not open ticket page."); - return; - } - window.flutter_inappwebview.callHandler('stateChange', "ticketPage"); - - window.flutter_inappwebview.callHandler('barcode', document.getElementsByClassName("barcode")[0].src); - - const ticket_details = document.getElementsByClassName("value-column"); - const arr = []; - - for(const detail of ticket_details) { - arr.push(detail.innerText); - } - - window.flutter_inappwebview.callHandler('ticket_details', arr); - }, 2500); + }, 100); + + setInterval(function(){ + if(document.URL.startsWith("https://abo.ride-ticketing.de/app/ticket")) { + clearInterval(ticketClickInterval); + const ticket_details = document.getElementsByClassName("value-column"); + const arr = []; + for(const detail of ticket_details) { + arr.push(detail.innerText); + } + window.flutter_inappwebview.callHandler('ticket', document.getElementsByClassName("barcode")[0].src, arr); + } + }, 200); ''', ); } @@ -148,8 +144,13 @@ class TicketDataSource { await headlessWebView.run(); - Timer(const Duration(seconds: 15), () async { - if (headlessWebView != null) { + // Fallback error handler, in case the webview hang up + Timer(const Duration(seconds: 10), () async { + if (loginTimer != null) { + loginTimer!.cancel(); + } + if (headlessWebView != null && headlessWebView.isRunning()) { + completer.completeError('Could not open ticket page.'); await headlessWebView.dispose(); } }); diff --git a/lib/pages/wallet/ticket/ticket_repository.dart b/lib/pages/wallet/ticket/ticket_repository.dart index fd5a49e0..d7c0f97e 100644 --- a/lib/pages/wallet/ticket/ticket_repository.dart +++ b/lib/pages/wallet/ticket/ticket_repository.dart @@ -13,6 +13,7 @@ class TicketRepository { required this.ticketDataSource, }); + /// Load the semester ticket Future loadTicket() async { Map? ticket; @@ -35,75 +36,81 @@ class TicketRepository { await saveTicket(ticket); } - Future saveTicket(Map ticket) async { + /// Load the qr code file from storage + Future getQRCodeFile() async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; - // Save the given png file to the app directory + // Define the image file final File qrCodeFile = File('$directoryPath/ticket.png'); - final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - await qrCodeFile.writeAsBytes(base64Decode(ticket['barcode'])); - await ticketDetailsFile.writeAsString(jsonEncode(ticket)); + return qrCodeFile; } - Future deleteTicket() async { + /// Load the ticket details file from storage + Future getTicketDetailsFile() async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; - // Define the image file - final File qrCodeFile = File('$directoryPath/ticket.png'); + // Define the ticket details file final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - if (await qrCodeFileExists()) { - await qrCodeFile.delete(); - } - - if (await ticketDetailsFileExists()) { - await ticketDetailsFile.delete(); - } + return ticketDetailsFile; } - Future getQRCodeFile() async { + /// Checks whether the qr code file exists + Future qrCodeFileExists() async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; // Define the image file final File qrCodeFile = File('$directoryPath/ticket.png'); - return qrCodeFile; + final bool exists = qrCodeFile.existsSync(); + + return exists; } - Future getTicketDetailsFile() async { + /// Checks whether the ticket details file exists + Future ticketDetailsFileExists() async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - return ticketDetailsFile; + final bool exists = ticketDetailsFile.existsSync(); + + return exists; } - Future qrCodeFileExists() async { + /// Save both the QR code and it's details to storage + Future saveTicket(Map ticket) async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; - // Define the image file + // Save the given png file to the app directory final File qrCodeFile = File('$directoryPath/ticket.png'); + final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - // If the images were parsed and saved in the past, they're loaded - final bool exists = qrCodeFile.existsSync(); - - return exists; + await qrCodeFile.writeAsBytes(base64Decode(ticket['barcode'])); + await ticketDetailsFile.writeAsString(jsonEncode(ticket)); } - Future ticketDetailsFileExists() async { + /// Delete the ticket files + Future deleteTicket() async { final Directory saveDirectory = await getApplicationDocumentsDirectory(); final String directoryPath = saveDirectory.path; + // Define the image file + final File qrCodeFile = File('$directoryPath/ticket.png'); final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - final bool exists = ticketDetailsFile.existsSync(); + if (await qrCodeFileExists()) { + await qrCodeFile.delete(); + } - return exists; + if (await ticketDetailsFileExists()) { + await ticketDetailsFile.delete(); + } } } diff --git a/lib/pages/wallet/ticket/ticket_usecases.dart b/lib/pages/wallet/ticket/ticket_usecases.dart index 97641d19..b365dd79 100644 --- a/lib/pages/wallet/ticket/ticket_usecases.dart +++ b/lib/pages/wallet/ticket/ticket_usecases.dart @@ -14,6 +14,7 @@ class TicketUsecases { required this.ticketRepository, }); + /// Render the QR code and resize it Future renderQRCode() async { if (await ticketRepository.qrCodeFileExists() == false) return null; @@ -29,6 +30,7 @@ class TicketUsecases { ); } + /// Parse the content of the ticket details file Future?> getTicketDetails() async { if (await ticketRepository.ticketDetailsFileExists() == false) return null; diff --git a/lib/pages/wallet/ticket_fullscreen.dart b/lib/pages/wallet/ticket_fullscreen.dart index fa05d4b2..e7fdd2e1 100644 --- a/lib/pages/wallet/ticket_fullscreen.dart +++ b/lib/pages/wallet/ticket_fullscreen.dart @@ -1,15 +1,13 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; -import 'package:pdfx/pdfx.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import 'package:campus_app/core/injection.dart'; import 'package:campus_app/core/themes.dart'; - +import 'package:campus_app/pages/wallet/ticket/ticket_usecases.dart'; import 'package:campus_app/utils/widgets/campus_icon_button.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -23,45 +21,17 @@ class BogestraTicketFullScreen extends StatefulWidget { class _BogestraTicketFullScreenState extends State { Image? qrCodeImage; - Future renderQRCode(String path) async { - final document = await PdfDocument.openFile(path); - final page = await document.getPage(1); - final pageImage = await page.render( - width: page.width * 2.4, - height: page.height * 2.4, - cropRect: Rect.fromLTWH(174, 250, page.width - 325, 269), - ); - await page.close(); - - if (pageImage == null) { - return Image(image: MemoryImage(Uint8List.fromList([0]))); - } - - return Image( - image: MemoryImage(pageImage.bytes), - ); - } + TicketUsecases ticketUsecases = sl(); /// Loads the previously saved image of the semester ticket and /// the corresponding aztec-code - Future loadTicket() async { - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; - - // Define the image files - final File ticketFile = File('$directoryPath/ticket.pdf'); - - // If the images were parsed and saved in the past, they're loaded - final bool tickedSaved = ticketFile.existsSync(); - if (tickedSaved) { - final Image qrCodeImage = await renderQRCode(ticketFile.path); + Future renderTicket() async { + final Image? qrCodeImage = await ticketUsecases.renderQRCode(); + if (qrCodeImage != null) { setState(() { this.qrCodeImage = qrCodeImage; }); - } else { - // ignore: use_build_context_synchronously - Navigator.pop(context); } } @@ -71,7 +41,7 @@ class _BogestraTicketFullScreenState extends State { setBrightness(1); - loadTicket(); + renderTicket(); } @override diff --git a/lib/pages/wallet/widgets/ticket_login_screen.dart b/lib/pages/wallet/ticket_login_screen.dart similarity index 67% rename from lib/pages/wallet/widgets/ticket_login_screen.dart rename to lib/pages/wallet/ticket_login_screen.dart index 608ecb04..c46e9e8b 100644 --- a/lib/pages/wallet/widgets/ticket_login_screen.dart +++ b/lib/pages/wallet/ticket_login_screen.dart @@ -1,4 +1,3 @@ -import 'package:campus_app/core/exceptions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; @@ -6,7 +5,9 @@ import 'package:provider/provider.dart'; import 'package:campus_app/core/injection.dart'; import 'package:campus_app/core/themes.dart'; +import 'package:campus_app/core/exceptions.dart'; import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; +import 'package:campus_app/utils/pages/wallet_utils.dart'; import 'package:campus_app/utils/widgets/campus_icon_button.dart'; import 'package:campus_app/utils/widgets/campus_textfield.dart'; import 'package:campus_app/utils/widgets/campus_button.dart'; @@ -22,6 +23,7 @@ class TicketLoginScreen extends StatefulWidget { class _TicketLoginScreenState extends State { final TicketRepository ticketRepository = sl(); final FlutterSecureStorage secureStorage = sl(); + final WalletUtils walletUtils = sl(); final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); @@ -64,9 +66,7 @@ class _TicketLoginScreenState extends State { children: [ Image.asset( 'assets/img/icons/rub-link.png', - color: Provider.of(context).currentTheme == AppThemes.light - ? Colors.black - : Colors.white, + color: const Color.fromRGBO(0, 53, 96, 1), width: 80, filterQuality: FilterQuality.high, ), @@ -74,14 +74,49 @@ class _TicketLoginScreenState extends State { CampusTextField( textFieldController: usernameController, textFieldText: 'RUB LoginID', + onTap: () { + setState(() { + showErrorMessage = false; + }); + }, ), const Padding(padding: EdgeInsets.only(top: 10)), CampusTextField( textFieldController: passwordController, obscuredInput: true, textFieldText: 'RUB Passwort', + onTap: () { + setState(() { + showErrorMessage = false; + }); + }, ), const Padding(padding: EdgeInsets.only(top: 15)), + if (showErrorMessage) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/img/icons/error.svg', + colorFilter: const ColorFilter.mode( + Colors.redAccent, + BlendMode.srcIn, + ), + width: 18, + ), + const Padding( + padding: EdgeInsets.only(left: 5), + ), + Text( + errorMessage, + style: Provider.of(context).currentThemeData.textTheme.labelSmall!.copyWith( + color: Colors.redAccent, + ), + ), + ], + ), + ], + const Padding(padding: EdgeInsets.only(top: 15)), CampusButton( text: 'Login', onTap: () async { @@ -95,6 +130,14 @@ class _TicketLoginScreenState extends State { return; } + if (await walletUtils.hasNetwork() == false) { + setState(() { + errorMessage = 'Überprüfe deine Internetverbindung!'; + showErrorMessage = true; + }); + return; + } + setState(() { showErrorMessage = false; loading = true; @@ -112,15 +155,20 @@ class _TicketLoginScreenState extends State { navigator.pop(); } catch (e) { if (e is InvalidLoginIDAndPasswordException) { - if (previousLoginId != null && previousPassword != null) { - await secureStorage.write(key: 'loginId', value: previousLoginId); - await secureStorage.write(key: 'password', value: previousPassword); - } - setState(() { errorMessage = 'Falsche LoginID und/oder Passwort!'; showErrorMessage = true; }); + } else { + setState(() { + errorMessage = 'Fehler beim Laden des Tickets!'; + showErrorMessage = true; + }); + } + + if (previousLoginId != null && previousPassword != null) { + await secureStorage.write(key: 'loginId', value: previousLoginId); + await secureStorage.write(key: 'password', value: previousPassword); } } setState(() { @@ -129,63 +177,37 @@ class _TicketLoginScreenState extends State { }, ), const Padding(padding: EdgeInsets.only(top: 25)), - Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - 'assets/img/icons/info.svg', - colorFilter: ColorFilter.mode( - Provider.of(context).currentTheme == AppThemes.light - ? Colors.black - : const Color.fromRGBO(184, 186, 191, 1), - BlendMode.srcIn, - ), - width: 18, - ), - const Padding( - padding: EdgeInsets.only(left: 5), - ), - SizedBox( - width: 320, - child: Text( - 'Deine Daten werden verschlüsselt auf deinem Gerät gespeichert und nur bei der Anmeldung an die RUB gesendet.', - style: Provider.of(context).currentThemeData.textTheme.labelSmall!.copyWith( - color: Provider.of(context).currentTheme == AppThemes.light - ? Colors.black - : const Color.fromRGBO(184, 186, 191, 1), - ), - overflow: TextOverflow.clip, - ), - ), - ], - ), - ), - if (showErrorMessage) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - 'assets/img/icons/info.svg', - colorFilter: const ColorFilter.mode( - Colors.redAccent, - BlendMode.srcIn, - ), - width: 18, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/img/icons/info.svg', + colorFilter: ColorFilter.mode( + Provider.of(context).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1), + BlendMode.srcIn, ), - const Padding( - padding: EdgeInsets.only(left: 5), - ), - Text( - errorMessage, + width: 18, + ), + const Padding( + padding: EdgeInsets.only(left: 8), + ), + SizedBox( + width: 320, + child: Text( + 'Deine Daten werden verschlüsselt auf deinem Gerät gespeichert und nur bei der Anmeldung an die RUB gesendet.', style: Provider.of(context).currentThemeData.textTheme.labelSmall!.copyWith( - color: Provider.of(context).currentTheme == AppThemes.light - ? Colors.black - : const Color.fromRGBO(184, 186, 191, 1)), + color: Provider.of(context).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1), + ), + overflow: TextOverflow.clip, ), - ], - ), - ], + ), + ], + ), + const Padding(padding: EdgeInsets.only(top: 25)), if (loading) ...[ CircularProgressIndicator( backgroundColor: Provider.of(context).currentThemeData.cardColor, diff --git a/lib/pages/wallet/widgets/wallet.dart b/lib/pages/wallet/widgets/wallet.dart index bcb1f5f9..172f5329 100644 --- a/lib/pages/wallet/widgets/wallet.dart +++ b/lib/pages/wallet/widgets/wallet.dart @@ -1,17 +1,17 @@ import 'dart:async'; -import 'package:campus_app/core/injection.dart'; -import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; -import 'package:campus_app/pages/wallet/ticket/ticket_usecases.dart'; -import 'package:campus_app/pages/wallet/widgets/ticket_login_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import 'package:campus_app/core/injection.dart'; import 'package:campus_app/core/settings.dart'; import 'package:campus_app/core/themes.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_repository.dart'; +import 'package:campus_app/pages/wallet/ticket/ticket_usecases.dart'; +import 'package:campus_app/pages/wallet/ticket_login_screen.dart'; import 'package:campus_app/pages/wallet/ticket_fullscreen.dart'; import 'package:campus_app/pages/wallet/widgets/stacked_card_carousel.dart'; import 'package:campus_app/utils/widgets/custom_button.dart'; @@ -33,9 +33,10 @@ class CampusWallet extends StatelessWidget { spaceBetweenItems: MediaQuery.of(context).size.shortestSide < 600 ? 400 : 500, items: [ SizedBox( - width: MediaQuery.of(context).size.shortestSide < 600 ? MediaQuery.of(context).size.width - 70 : 330, - height: 217, - child: const BogestraTicket()), + width: MediaQuery.of(context).size.shortestSide < 600 ? MediaQuery.of(context).size.width - 70 : 330, + height: 217, + child: const BogestraTicket(), + ), ], ); } @@ -60,8 +61,7 @@ class _BogestraTicketState extends State with AutomaticKeepAlive TicketRepository ticketRepository = sl(); TicketUsecases ticketUsecases = sl(); - /// Loads the previously saved image of the semester ticket and - /// the corresponding aztec-code + /// Loads the previously saved image of the semester ticket and the corresponding ticket details Future renderTicket() async { final Image? qrCodeImage = await ticketUsecases.renderQRCode(); final Map? ticketDetails = await ticketUsecases.getTicketDetails(); @@ -158,60 +158,62 @@ class _BogestraTicketState extends State with AutomaticKeepAlive const Expanded(child: SizedBox()), Padding( padding: const EdgeInsets.only(right: 10, left: 5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Deutschlandsemesterticket', - style: Provider.of(context) - .currentThemeData - .textTheme - .headlineSmall! - .copyWith(color: Colors.black, fontSize: 12.5), - overflow: TextOverflow.ellipsis, - ), - Text( - ticketDetails['owner'], - style: Provider.of(context) - .currentThemeData - .textTheme - .headlineSmall! - .copyWith(color: Colors.black, fontSize: 12.5), - ), - Text( - 'Geburtstag: ${ticketDetails['birthdate']}', - style: Provider.of(context) - .currentThemeData - .textTheme - .bodyMedium! - .copyWith(color: Colors.black, fontSize: 12), - ), - Text( - 'Von: ${ticketDetails['valid_from']}', - style: Provider.of(context) - .currentThemeData - .textTheme - .bodyMedium! - .copyWith(color: Colors.black, fontSize: 12), - ), - Text( - 'Bis: ${ticketDetails['valid_till']}', - style: Provider.of(context) - .currentThemeData - .textTheme - .bodyMedium! - .copyWith(color: Colors.black, fontSize: 12), - ), - if (ticketDetails['validity_region'].toString().isNotEmpty) + child: SizedBox( + width: 200, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Deutschlandsemesterticket', + style: Provider.of(context) + .currentThemeData + .textTheme + .headlineSmall! + .copyWith(color: Colors.black, fontSize: 12.5), + ), + Text( + ticketDetails['owner'], + style: Provider.of(context) + .currentThemeData + .textTheme + .headlineSmall! + .copyWith(color: Colors.black, fontSize: 12.5), + ), + Text( + 'Geburtstag: ${ticketDetails['birthdate']}', + style: Provider.of(context) + .currentThemeData + .textTheme + .bodyMedium! + .copyWith(color: Colors.black, fontSize: 12), + ), + Text( + 'Von: ${ticketDetails['valid_from']}', + style: Provider.of(context) + .currentThemeData + .textTheme + .bodyMedium! + .copyWith(color: Colors.black, fontSize: 12), + ), Text( - 'Geltungsbereich: ${ticketDetails['validity_region']}', + 'Bis: ${ticketDetails['valid_till']}', style: Provider.of(context) .currentThemeData .textTheme .bodyMedium! .copyWith(color: Colors.black, fontSize: 12), ), - ], + if (ticketDetails['validity_region'].toString().isNotEmpty) + Text( + 'Geltungsbereich: ${ticketDetails['validity_region']}', + style: Provider.of(context) + .currentThemeData + .textTheme + .bodyMedium! + .copyWith(color: Colors.black, fontSize: 12), + ), + ], + ), ), ), ], diff --git a/lib/utils/pages/wallet_utils.dart b/lib/utils/pages/wallet_utils.dart new file mode 100644 index 00000000..fff96ac9 --- /dev/null +++ b/lib/utils/pages/wallet_utils.dart @@ -0,0 +1,15 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +class WalletUtils { + Future hasNetwork() async { + try { + final result = await InternetAddress.lookup('api-app.asta-bochum.de'); + return result.isNotEmpty && result[0].rawAddress.isNotEmpty; + } on SocketException catch (_) { + return false; + } + } +} diff --git a/lib/utils/widgets/campus_textfield.dart b/lib/utils/widgets/campus_textfield.dart index 90c7f005..06d84b73 100644 --- a/lib/utils/widgets/campus_textfield.dart +++ b/lib/utils/widgets/campus_textfield.dart @@ -23,11 +23,14 @@ class CampusTextField extends StatefulWidget { late final CampusTextFieldType type; + late final void Function()? onTap; + CampusTextField({ Key? key, required this.textFieldController, this.textFieldText = 'Confirm Password', this.obscuredInput = false, + this.onTap, }) : super(key: key) { type = CampusTextFieldType.normal; } @@ -58,6 +61,10 @@ class CampusTextFieldState extends State { () => setState(() { if (_focusNode.hasFocus) { hint = ''; + if (widget.onTap != null) { + // ignore: prefer_null_aware_method_calls + widget.onTap!(); + } } else { hint = widget.textFieldText; } diff --git a/pubspec.yaml b/pubspec.yaml index 1984076b..8d1964ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -126,6 +126,7 @@ flutter: - assets/img/icons/wallet-outlined.png - assets/img/icons/wallet-filled.png - assets/img/icons/vote.svg + - assets/img/icons/error.svg - assets/img/icons/more.png - assets/img/icons/settings.svg - assets/img/icons/info.svg diff --git a/test/pages/wallet/ticket_repository_test.dart b/test/pages/wallet/ticket_repository_test.dart deleted file mode 100644 index a009f0bb..00000000 --- a/test/pages/wallet/ticket_repository_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -@GenerateMocks([TicketDataSource]) -void main() { - //late TicketDataSource mockTicketDatasource; - - setUp(() { - //mockTicketDatasource = MockTicketDatasource(); - }); - - group('[getRemoteNewsfeed]', () { - test('Should return news list on successfully web request', () async {}); - }); -} From 56adf2687b4fcfea560185b01578ea790db3c68e Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Wed, 3 Apr 2024 18:38:36 +0200 Subject: [PATCH 10/16] Fix access on disposed web view controller & update privacy policy with the new AStA chairman --- lib/pages/more/privacy_policy_page.dart | 2 +- .../wallet/ticket/ticket_datasource.dart | 9 ++++++--- pubspec.lock | 20 +++++++++---------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/pages/more/privacy_policy_page.dart b/lib/pages/more/privacy_policy_page.dart index 42e8a174..b7fa9c7d 100644 --- a/lib/pages/more/privacy_policy_page.dart +++ b/lib/pages/more/privacy_policy_page.dart @@ -50,7 +50,7 @@ class PrivacyPolicyPage extends StatelessWidget {

Verantwortliche Stelle im Sinne der Datenschutzgesetze, insbesondere der EU-Datenschutzgrundverordnung (DSGVO), ist:



AStA an der Ruhr-Universität Bochum
- Hanife Demir (Vorsitzende)
+ Paul Hoffstiepel (Vorsitzender)

Studierendenhaus 0/11
Universitätsstr. 150
diff --git a/lib/pages/wallet/ticket/ticket_datasource.dart b/lib/pages/wallet/ticket/ticket_datasource.dart index 3270b6ca..cd6763ac 100644 --- a/lib/pages/wallet/ticket/ticket_datasource.dart +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -109,14 +109,17 @@ class TicketDataSource { url.endsWith('s2')) { Timer.periodic(const Duration(milliseconds: 100), (ti) async { loginTimer = ti; - await controller.evaluateJavascript( - source: """ + + if (headlessWebView != null && headlessWebView.isRunning()) { + await controller.evaluateJavascript( + source: """ if(document.getElementsByClassName("form-error").length == 1) { window.flutter_inappwebview.callHandler('error', "Invalid credentials."); } document.getElementById('consentbutton_2').click(); """, - ); + ); + } }); } else if (url.startsWith('https://abo.ride-ticketing.de')) { await controller.evaluateJavascript( diff --git a/pubspec.lock b/pubspec.lock index 9e478252..f3d0728b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.3+8" crypto: dependency: transitive description: @@ -1553,10 +1553,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.3" url_launcher_windows: dependency: transitive description: @@ -1657,18 +1657,18 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.4.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.3" win32: dependency: transitive description: @@ -1718,5 +1718,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.3.0-279.1.beta <4.0.0" + flutter: ">=3.16.6" From bd34178818db35ccb8d26236883cecb3d9d7675a Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Wed, 3 Apr 2024 18:40:40 +0200 Subject: [PATCH 11/16] Revert pubspec.lock --- pubspec.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f3d0728b..a4ebb9f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: dependency: transitive description: @@ -1553,10 +1553,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -1657,18 +1657,18 @@ packages: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" win32: dependency: transitive description: @@ -1718,5 +1718,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0-279.1.beta <4.0.0" - flutter: ">=3.16.6" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" \ No newline at end of file From 897cc0f3ae77fb2203798a627739d55feb589fc2 Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Wed, 3 Apr 2024 19:31:52 +0200 Subject: [PATCH 12/16] Documentation --- docs/wiki/Pages/ticket.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/wiki/Pages/ticket.md diff --git a/docs/wiki/Pages/ticket.md b/docs/wiki/Pages/ticket.md new file mode 100644 index 00000000..58d78058 --- /dev/null +++ b/docs/wiki/Pages/ticket.md @@ -0,0 +1,35 @@ +# Semesterticket + +This page is to provide a basic overview about the "Calendar" feature located inside +`lib/pages/wallet/ticket`. + +--- + +## Ticket usescases + +| Type | Name | Description | +|------|------|-------------| +| Future | renderQRCode() | Returns an Image object, containing the resized QR code loaded from storage. +| Future?> | getTicketDetails() | Returns a map, containing the ticket details such as the name of the owner, the birthdate and validity information. + +--- + +## Ticket repository + +| Type | Name | Description | +|------|------|-------------| +| Future | loadTicket() | Calls the ticket datasource to load the remote ticket and then saves it to storage or deletes the exisiting one if it expired or was removed. +| Future | getQRCodeFile() | Returns a File object of the saved QR code +| Future | getTicketDetailsFile() | Returns a File object of the saved ticket details +| Future | qrCodeFileExists() | Checks whether a QR code was saved to storage +| Future | ticketDetailsFileExists() | Checks whether ticket details were saved to storage +| Future | saveTicket(Map ticket) | Saves the QR Code and ticket details to two separate files using the passed Map +| Future | deleteTicket() | Deletes the ticket from storage + +--- + +## Ticket datasource + +| Type | Name | Description | +|------|------|-------------| +| Future> | getRemoteTicket() | Loads the remote ticket using a headless webview which clicks through the RUB login process and then extracts the QR code and ticket details from the RIDE website. From 6863476ef0c225cfa741d6e9beb9ad89c3d95c0b Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Sun, 7 Apr 2024 20:38:54 +0200 Subject: [PATCH 13/16] Save ticket to encrypted storage & naming changes --- docs/wiki/Pages/ticket.md | 12 ++- lib/core/injection.dart | 2 +- .../wallet/ticket/ticket_datasource.dart | 4 +- .../wallet/ticket/ticket_repository.dart | 87 ++++--------------- lib/pages/wallet/ticket/ticket_usecases.dart | 17 ++-- lib/pages/wallet/ticket_fullscreen.dart | 10 +-- lib/pages/wallet/widgets/wallet.dart | 22 ++--- 7 files changed, 50 insertions(+), 104 deletions(-) diff --git a/docs/wiki/Pages/ticket.md b/docs/wiki/Pages/ticket.md index 58d78058..b1539d31 100644 --- a/docs/wiki/Pages/ticket.md +++ b/docs/wiki/Pages/ticket.md @@ -9,7 +9,7 @@ This page is to provide a basic overview about the "Calendar" feature located in | Type | Name | Description | |------|------|-------------| -| Future | renderQRCode() | Returns an Image object, containing the resized QR code loaded from storage. +| Future | renderAztecCode() | Returns an Image object, containing the resized Aztec code loaded from storage. | Future?> | getTicketDetails() | Returns a map, containing the ticket details such as the name of the owner, the birthdate and validity information. --- @@ -19,11 +19,9 @@ This page is to provide a basic overview about the "Calendar" feature located in | Type | Name | Description | |------|------|-------------| | Future | loadTicket() | Calls the ticket datasource to load the remote ticket and then saves it to storage or deletes the exisiting one if it expired or was removed. -| Future | getQRCodeFile() | Returns a File object of the saved QR code -| Future | getTicketDetailsFile() | Returns a File object of the saved ticket details -| Future | qrCodeFileExists() | Checks whether a QR code was saved to storage -| Future | ticketDetailsFileExists() | Checks whether ticket details were saved to storage -| Future | saveTicket(Map ticket) | Saves the QR Code and ticket details to two separate files using the passed Map +| Future | getAztecCode() | Returns the Aztec code as a Base64 String +| Future | getTicketDetails() | Returns the whole ticket map as JSON +| Future | saveTicket(Map ticket) | Saves the Aztec Code and ticket details to two separate files using the passed Map | Future | deleteTicket() | Deletes the ticket from storage --- @@ -32,4 +30,4 @@ This page is to provide a basic overview about the "Calendar" feature located in | Type | Name | Description | |------|------|-------------| -| Future> | getRemoteTicket() | Loads the remote ticket using a headless webview which clicks through the RUB login process and then extracts the QR code and ticket details from the RIDE website. +| Future> | getRemoteTicket() | Loads the remote ticket using a headless webview which clicks through the RUB login process and then extracts the Aztec code and ticket details from the RIDE website. diff --git a/lib/core/injection.dart b/lib/core/injection.dart index bbb059ad..3240dbaf 100644 --- a/lib/core/injection.dart +++ b/lib/core/injection.dart @@ -88,7 +88,7 @@ Future init() async { ); sl.registerLazySingleton( - () => TicketRepository(ticketDataSource: sl()), + () => TicketRepository(ticketDataSource: sl(), secureStorage: sl()), ); //! diff --git a/lib/pages/wallet/ticket/ticket_datasource.dart b/lib/pages/wallet/ticket/ticket_datasource.dart index cd6763ac..493f3eca 100644 --- a/lib/pages/wallet/ticket/ticket_datasource.dart +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -20,7 +20,7 @@ class TicketDataSource { // Define empty ticket final Map ticket = { - 'barcode': '', + 'aztec_code': '', 'valid_from': '', 'valid_till': '', 'validity_region': '', @@ -52,7 +52,7 @@ class TicketDataSource { final List arguments = List.of(args)[1]; final String image = List.from(args)[0].toString().split(',')[1]; - ticket['barcode'] = image; + ticket['aztec_code'] = image; if (arguments.length == 4) { ticket['valid_from'] = arguments[0]; diff --git a/lib/pages/wallet/ticket/ticket_repository.dart b/lib/pages/wallet/ticket/ticket_repository.dart index d7c0f97e..c438bdc0 100644 --- a/lib/pages/wallet/ticket/ticket_repository.dart +++ b/lib/pages/wallet/ticket/ticket_repository.dart @@ -1,16 +1,17 @@ import 'dart:convert'; -import 'dart:io'; -import 'package:path_provider/path_provider.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:campus_app/core/exceptions.dart'; import 'package:campus_app/pages/wallet/ticket/ticket_datasource.dart'; class TicketRepository { final TicketDataSource ticketDataSource; + final FlutterSecureStorage secureStorage; TicketRepository({ required this.ticketDataSource, + required this.secureStorage, }); /// Load the semester ticket @@ -29,88 +30,36 @@ class TicketRepository { throw TicketNotFoundException(); } } - if (ticket == null || ticket['barcode'].toString().isEmpty) { + if (ticket == null || ticket['aztec_code'].toString().isEmpty) { throw TicketNotFoundException(); } await saveTicket(ticket); } - /// Load the qr code file from storage - Future getQRCodeFile() async { - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; + /// Loads the Aztec code from secure storage + Future getAztecCode() async { + final String? aztecCode = await secureStorage.read(key: 'ticket_aztec_code'); - // Define the image file - final File qrCodeFile = File('$directoryPath/ticket.png'); - - return qrCodeFile; - } - - /// Load the ticket details file from storage - Future getTicketDetailsFile() async { - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; - - // Define the ticket details file - final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - - return ticketDetailsFile; + return aztecCode; } - /// Checks whether the qr code file exists - Future qrCodeFileExists() async { - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; + /// Loads the ticket details from secure storage + Future getTicketDetails() async { + final String? ticketDetails = await secureStorage.read(key: 'ticket_details'); - // Define the image file - final File qrCodeFile = File('$directoryPath/ticket.png'); - - final bool exists = qrCodeFile.existsSync(); - - return exists; + return ticketDetails; } - /// Checks whether the ticket details file exists - Future ticketDetailsFileExists() async { - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; - - final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - - final bool exists = ticketDetailsFile.existsSync(); - - return exists; - } - - /// Save both the QR code and it's details to storage + /// Save both the Aztec code and it's details to storage Future saveTicket(Map ticket) async { - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; - - // Save the given png file to the app directory - final File qrCodeFile = File('$directoryPath/ticket.png'); - final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - - await qrCodeFile.writeAsBytes(base64Decode(ticket['barcode'])); - await ticketDetailsFile.writeAsString(jsonEncode(ticket)); + await secureStorage.write(key: 'ticket_aztec_code', value: ticket['aztec_code']); + await secureStorage.write(key: 'ticket_details', value: jsonEncode(ticket)); } - /// Delete the ticket files + /// Delete the stored ticket Future deleteTicket() async { - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; - - // Define the image file - final File qrCodeFile = File('$directoryPath/ticket.png'); - final File ticketDetailsFile = File('$directoryPath/ticket_details.json'); - - if (await qrCodeFileExists()) { - await qrCodeFile.delete(); - } - - if (await ticketDetailsFileExists()) { - await ticketDetailsFile.delete(); - } + await secureStorage.delete(key: 'ticket_aztec_code'); + await secureStorage.delete(key: 'ticket_details'); } } diff --git a/lib/pages/wallet/ticket/ticket_usecases.dart b/lib/pages/wallet/ticket/ticket_usecases.dart index b365dd79..54ea0e27 100644 --- a/lib/pages/wallet/ticket/ticket_usecases.dart +++ b/lib/pages/wallet/ticket/ticket_usecases.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -14,13 +13,13 @@ class TicketUsecases { required this.ticketRepository, }); - /// Render the QR code and resize it - Future renderQRCode() async { - if (await ticketRepository.qrCodeFileExists() == false) return null; + /// Render the Aztec code and resize it + Future renderAztecCode() async { + final String? aztecCode = await ticketRepository.getAztecCode(); - final File ticketFile = await ticketRepository.getQRCodeFile(); + if (aztecCode == null) return null; - Uint8List resizedData = ticketFile.readAsBytesSync(); + Uint8List resizedData = base64Decode(aztecCode); final img.Image image = img.decodeImage(resizedData)!; final img.Image resized = img.copyResize(image, width: 200, height: 200); resizedData = img.encodePng(resized); @@ -32,14 +31,14 @@ class TicketUsecases { /// Parse the content of the ticket details file Future?> getTicketDetails() async { - if (await ticketRepository.ticketDetailsFileExists() == false) return null; + final String? ticketDetailsEncoded = await ticketRepository.getTicketDetails(); - final File ticketDetailsFile = await ticketRepository.getTicketDetailsFile(); + if (ticketDetailsEncoded == null) return null; Map? ticketDetails; try { - ticketDetails = jsonDecode(await ticketDetailsFile.readAsString()); + ticketDetails = jsonDecode(ticketDetailsEncoded); } catch (e) { return null; } diff --git a/lib/pages/wallet/ticket_fullscreen.dart b/lib/pages/wallet/ticket_fullscreen.dart index e7fdd2e1..7f830f86 100644 --- a/lib/pages/wallet/ticket_fullscreen.dart +++ b/lib/pages/wallet/ticket_fullscreen.dart @@ -19,18 +19,18 @@ class BogestraTicketFullScreen extends StatefulWidget { } class _BogestraTicketFullScreenState extends State { - Image? qrCodeImage; + Image? aztecCodeImage; TicketUsecases ticketUsecases = sl(); /// Loads the previously saved image of the semester ticket and /// the corresponding aztec-code Future renderTicket() async { - final Image? qrCodeImage = await ticketUsecases.renderQRCode(); + final Image? aztecCodeImage = await ticketUsecases.renderAztecCode(); - if (qrCodeImage != null) { + if (aztecCodeImage != null) { setState(() { - this.qrCodeImage = qrCodeImage; + this.aztecCodeImage = aztecCodeImage; }); } } @@ -89,7 +89,7 @@ class _BogestraTicketFullScreenState extends State { child: Padding( padding: EdgeInsets.only(bottom: Platform.isIOS ? 88 : 68), child: Center( - child: qrCodeImage ?? Container(), + child: aztecCodeImage ?? Container(), ), ), ), diff --git a/lib/pages/wallet/widgets/wallet.dart b/lib/pages/wallet/widgets/wallet.dart index 172f5329..a8c641db 100644 --- a/lib/pages/wallet/widgets/wallet.dart +++ b/lib/pages/wallet/widgets/wallet.dart @@ -53,23 +53,23 @@ class _BogestraTicketState extends State with AutomaticKeepAlive bool scanned = false; String scannedValue = ''; - late Image qrCodeImage; + late Image aztecCodeImage; late Map ticketDetails; - bool showQrCode = false; + bool showAztecCode = false; TicketRepository ticketRepository = sl(); TicketUsecases ticketUsecases = sl(); /// Loads the previously saved image of the semester ticket and the corresponding ticket details Future renderTicket() async { - final Image? qrCodeImage = await ticketUsecases.renderQRCode(); + final Image? aztecCodeImage = await ticketUsecases.renderAztecCode(); final Map? ticketDetails = await ticketUsecases.getTicketDetails(); - if (qrCodeImage != null && ticketDetails != null) { + if (aztecCodeImage != null && ticketDetails != null) { setState(() { scanned = true; - this.qrCodeImage = qrCodeImage; + this.aztecCodeImage = aztecCodeImage; this.ticketDetails = ticketDetails; }); } @@ -123,8 +123,8 @@ class _BogestraTicketState extends State with AutomaticKeepAlive ), ); } else { - setState(() => showQrCode = !showQrCode); - if (showQrCode) { + setState(() => showAztecCode = !showAztecCode); + if (showAztecCode) { setBrightness(1); } else { resetBrightness(); @@ -132,8 +132,8 @@ class _BogestraTicketState extends State with AutomaticKeepAlive } }, onLongPress: addTicket, - child: showQrCode - ? qrCodeImage + child: showAztecCode + ? aztecCodeImage : Column( children: [ Padding( @@ -152,14 +152,14 @@ class _BogestraTicketState extends State with AutomaticKeepAlive child: SizedBox( width: 130, height: 130, - child: qrCodeImage, + child: aztecCodeImage, ), ), const Expanded(child: SizedBox()), Padding( padding: const EdgeInsets.only(right: 10, left: 5), child: SizedBox( - width: 200, + width: 180, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 83371c7fc0c6cc42c79f6279f76a7a92501bb0d4 Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Sun, 7 Apr 2024 20:54:09 +0200 Subject: [PATCH 14/16] Update ticket removal functionality --- lib/pages/wallet/ticket/ticket_datasource.dart | 11 +++++++++++ lib/pages/wallet/ticket/ticket_repository.dart | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/pages/wallet/ticket/ticket_datasource.dart b/lib/pages/wallet/ticket/ticket_datasource.dart index 493f3eca..ec0ef238 100644 --- a/lib/pages/wallet/ticket/ticket_datasource.dart +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -153,6 +153,17 @@ class TicketDataSource { loginTimer!.cancel(); } if (headlessWebView != null && headlessWebView.isRunning()) { + if (headlessWebView.webViewController!.getUrl().toString().startsWith('https://abo.ride-ticketing.de')) { + await headlessWebView.webViewController!.evaluateJavascript( + source: ''' + const cardWrappers = document.getElementsByClassName("abo-card-wrapper"); + if(cardWrappers.length == 0) { + window.flutter_inappwebview.callHandler('error', "Ticket removed."); + return; + } + ''', + ); + } completer.completeError('Could not open ticket page.'); await headlessWebView.dispose(); } diff --git a/lib/pages/wallet/ticket/ticket_repository.dart b/lib/pages/wallet/ticket/ticket_repository.dart index c438bdc0..8528f7e1 100644 --- a/lib/pages/wallet/ticket/ticket_repository.dart +++ b/lib/pages/wallet/ticket/ticket_repository.dart @@ -25,9 +25,11 @@ class TicketRepository { throw MissingCredentialsException(); } else if (e == 'Invalid credentials.') { throw InvalidLoginIDAndPasswordException(); - } else if (e == 'Could not open ticket page.') { + } else if (e == 'Ticket removed.') { await deleteTicket(); throw TicketNotFoundException(); + } else if (e == 'Could not open ticket page.') { + throw TicketNotFoundException(); } } if (ticket == null || ticket['aztec_code'].toString().isEmpty) { From 894ca6b380a2c6cfa5d3415ccffad7e0594a4321 Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Sun, 7 Apr 2024 20:56:10 +0200 Subject: [PATCH 15/16] Change color of RUB logo depending the chosen theme --- lib/pages/wallet/ticket_login_screen.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/wallet/ticket_login_screen.dart b/lib/pages/wallet/ticket_login_screen.dart index c46e9e8b..330bb027 100644 --- a/lib/pages/wallet/ticket_login_screen.dart +++ b/lib/pages/wallet/ticket_login_screen.dart @@ -66,7 +66,9 @@ class _TicketLoginScreenState extends State { children: [ Image.asset( 'assets/img/icons/rub-link.png', - color: const Color.fromRGBO(0, 53, 96, 1), + color: Provider.of(context).currentTheme == AppThemes.light + ? const Color.fromRGBO(0, 53, 96, 1) + : Colors.white, width: 80, filterQuality: FilterQuality.high, ), From 1eab2eaeeeba760b40cb2ac101a72e5df8a7e2f1 Mon Sep 17 00:00:00 2001 From: Henry Herrmann Date: Sun, 7 Apr 2024 20:59:17 +0200 Subject: [PATCH 16/16] Change version number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 29db758d..7641bb33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: campus_app description: Simplifie, improve and facilitate everyday students life. publish_to: 'none' -version: 2.2.2 +version: 2.3.0 environment: sdk: ">=3.2.0 <4.0.0"