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/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/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/docs/wiki/Pages/ticket.md b/docs/wiki/Pages/ticket.md new file mode 100644 index 00000000..b1539d31 --- /dev/null +++ b/docs/wiki/Pages/ticket.md @@ -0,0 +1,33 @@ +# 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 | 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. + +--- + +## 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 | 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 + +--- + +## 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 Aztec code and ticket details from the RIDE website. diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a7895a19..f58b3345 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 = ""; @@ -193,6 +196,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 + + + + + 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 5789b503..3240dbaf 100644 --- a/lib/core/injection.dart +++ b/lib/core/injection.dart @@ -1,10 +1,7 @@ -import 'dart:io'; - import 'package:appwrite/appwrite.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:dio/io.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.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'; @@ -60,14 +58,20 @@ Future init() async { ); sl.registerLazySingleton(() { - final Client client = Client().setEndpoint(appwrite).setProject('campus_app'); - return BackendRepository(client: client); + return TicketDataSource( + secureStorage: sl(), + ); }); //! //! Repositories //! + sl.registerLazySingleton(() { + final Client client = Client().setEndpoint(appwrite).setProject('campus_app'); + return BackendRepository(client: client); + }); + sl.registerSingletonWithDependencies( () => NewsRepository(newsDatasource: sl()), dependsOn: [NewsDatasource], @@ -83,6 +87,10 @@ Future init() async { dependsOn: [MensaDataSource], ); + sl.registerLazySingleton( + () => TicketRepository(ticketDataSource: sl(), secureStorage: sl()), + ); + //! //! Usecases //! @@ -102,6 +110,10 @@ Future init() async { dependsOn: [MensaRepository], ); + sl.registerLazySingleton( + () => TicketUsecases(ticketRepository: sl()), + ); + //! //! Utils //! @@ -116,17 +128,21 @@ Future init() async { sl.registerLazySingleton(CalendarUtils.new); sl.registerLazySingleton(FeedUtils.new); sl.registerLazySingleton(MensaUtils.new); - sl.registerLazySingleton(MainUtils.new); + sl.registerLazySingleton(WalletUtils.new); //! //! External //! //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/env/env.g.dart b/lib/env/env.g.dart index 994346aa..42c12a57 100644 --- a/lib/env/env.g.dart +++ b/lib/env/env.g.dart @@ -6,374 +6,516 @@ 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, + + static final String firebaseAndroidApiKey = String.fromCharCodes(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, + + static final String firebaseIosApiKey = String.fromCharCodes(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, + + static final String appwriteCreateUserKey = String.fromCharCodes(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/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/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/more/privacy_policy_page.dart b/lib/pages/more/privacy_policy_page.dart index 6400be11..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
@@ -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 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.
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 new file mode 100644 index 00000000..ec0ef238 --- /dev/null +++ b/lib/pages/wallet/ticket/ticket_datasource.dart @@ -0,0 +1,177 @@ +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 { + final FlutterSecureStorage secureStorage; + + TicketDataSource({ + required this.secureStorage, + }); + + Future> getRemoteTicket() async { + debugPrint('Loading semester ticket'); + + final Completer> completer = Completer>(); + + // Define empty ticket + final Map ticket = { + 'aztec_code': '', + 'valid_from': '', + 'valid_till': '', + 'validity_region': '', + 'owner': '', + '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: 'ticket', + callback: (args) { + 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)[1]; + final String image = List.from(args)[0].toString().split(',')[1]; + + ticket['aztec_code'] = image; + + 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(); + }, + ); + + // 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, 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: """ + document.getElementById('username').value="$loginId"; + document.getElementById('password').value="$password"; + setTimeout(function(){ + document.getElementById('shibbutton').click(); + }, 100); + """, + ); + }); + } 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; + + 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( + source: ''' + const ticketClickInterval = setInterval(function(){ + document.getElementsByClassName("abo-card-wrapper")[0].click(); + }, 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); + ''', + ); + } + }, + ); + + await headlessWebView.run(); + + // 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()) { + 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(); + } + }); + } 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 new file mode 100644 index 00000000..8528f7e1 --- /dev/null +++ b/lib/pages/wallet/ticket/ticket_repository.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +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 + Future loadTicket() async { + Map? ticket; + + try { + ticket = await ticketDataSource.getRemoteTicket(); + } catch (e) { + if (e == 'No login credentials found.') { + throw MissingCredentialsException(); + } else if (e == 'Invalid credentials.') { + throw InvalidLoginIDAndPasswordException(); + } 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) { + throw TicketNotFoundException(); + } + + await saveTicket(ticket); + } + + /// Loads the Aztec code from secure storage + Future getAztecCode() async { + final String? aztecCode = await secureStorage.read(key: 'ticket_aztec_code'); + + return aztecCode; + } + + /// Loads the ticket details from secure storage + Future getTicketDetails() async { + final String? ticketDetails = await secureStorage.read(key: 'ticket_details'); + + return ticketDetails; + } + + /// Save both the Aztec code and it's details to storage + Future saveTicket(Map ticket) async { + await secureStorage.write(key: 'ticket_aztec_code', value: ticket['aztec_code']); + await secureStorage.write(key: 'ticket_details', value: jsonEncode(ticket)); + } + + /// Delete the stored ticket + Future deleteTicket() async { + 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 new file mode 100644 index 00000000..54ea0e27 --- /dev/null +++ b/lib/pages/wallet/ticket/ticket_usecases.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; +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, + }); + + /// Render the Aztec code and resize it + Future renderAztecCode() async { + final String? aztecCode = await ticketRepository.getAztecCode(); + + if (aztecCode == null) return null; + + 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); + + return Image( + image: MemoryImage(resizedData), + ); + } + + /// Parse the content of the ticket details file + Future?> getTicketDetails() async { + final String? ticketDetailsEncoded = await ticketRepository.getTicketDetails(); + + if (ticketDetailsEncoded == null) return null; + + Map? ticketDetails; + + try { + ticketDetails = jsonDecode(ticketDetailsEncoded); + } catch (e) { + return null; + } + + if (ticketDetails == null || ticketDetails['owner'] == null) { + return null; + } + + return ticketDetails; + } +} diff --git a/lib/pages/wallet/ticket_fullscreen.dart b/lib/pages/wallet/ticket_fullscreen.dart index fa05d4b2..7f830f86 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'; @@ -21,47 +19,19 @@ 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]))); - } + Image? aztecCodeImage; - 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? aztecCodeImage = await ticketUsecases.renderAztecCode(); + if (aztecCodeImage != null) { setState(() { - this.qrCodeImage = qrCodeImage; + this.aztecCodeImage = aztecCodeImage; }); - } else { - // ignore: use_build_context_synchronously - Navigator.pop(context); } } @@ -71,7 +41,7 @@ class _BogestraTicketFullScreenState extends State { setBrightness(1); - loadTicket(); + renderTicket(); } @override @@ -119,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/ticket_login_screen.dart b/lib/pages/wallet/ticket_login_screen.dart new file mode 100644 index 00000000..330bb027 --- /dev/null +++ b/lib/pages/wallet/ticket_login_screen.dart @@ -0,0 +1,228 @@ +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/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'; + +class TicketLoginScreen extends StatefulWidget { + final void Function() onTicketLoaded; + const TicketLoginScreen({super.key, required this.onTicketLoaded}); + + @override + State createState() => _TicketLoginScreenState(); +} + +class _TicketLoginScreenState extends State { + final TicketRepository ticketRepository = sl(); + final FlutterSecureStorage secureStorage = sl(); + final WalletUtils walletUtils = 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( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/img/icons/rub-link.png', + color: Provider.of(context).currentTheme == AppThemes.light + ? const Color.fromRGBO(0, 53, 96, 1) + : Colors.white, + width: 80, + filterQuality: FilterQuality.high, + ), + const Padding(padding: EdgeInsets.only(top: 30)), + 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 { + final NavigatorState navigator = Navigator.of(context); + + if (usernameController.text.isEmpty || passwordController.text.isEmpty) { + setState(() { + errorMessage = 'Bitte fülle beide Felder aus!'; + showErrorMessage = true; + }); + return; + } + + if (await walletUtils.hasNetwork() == false) { + setState(() { + errorMessage = 'Überprüfe deine Internetverbindung!'; + showErrorMessage = true; + }); + return; + } + + setState(() { + showErrorMessage = false; + 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) { + 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(() { + loading = false; + }); + }, + ), + const Padding(padding: EdgeInsets.only(top: 25)), + 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: 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), + ), + overflow: TextOverflow.clip, + ), + ), + ], + ), + const Padding(padding: EdgeInsets.only(top: 25)), + 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 36af6de4..a8c641db 100644 --- a/lib/pages/wallet/widgets/wallet.dart +++ b/lib/pages/wallet/widgets/wallet.dart @@ -1,21 +1,18 @@ 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/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'; @@ -24,16 +21,22 @@ 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(), + ), ], ); } @@ -50,137 +53,39 @@ class _BogestraTicketState extends State with AutomaticKeepAlive bool scanned = false; String scannedValue = ''; - late Image semesterTicketImage; - late Image qrCodeImage; - - bool showQrCode = false; + late Image aztecCodeImage; + late Map ticketDetails; - /// Loads the previously saved image of the semester ticket and - /// the corresponding aztec-code - Future loadTicket() async { - debugPrint('Loading semester ticket'); + bool showAztecCode = false; - final Directory saveDirectory = await getApplicationDocumentsDirectory(); - final String directoryPath = saveDirectory.path; + TicketRepository ticketRepository = sl(); + TicketUsecases ticketUsecases = sl(); - // 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 semesterTicketImage = await renderSemesterTicket(ticketFile.path); - final Image qrCodeImage = await renderQRCode(ticketFile.path); + /// Loads the previously saved image of the semester ticket and the corresponding ticket details + Future renderTicket() async { + final Image? aztecCodeImage = await ticketUsecases.renderAztecCode(); + final Map? ticketDetails = await ticketUsecases.getTicketDetails(); + if (aztecCodeImage != null && ticketDetails != null) { setState(() { scanned = true; - this.semesterTicketImage = semesterTicketImage; - this.qrCodeImage = qrCodeImage; + this.aztecCodeImage = aztecCodeImage; + this.ticketDetails = ticketDetails; }); } } - /// Saves a loaded semester ticket and its corresponding aztec-code - Future saveTicketPDF(File ticketPdf) 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'); - } - - 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(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)); - } - 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 - } + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TicketLoginScreen( + onTicketLoaded: () async { + await renderTicket(); + }, + ), + ), + ); } @override @@ -190,7 +95,10 @@ class _BogestraTicketState extends State with AutomaticKeepAlive void initState() { super.initState(); - loadTicket(); + ticketRepository.loadTicket().catchError((error) { + debugPrint('Wallet widget: $error'); + }); + renderTicket(); } @override @@ -215,8 +123,8 @@ class _BogestraTicketState extends State with AutomaticKeepAlive ), ); } else { - setState(() => showQrCode = !showQrCode); - if (showQrCode) { + setState(() => showAztecCode = !showAztecCode); + if (showAztecCode) { setBrightness(1); } else { resetBrightness(); @@ -224,7 +132,94 @@ class _BogestraTicketState extends State with AutomaticKeepAlive } }, onLongPress: addTicket, - child: showQrCode ? qrCodeImage : semesterTicketImage, + child: showAztecCode + ? aztecCodeImage + : 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: aztecCodeImage, + ), + ), + const Expanded(child: SizedBox()), + Padding( + padding: const EdgeInsets.only(right: 10, left: 5), + child: SizedBox( + width: 180, + 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( + '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 89cd92f9..301f42ae 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,13 @@ 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://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; final String firebaseAndroidApiKey = Env.firebaseAndroidApiKey; 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/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 4c0ec6d1..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; } @@ -76,22 +83,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), ), diff --git a/pubspec.lock b/pubspec.lock index 2c6d76fd..a4ebb9f8 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: @@ -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" \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index b85b5d6e..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" @@ -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: @@ -125,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/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