diff --git a/refilc/lib/api/login.dart b/refilc/lib/api/login.dart index 8f44bce..3895afc 100644 --- a/refilc/lib/api/login.dart +++ b/refilc/lib/api/login.dart @@ -68,6 +68,8 @@ Future loginAPI({ gradeDelay: 0, ), role: Role.parent, + accessToken: '', + accessTokenExpire: DateTime.now(), refreshToken: '', ); @@ -154,6 +156,8 @@ Future loginAPI({ name: student.name, student: student, role: JwtUtils.getRoleFromJWT(res["access_token"])!, + accessToken: res["access_token"], + accessTokenExpire: DateTime.now(), refreshToken: '', ); @@ -235,6 +239,15 @@ Future newLoginAPI({ if (res != null) { if (kDebugMode) { print(res); + + // const splitSize = 1000; + // RegExp exp = RegExp(r"\w{" "$splitSize" "}"); + // // String str = "0102031522"; + // Iterable matches = exp.allMatches(res.toString()); + // var list = matches.map((m) => m.group(0)); + // list.forEach((e) { + // print(e); + // }); } if (res.containsKey("error")) { @@ -267,6 +280,9 @@ Future newLoginAPI({ name: student.name, student: student, role: role, + accessToken: res["access_token"], + accessTokenExpire: + DateTime.now().add(Duration(seconds: (res["expires_in"] - 30))), refreshToken: res["refresh_token"], ); diff --git a/refilc/lib/api/providers/sync.dart b/refilc/lib/api/providers/sync.dart index 5e44466..cb8c474 100644 --- a/refilc/lib/api/providers/sync.dart +++ b/refilc/lib/api/providers/sync.dart @@ -40,6 +40,12 @@ Future syncAll(BuildContext context) { StatusProvider statusProvider = Provider.of(context, listen: false); + // check if access token isn't expired + // if (user.user?.accessToken == null) { + // lock = false; + // return Future.value(); + // } + List> tasks = []; int taski = 0; @@ -50,6 +56,25 @@ Future syncAll(BuildContext context) { } tasks = [ + // refresh login + syncStatus(() async { + print(user.user?.accessTokenExpire); + + if (user.user == null) return; + if (user.user!.accessTokenExpire.isBefore(DateTime.now())) { + String authRes = await Provider.of(context, listen: false) + .refreshLogin() ?? + ''; + if (authRes != 'success') { + print('ERROR: failed to refresh login'); + lock = false; + return Future.value(); + } + } else { + print('INFO: access token is not expired'); + } + }()), + syncStatus(Provider.of(context, listen: false).fetch()), syncStatus(Provider.of(context, listen: false) .fetch(week: Week.current())), diff --git a/refilc/lib/database/init.dart b/refilc/lib/database/init.dart index f6a744a..2850814 100644 --- a/refilc/lib/database/init.dart +++ b/refilc/lib/database/init.dart @@ -67,6 +67,7 @@ const usersDB = DatabaseStruct("users", { "institute_code": String, "student": String, "role": int, "nickname": String, "picture": String, // premium only (it's now plus btw) "grade_streak": int, + "access_token": String, "access_token_expire": String, "refresh_token": String, }); const userDataDB = DatabaseStruct("user_data", { @@ -141,6 +142,8 @@ Future initDB(DatabaseProvider database) async { "nickname": "", "picture": "", "grade_streak": 0, + "access_token": "", + "access_token_expire": "", "refresh_token": "", }, ); diff --git a/refilc/lib/models/user.dart b/refilc/lib/models/user.dart index 0f4ae6c..eab3624 100644 --- a/refilc/lib/models/user.dart +++ b/refilc/lib/models/user.dart @@ -18,6 +18,8 @@ class User { String picture; int gradeStreak; // new login method + String accessToken; + DateTime accessTokenExpire; String refreshToken; String get displayName => nickname != '' ? nickname : name; @@ -34,6 +36,8 @@ class User { this.nickname = "", this.picture = "", this.gradeStreak = 0, + required this.accessToken, + required this.accessTokenExpire, required this.refreshToken, }) { if (id != null) { @@ -65,6 +69,9 @@ class User { nickname: map["nickname"] ?? "", picture: map["picture"] ?? "", gradeStreak: map["grade_streak"] ?? 0, + accessToken: map["access_token"] ?? "", + accessTokenExpire: DateTime.parse( + map["access_token_expire"] ?? DateTime.now().toIso8601String()), refreshToken: map["refresh_token"] ?? "", ); } @@ -81,6 +88,8 @@ class User { "nickname": nickname, "picture": picture, "grade_streak": gradeStreak, + "access_token": accessToken, + "access_token_expire": accessTokenExpire.toIso8601String(), "refresh_token": refreshToken, }; } diff --git a/refilc/pubspec.yaml b/refilc/pubspec.yaml index 9f81ba2..fd4a2b7 100644 --- a/refilc/pubspec.yaml +++ b/refilc/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: # ref: master path_provider: ^2.0.2 permission_handler: ^11.0.1 - share_plus: ^9.0.0 + share_plus: ^10.0.3 connectivity_plus: ^6.0.3 flutter_displaymode: ^0.6.0 quick_actions: ^1.0.1 diff --git a/refilc_kreta_api/lib/client/client.dart b/refilc_kreta_api/lib/client/client.dart index 0f93cda..1cd5964 100644 --- a/refilc_kreta_api/lib/client/client.dart +++ b/refilc_kreta_api/lib/client/client.dart @@ -28,7 +28,7 @@ class KretaClient { late final DatabaseProvider _database; late final StatusProvider _status; - bool _loginRefreshing = false; + // bool _loginRefreshing = false; KretaClient({ this.accessToken, @@ -67,10 +67,14 @@ class KretaClient { headerMap = {}; } + if (accessToken == null || accessToken == '') { + accessToken = _user.user?.accessToken; + } + try { http.Response? res; - for (int i = 0; i < 3; i++) { + for (int i = 0; i < 2; i++) { if (autoHeader) { if (!headerMap.containsKey("authorization") && accessToken != null) { headerMap["authorization"] = "Bearer $accessToken"; @@ -86,13 +90,14 @@ class KretaClient { if (res.statusCode == 401) { headerMap.remove("authorization"); print("DEBUG: 401 error, refreshing login"); - await refreshLogin(); + print("DEBUG: 401 error, URL: $url"); + // await refreshLogin(); } else { break; } // Wait before retrying - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 1500)); } if (res == null) throw "Login error"; @@ -130,10 +135,14 @@ class KretaClient { headerMap = {}; } + if (accessToken == null || accessToken == '') { + accessToken = _user.user?.accessToken; + } + try { http.Response? res; - for (int i = 0; i < 3; i++) { + for (int i = 0; i < 2; i++) { if (autoHeader) { if (!headerMap.containsKey("authorization") && accessToken != null) { headerMap["authorization"] = "Bearer $accessToken"; @@ -151,11 +160,14 @@ class KretaClient { res = await client.post(Uri.parse(url), headers: headerMap, body: body); if (res.statusCode == 401) { - await refreshLogin(); + // await refreshLogin(); headerMap.remove("authorization"); } else { break; } + + // Wait before retrying + await Future.delayed(const Duration(milliseconds: 1500)); } if (res == null) throw "Login error"; @@ -188,6 +200,10 @@ class KretaClient { headerMap = {}; } + if (accessToken == null || accessToken == '') { + accessToken = _user.user?.accessToken; + } + try { http.StreamedResponse? res; @@ -218,7 +234,7 @@ class KretaClient { if (res.statusCode == 401) { headerMap.remove("authorization"); - await refreshLogin(); + // await refreshLogin(); } else { break; } @@ -238,8 +254,8 @@ class KretaClient { } Future refreshLogin() async { - if (_loginRefreshing) return null; - _loginRefreshing = true; + // if (_loginRefreshing) return null; + // _loginRefreshing = true; User? loginUser = _user.user; if (loginUser == null) return null; @@ -288,6 +304,11 @@ class KretaClient { if (res.containsKey("access_token")) { accessToken = res["access_token"]; + loginUser.accessToken = res["refresh_token"]; + loginUser.accessTokenExpire = + DateTime.now().add(Duration(seconds: (res["expires_in"] - 30))); + _database.store.storeUser(loginUser); + _user.refresh(); } if (res.containsKey("refresh_token")) { refreshToken = res["refresh_token"]; @@ -298,15 +319,20 @@ class KretaClient { if (res.containsKey("id_token")) { idToken = res["id_token"]; } - _loginRefreshing = false; + // _loginRefreshing = false; + print('successful refresh'); + + return 'success'; } else { - _loginRefreshing = false; + // _loginRefreshing = false; + return null; } } else { - _loginRefreshing = false; + // _loginRefreshing = false; + return null; } - return null; + // return null; } Future logout() async { diff --git a/refilc_mobile_ui/lib/screens/settings/settings_screen.dart b/refilc_mobile_ui/lib/screens/settings/settings_screen.dart index 8bf78c4..6868474 100644 --- a/refilc_mobile_ui/lib/screens/settings/settings_screen.dart +++ b/refilc_mobile_ui/lib/screens/settings/settings_screen.dart @@ -42,6 +42,7 @@ import 'package:refilc_mobile_ui/screens/settings/accounts/account_view.dart'; import 'package:refilc_mobile_ui/screens/settings/notifications_screen.dart'; import 'package:refilc_mobile_ui/screens/settings/privacy_view.dart'; import 'package:refilc_mobile_ui/screens/settings/settings_helper.dart'; +import 'package:refilc_mobile_ui/screens/settings/submenu/code_scanner.dart'; import 'package:refilc_mobile_ui/screens/settings/submenu/extras_screen.dart'; import 'package:refilc_mobile_ui/screens/settings/submenu/personalize_screen.dart'; import 'package:flutter/foundation.dart'; @@ -1012,14 +1013,15 @@ class SettingsScreenState extends State children: [ PanelButton( leading: Icon( - FeatherIcons.map, + Icons.qr_code, size: 22.0, color: AppColors.of(context).text.withOpacity(0.95), ), - title: Text("stickermap".i18n), - onPressed: () => launchUrl( - Uri.parse("https://stickermap.refilc.hu"), - mode: LaunchMode.inAppBrowserView, + title: Text("qr_scanner".i18n), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const CodeScannerScreen(), + ), ), borderRadius: const BorderRadius.vertical( top: Radius.circular(12.0), @@ -1034,6 +1036,22 @@ class SettingsScreenState extends State ), title: Text("news".i18n), onPressed: () => _openNews(context), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(4.0), + bottom: Radius.circular(4.0), + ), + ), + PanelButton( + leading: Icon( + FeatherIcons.map, + size: 22.0, + color: AppColors.of(context).text.withOpacity(0.95), + ), + title: Text("stickermap".i18n), + onPressed: () => launchUrl( + Uri.parse("https://stickermap.refilc.hu"), + mode: LaunchMode.inAppBrowserView, + ), borderRadius: const BorderRadius.vertical( top: Radius.circular(4.0), bottom: Radius.circular(12.0), diff --git a/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart b/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart index 2c4387c..26a9cf1 100644 --- a/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart +++ b/refilc_mobile_ui/lib/screens/settings/settings_screen.i18n.dart @@ -131,6 +131,7 @@ extension SettingsLocalization on String { "feedback": "Feedback", "other": "Other", "stickermap": "Sticker Map", + "qr_scanner": "QR Scanner", }, "hu_hu": { "heads_up": "Figyelem!", @@ -260,6 +261,7 @@ extension SettingsLocalization on String { "feedback": "Visszajelzés", "other": "Egyéb", "stickermap": "Matrica térkép", + "qr_scanner": "QR Kódolvasó", }, "de_de": { "heads_up": "Achtung!", @@ -389,6 +391,7 @@ extension SettingsLocalization on String { "feedback": "Feedback", "other": "Sonstiges", "stickermap": "Sticker Map", + "qr_scanner": "QR-Scanner", }, }; diff --git a/refilc_mobile_ui/lib/screens/settings/submenu/code_scanner.dart b/refilc_mobile_ui/lib/screens/settings/submenu/code_scanner.dart new file mode 100644 index 0000000..5aec597 --- /dev/null +++ b/refilc_mobile_ui/lib/screens/settings/submenu/code_scanner.dart @@ -0,0 +1,136 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; +import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; + +class CodeScannerScreen extends StatefulWidget { + const CodeScannerScreen({super.key}); + + @override + State createState() => _CodeScannerScreenState(); +} + +class _CodeScannerScreenState extends State { + Barcode? result; + QRViewController? controller; + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + + // In order to get hot reload to work we need to pause the camera if the platform + // is android, or resume the camera if the platform is iOS. + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } + controller!.resumeCamera(); + } + + // @override + // void initState() { + // super.initState(); + + // controller!.resumeCamera(); + // } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + title: Text('qr_scanner'.i18n), + leading: const BackButton(), + actions: [ + IconButton( + icon: FutureBuilder( + future: controller?.getFlashStatus(), + builder: (context, snapshot) { + return Icon( + snapshot.data == true + ? FeatherIcons.zapOff + : FeatherIcons.zap, + ); + }, + ), + onPressed: () async { + await controller?.toggleFlash(); + setState(() {}); + }, + ), + ], + ), + body: _buildQrView(context), + // body: Column( + // children: [ + // Expanded(flex: 4, child: _buildQrView(context)), + // // Expanded( + // // flex: 1, + // // child: FittedBox( + // // fit: BoxFit.contain, + // // child: Column( + // // mainAxisAlignment: MainAxisAlignment.spaceEvenly, + // // children: [ + // // if (result != null) + // // Text( + // // 'Barcode Type: ${describeEnum(result!.format)} Data: ${result!.code}') + // // else + // // const Text('Scan a code'), + // // ], + // // ), + // // ), + // // ) + // ], + // ), + ); + } + + Widget _buildQrView(BuildContext context) { + // For this example we check how width or tall the device is and change the scanArea and overlay accordingly. + var scanArea = (MediaQuery.of(context).size.width < 400 || + MediaQuery.of(context).size.height < 400) + ? 150.0 + : 280.0; + // To ensure the Scanner view is properly sizes after rotation + // we need to listen for Flutter SizeChanged notification and update controller + return QRView( + key: qrKey, + onQRViewCreated: _onQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Theme.of(context).primaryColor, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea, + ), + onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), + ); + } + + void _onQRViewCreated(QRViewController controller) { + setState(() { + this.controller = controller; + }); + controller.scannedDataStream.listen((scanData) { + setState(() { + result = scanData; + }); + }); + } + + void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) { + // log('${DateTime.now().toIso8601String()}_onPermissionSet $p'); + if (!p) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('no Permission')), + ); + } + } + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } +} diff --git a/refilc_mobile_ui/lib/screens/settings/submenu/share_theme_popup.dart b/refilc_mobile_ui/lib/screens/settings/submenu/share_theme_popup.dart index 9c6b50b..d6d8572 100644 --- a/refilc_mobile_ui/lib/screens/settings/submenu/share_theme_popup.dart +++ b/refilc_mobile_ui/lib/screens/settings/submenu/share_theme_popup.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; // import 'package:refilc/models/settings.dart'; -import 'package:refilc/models/shared_theme.dart'; import 'package:refilc/theme/colors/colors.dart'; import 'package:refilc_kreta_api/providers/share_provider.dart'; import 'package:refilc_mobile_ui/common/action_button.dart'; diff --git a/refilc_mobile_ui/pubspec.yaml b/refilc_mobile_ui/pubspec.yaml index 53f2629..7c050a9 100644 --- a/refilc_mobile_ui/pubspec.yaml +++ b/refilc_mobile_ui/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: auto_size_text: ^3.0.0 connectivity_plus: ^6.0.3 collection: ^1.18.0 - share_plus: ^9.0.0 + share_plus: ^10.0.3 image_picker: ^1.0.7 path_provider: ^2.1.2 image_crop: @@ -77,6 +77,7 @@ dependencies: webview_flutter: ^4.8.0 file_picker: ^8.0.5 shake_flutter: ^17.0.0 + qr_code_scanner_plus: ^2.0.6 dev_dependencies: flutter_lints: ^4.0.0 diff --git a/refilc_plus b/refilc_plus index 26cd3fc..6abc4ed 160000 --- a/refilc_plus +++ b/refilc_plus @@ -1 +1 @@ -Subproject commit 26cd3fc163d72ddb849edfeb7fdb7b64c7df44bc +Subproject commit 6abc4edf70deeaffea8b8a7dd95acebecc5a520b