diff --git a/refilc/assets/images/btn_kreten_login.png b/refilc/assets/images/btn_kreten_login.png new file mode 100644 index 0000000..0af9566 Binary files /dev/null and b/refilc/assets/images/btn_kreten_login.png differ diff --git a/refilc/lib/api/login.dart b/refilc/lib/api/login.dart index 0977ce8..1074dff 100644 --- a/refilc/lib/api/login.dart +++ b/refilc/lib/api/login.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid_print, use_build_context_synchronously +import 'package:flutter/foundation.dart'; import 'package:refilc/utils/jwt.dart'; import 'package:refilc_kreta_api/models/school.dart'; import 'package:refilc_kreta_api/providers/absence_provider.dart'; @@ -107,7 +108,9 @@ Future loginAPI({ default: // normal login from here Provider.of(context, listen: false).userAgent = - Provider.of(context, listen: false).config.userAgent; + Provider.of(context, listen: false) + .config + .userAgent; Map headers = { "content-type": "application/x-www-form-urlencoded", @@ -157,7 +160,8 @@ Future loginAPI({ .store .storeUser(user); Provider.of(context, listen: false).addUser(user); - Provider.of(context, listen: false).setUser(user.id); + Provider.of(context, listen: false) + .setUser(user.id); // Get user data try { @@ -167,7 +171,8 @@ Future loginAPI({ .fetch(week: Week.current()), Provider.of(context, listen: false).fetch(), Provider.of(context, listen: false).fetch(), - Provider.of(context, listen: false).fetchAll(), + Provider.of(context, listen: false) + .fetchAll(), Provider.of(context, listen: false) .fetchAllRecipients(), Provider.of(context, listen: false).fetch(), @@ -195,3 +200,109 @@ Future loginAPI({ return LoginState.failed; } + +// new login api +Future newLoginAPI({ + required String code, + required BuildContext context, + void Function(User)? onLogin, + void Function()? onSuccess, +}) async { + // actual login (token grant) logic + Map headers = { + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "accept": "*/*", + "user-agent": "eKretaStudent/264745 CFNetwork/1494.0.7 Darwin/23.4.0", + }; + + Map? res = await Provider.of(context, listen: false) + .postAPI(KretaAPI.login, autoHeader: false, headers: headers, body: { + "code": code, + "code_verifier": "DSpuqj_HhDX4wzQIbtn8lr8NLE5wEi1iVLMtMK0jY6c", + "redirect_uri": + "https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect", + "client_id": "kreta-ellenorzo-student-mobile-ios", + "grant_type": "authorization_code", + }); + + if (res != null) { + if (kDebugMode) { + print(res); + } + + if (res.containsKey("error")) { + if (res["error"] == "invalid_grant") { + print("ERROR: invalid_grant"); + return; + } + } else { + // print("MUKODIK GECI"); + // print("ACCESS TOKEN: ${res["access_token"]}"); + if (res.containsKey("access_token")) { + print(JwtUtils.decodeJwt(res["access_token"])); + try { + Provider.of(context, listen: false).accessToken = + res["access_token"]; + + String instituteCode = + JwtUtils.getInstituteFromJWT(res["access_token"])!; + String username = JwtUtils.getUsernameFromJWT(res["access_token"])!; + Role role = JwtUtils.getRoleFromJWT(res["access_token"])!; + + Map? studentJson = + await Provider.of(context, listen: false) + .getAPI(KretaAPI.student(instituteCode)); + Student student = Student.fromJson(studentJson!); + + var user = User( + username: username, + password: '', + instituteCode: instituteCode, + name: student.name, + student: student, + role: role, + ); + + if (onLogin != null) onLogin(user); + + // Store User in the database + await Provider.of(context, listen: false) + .store + .storeUser(user); + Provider.of(context, listen: false).addUser(user); + Provider.of(context, listen: false).setUser(user.id); + + // Get user data + try { + await Future.wait([ + Provider.of(context, listen: false).fetch(), + Provider.of(context, listen: false) + .fetch(week: Week.current()), + Provider.of(context, listen: false).fetch(), + Provider.of(context, listen: false).fetch(), + Provider.of(context, listen: false).fetchAll(), + Provider.of(context, listen: false) + .fetchAllRecipients(), + Provider.of(context, listen: false).fetch(), + Provider.of(context, listen: false).fetch(), + Provider.of(context, listen: false).fetch(), + ]); + } catch (error) { + print("WARNING: failed to fetch user data: $error"); + } + + if (onSuccess != null) onSuccess(); + + return LoginState.success; + } catch (error) { + print("ERROR: loginAPI: $error"); + // maybe check debug mode + // ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("ERROR: $error"))); + return LoginState.failed; + } + } + } + } + + return LoginState.failed; +} diff --git a/refilc/lib/app.dart b/refilc/lib/app.dart index 7bfb9c0..ef2207d 100644 --- a/refilc/lib/app.dart +++ b/refilc/lib/app.dart @@ -262,7 +262,7 @@ class App extends StatelessWidget { switch (route.name) { case "login_back": return CupertinoPageRoute( - builder: (context) => const mobile.LoginScreen()); + builder: (context) => const mobile.LoginScreen(back: true)); case "login": return _rootRoute(const mobile.LoginScreen()); case "navigation": diff --git a/refilc/lib/utils/jwt.dart b/refilc/lib/utils/jwt.dart index 2e0898b..d933693 100644 --- a/refilc/lib/utils/jwt.dart +++ b/refilc/lib/utils/jwt.dart @@ -39,4 +39,16 @@ class JwtUtils { } return null; } + + static String? getInstituteFromJWT(String jwt) { + var jwtData = decodeJwt(jwt); + + return jwtData?["kreta:institute_code"]; + } + + static String? getUsernameFromJWT(String jwt) { + var jwtData = decodeJwt(jwt); + + return jwtData?["kreta:user_name"]; + } } diff --git a/refilc_mobile_ui/lib/screens/login/kreten_login.dart b/refilc_mobile_ui/lib/screens/login/kreten_login.dart index d809af6..1b5cfd3 100644 --- a/refilc_mobile_ui/lib/screens/login/kreten_login.dart +++ b/refilc_mobile_ui/lib/screens/login/kreten_login.dart @@ -1,14 +1,13 @@ // ignore_for_file: use_build_context_synchronously -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:refilc_kreta_api/client/api.dart'; -import 'package:refilc_kreta_api/client/client.dart'; import 'package:webview_flutter/webview_flutter.dart'; class KretenLoginScreen extends StatefulWidget { - const KretenLoginScreen({super.key}); + const KretenLoginScreen({super.key, required this.onLogin}); + + // final String selectedSchool; + final void Function(String code) onLogin; @override State createState() => _KretenLoginScreenState(); @@ -31,6 +30,12 @@ class _KretenLoginScreenState extends State { currentUrl = url; }); + // final String instituteCode = widget.selectedSchool; + if (!url.startsWith( + 'https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect?code=')) { + return; + } + List requiredThings = url .replaceAll( 'https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect?code=', @@ -47,103 +52,11 @@ class _KretenLoginScreenState extends State { print(code); - // actual login (token grant) logic - Map headers = { - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", - "accept": "*/*", - "user-agent": - "eKretaStudent/264745 CFNetwork/1494.0.7 Darwin/23.4.0", - }; - - Map? res = await Provider.of(context, listen: false) - .postAPI(KretaAPI.login, - autoHeader: false, - headers: headers, - body: { - "code": code, - "code_verifier": "DSpuqj_HhDX4wzQIbtn8lr8NLE5wEi1iVLMtMK0jY6c", - "redirect_uri": - "https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect", - "client_id": "kreta-ellenorzo-student-mobile-ios", - "grant_type": "authorization_code", - }); - if (res != null) { - if (kDebugMode) { - print(res); - } - - if (res.containsKey("error")) { - if (res["error"] == "invalid_grant") { - print("ERROR: invalid_grant"); - return; - } - } else { - print("MUKODIK GECI"); - print("ACCESS TOKEN: ${res["access_token"]}"); - // if (res.containsKey("access_token")) { - // try { - // Provider.of(context, listen: false).accessToken = - // res["access_token"]; - // Map? studentJson = - // await Provider.of(context, listen: false) - // .getAPI(KretaAPI.student(instituteCode)); - // Student student = Student.fromJson(studentJson!); - // var user = User( - // username: username, - // password: password, - // instituteCode: instituteCode, - // name: student.name, - // student: student, - // role: JwtUtils.getRoleFromJWT(res["access_token"])!, - // ); - - // if (onLogin != null) onLogin(user); - - // // Store User in the database - // await Provider.of(context, listen: false) - // .store - // .storeUser(user); - // Provider.of(context, listen: false) - // .addUser(user); - // Provider.of(context, listen: false) - // .setUser(user.id); - - // // Get user data - // try { - // await Future.wait([ - // Provider.of(context, listen: false) - // .fetch(), - // Provider.of(context, listen: false) - // .fetch(week: Week.current()), - // Provider.of(context, listen: false).fetch(), - // Provider.of(context, listen: false) - // .fetch(), - // Provider.of(context, listen: false) - // .fetchAll(), - // Provider.of(context, listen: false) - // .fetchAllRecipients(), - // Provider.of(context, listen: false).fetch(), - // Provider.of(context, listen: false) - // .fetch(), - // Provider.of(context, listen: false) - // .fetch(), - // ]); - // } catch (error) { - // print("WARNING: failed to fetch user data: $error"); - // } - - // if (onSuccess != null) onSuccess(); - - // return LoginState.success; - // } catch (error) { - // print("ERROR: loginAPI: $error"); - // // maybe check debug mode - // // ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("ERROR: $error"))); - // return LoginState.failed; - // } - // } - } - } + widget.onLogin(code); + // Future.delayed(const Duration(milliseconds: 500), () { + // Navigator.of(context).pop(); + // }); + // Navigator.of(context).pop(); }, onProgress: (progress) { setState(() { @@ -158,7 +71,7 @@ class _KretenLoginScreenState extends State { )) ..loadRequest( Uri.parse( - 'https://idp.e-kreta.hu/connect/authorize?prompt=login&nonce=wylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU&response_type=code&code_challenge_method=S256&scope=openid%20email%20offline_access%20kreta-ellenorzo-webapi.public%20kreta-eugyintezes-webapi.public%20kreta-fileservice-webapi.public%20kreta-mobile-global-webapi.public%20kreta-dkt-webapi.public%20kreta-ier-webapi.public&code_challenge=HByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ&redirect_uri=https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect&client_id=kreta-ellenorzo-student-mobile-ios&state=refilc_student_mobile'), + 'https://idp.e-kreta.hu/connect/authorize?prompt=login&nonce=wylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU&response_type=code&code_challenge_method=S256&scope=openid%20email%20offline_access%20kreta-ellenorzo-webapi.public%20kreta-eugyintezes-webapi.public%20kreta-fileservice-webapi.public%20kreta-mobile-global-webapi.public%20kreta-dkt-webapi.public%20kreta-ier-webapi.public&code_challenge=HByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ&redirect_uri=https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect&client_id=kreta-ellenorzo-student-mobile-ios&state=refilc_student_mobile'), // &institute_code=${widget.selectedSchool} ); } diff --git a/refilc_mobile_ui/lib/screens/login/login_screen.dart b/refilc_mobile_ui/lib/screens/login/login_screen.dart index 123b046..0d8d976 100644 --- a/refilc_mobile_ui/lib/screens/login/login_screen.dart +++ b/refilc_mobile_ui/lib/screens/login/login_screen.dart @@ -1,13 +1,11 @@ // import 'dart:async'; -import 'package:refilc/api/client.dart'; +import 'package:flutter/widgets.dart'; import 'package:refilc/api/login.dart'; import 'package:refilc/theme/colors/colors.dart'; import 'package:refilc_mobile_ui/common/custom_snack_bar.dart'; import 'package:refilc_mobile_ui/common/system_chrome.dart'; import 'package:refilc_mobile_ui/screens/login/kreten_login.dart'; -import 'package:refilc_mobile_ui/screens/login/login_button.dart'; -import 'package:refilc_mobile_ui/screens/login/login_input.dart'; import 'package:refilc_mobile_ui/screens/login/school_input/school_input.dart'; import 'package:refilc_mobile_ui/screens/settings/privacy_view.dart'; import 'package:flutter/material.dart'; @@ -29,6 +27,9 @@ class LoginScreenState extends State { final schoolController = SchoolInputController(); final _scrollController = ScrollController(); + // new controllers + final codeController = TextEditingController(); + LoginState _loginState = LoginState.normal; bool showBack = false; @@ -58,20 +59,20 @@ class LoginScreenState extends State { systemNavigationBarIconBrightness: Brightness.dark, )); - FilcAPI.getSchools().then((schools) { - if (schools != null) { - schoolController.update(() { - schoolController.schools = schools; - }); - } else { - ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( - content: Text("schools_error".i18n, - style: const TextStyle(color: Colors.white)), - backgroundColor: AppColors.of(context).red, - context: context, - )); - } - }); + // FilcAPI.getSchools().then((schools) { + // if (schools != null) { + // schoolController.update(() { + // schoolController.schools = schools; + // }); + // } else { + // ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + // content: Text("schools_error".i18n, + // style: const TextStyle(color: Colors.white)), + // backgroundColor: AppColors.of(context).red, + // context: context, + // )); + // } + // }); } @override @@ -105,17 +106,6 @@ class LoginScreenState extends State { ), ), - TextButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const KretenLoginScreen(), - ), - ); - }, - child: const Text("login_w_kreten"), - ), - // app icon Padding( padding: EdgeInsets.zero, @@ -160,149 +150,218 @@ class LoginScreenState extends State { flex: 2, ), - // inputs - Padding( - padding: const EdgeInsets.only( - left: 22.0, - right: 22.0, - top: 0.0, - ), - child: AutofillGroup( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // username - Padding( - padding: const EdgeInsets.only(bottom: 6.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - "username".i18n, - maxLines: 1, - style: TextStyle( - color: AppColors.of(context).loginPrimary, - fontWeight: FontWeight.w500, - fontSize: 12.0, - ), - ), - ), - Expanded( - child: Text( - "usernameHint".i18n, - maxLines: 1, - textAlign: TextAlign.right, - style: TextStyle( - color: - AppColors.of(context).loginSecondary, - fontWeight: FontWeight.w500, - fontSize: 12.0, - ), - ), - ), - ], - ), + // kreten login button + GestureDetector( + onTap: () { + final NavigatorState navigator = Navigator.of(context); + navigator + .push( + MaterialPageRoute( + builder: (context) => KretenLoginScreen( + onLogin: (String code) { + codeController.text = code; + navigator.pop(); + }, ), - Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: LoginInput( - style: LoginInputStyle.username, - controller: usernameController, + ), + ) + .then((value) { + if (codeController.text != "") { + _NewLoginAPI(context: context); + } + }); + }, + child: Container( + width: MediaQuery.of(context).size.width * 0.75, + height: 50.0, + decoration: BoxDecoration( + // image: const DecorationImage( + // image: + // AssetImage('assets/images/btn_kreten_login.png'), + // fit: BoxFit.scaleDown, + // ), + borderRadius: BorderRadius.circular(12.0), + color: const Color(0xFF0097C1), + ), + padding: const EdgeInsets.only( + top: 5.0, left: 5.0, right: 5.0, bottom: 5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/btn_kreten_login.png', ), - ), - - // password - Padding( - padding: const EdgeInsets.only(bottom: 6.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - "password".i18n, - maxLines: 1, - style: TextStyle( - color: AppColors.of(context).loginPrimary, - fontWeight: FontWeight.w500, - fontSize: 12.0, - ), - ), - ), - Expanded( - child: Text( - "passwordHint".i18n, - maxLines: 1, - textAlign: TextAlign.right, - style: TextStyle( - color: - AppColors.of(context).loginSecondary, - fontWeight: FontWeight.w500, - fontSize: 12.0, - ), - ), - ), - ], + const SizedBox( + width: 10.0, ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: LoginInput( - style: LoginInputStyle.password, - controller: passwordController, + Container( + width: 1.0, + height: 30.0, + color: Colors.white, ), - ), - - // school - Padding( - padding: const EdgeInsets.only(bottom: 6.0), - child: Text( - "school".i18n, - maxLines: 1, - style: TextStyle( - color: AppColors.of(context).loginPrimary, - fontWeight: FontWeight.w500, - fontSize: 12.0, + const SizedBox( + width: 10.0, + ), + Text( + 'login_w_kreta_acc'.i18n, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15.0, ), ), - ), - SchoolInput( - scroll: _scrollController, - controller: schoolController, - ), - ], - ), - ), + ], + )), ), - // login button - Padding( - padding: const EdgeInsets.only( - top: 35.0, - left: 22.0, - right: 22.0, - ), - child: Visibility( - visible: _loginState != LoginState.inProgress, - replacement: const Padding( - padding: EdgeInsets.symmetric(vertical: 6.0), - child: CircularProgressIndicator( - valueColor: - AlwaysStoppedAnimation(Colors.white), - ), - ), - child: LoginButton( - child: Text("login".i18n, - maxLines: 1, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0, - )), - onPressed: () => _loginAPI(context: context), - ), - ), + const Spacer( + flex: 1, ), + // inputs + // Padding( + // padding: const EdgeInsets.only( + // left: 22.0, + // right: 22.0, + // top: 0.0, + // ), + // child: AutofillGroup( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // // username + // Padding( + // padding: const EdgeInsets.only(bottom: 6.0), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Expanded( + // child: Text( + // "username".i18n, + // maxLines: 1, + // style: TextStyle( + // color: AppColors.of(context).loginPrimary, + // fontWeight: FontWeight.w500, + // fontSize: 12.0, + // ), + // ), + // ), + // Expanded( + // child: Text( + // "usernameHint".i18n, + // maxLines: 1, + // textAlign: TextAlign.right, + // style: TextStyle( + // color: + // AppColors.of(context).loginSecondary, + // fontWeight: FontWeight.w500, + // fontSize: 12.0, + // ), + // ), + // ), + // ], + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(bottom: 12.0), + // child: LoginInput( + // style: LoginInputStyle.username, + // controller: usernameController, + // ), + // ), + + // // password + // Padding( + // padding: const EdgeInsets.only(bottom: 6.0), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Expanded( + // child: Text( + // "password".i18n, + // maxLines: 1, + // style: TextStyle( + // color: AppColors.of(context).loginPrimary, + // fontWeight: FontWeight.w500, + // fontSize: 12.0, + // ), + // ), + // ), + // Expanded( + // child: Text( + // "passwordHint".i18n, + // maxLines: 1, + // textAlign: TextAlign.right, + // style: TextStyle( + // color: + // AppColors.of(context).loginSecondary, + // fontWeight: FontWeight.w500, + // fontSize: 12.0, + // ), + // ), + // ), + // ], + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(bottom: 12.0), + // child: LoginInput( + // style: LoginInputStyle.password, + // controller: passwordController, + // ), + // ), + + // // school + // Padding( + // padding: const EdgeInsets.only(bottom: 6.0), + // child: Text( + // "school".i18n, + // maxLines: 1, + // style: TextStyle( + // color: AppColors.of(context).loginPrimary, + // fontWeight: FontWeight.w500, + // fontSize: 12.0, + // ), + // ), + // ), + // SchoolInput( + // scroll: _scrollController, + // controller: schoolController, + // ), + // ], + // ), + // ), + // ), + + // login button + // Padding( + // padding: const EdgeInsets.only( + // top: 35.0, + // left: 22.0, + // right: 22.0, + // ), + // child: Visibility( + // visible: _loginState != LoginState.inProgress, + // replacement: const Padding( + // padding: EdgeInsets.symmetric(vertical: 6.0), + // child: CircularProgressIndicator( + // valueColor: + // AlwaysStoppedAnimation(Colors.white), + // ), + // ), + // child: LoginButton( + // child: Text("login".i18n, + // maxLines: 1, + // style: const TextStyle( + // fontWeight: FontWeight.bold, + // fontSize: 20.0, + // )), + // onPressed: () => _loginAPI(context: context), + // ), + // ), + // ), + // error messages if (_loginState == LoginState.missingFields || _loginState == LoginState.invalidGrant || @@ -351,6 +410,52 @@ class LoginScreenState extends State { ); } + // new login api + void _NewLoginAPI({required BuildContext context}) { + String code = codeController.text; + + if (code == "") { + return setState(() => _loginState = LoginState.failed); + } + + // ignore: no_leading_underscores_for_local_identifiers + void _callAPI() { + newLoginAPI( + code: code, + context: context, + onLogin: (user) { + ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( + context: context, + brightness: Brightness.light, + content: Text("welcome".i18n.fill([user.name]), + overflow: TextOverflow.ellipsis), + )); + }, + onSuccess: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + setSystemChrome(context); + Navigator.of(context).pushReplacementNamed("login_to_navigation"); + }).then( + (res) => setState(() { + // if (res == LoginState.invalidGrant && + // tempUsername.replaceAll(username, '').length <= 3) { + // tempUsername = username + ' '; + // Timer( + // const Duration(milliseconds: 500), + // () => _loginAPI(context: context), + // ); + // // _loginAPI(context: context); + // } else { + _loginState = res; + // } + }), + ); + } + + setState(() => _loginState = LoginState.inProgress); + _callAPI(); + } + void _loginAPI({required BuildContext context}) { String username = usernameController.text; String password = passwordController.text; diff --git a/refilc_mobile_ui/lib/screens/login/login_screen.i18n.dart b/refilc_mobile_ui/lib/screens/login/login_screen.i18n.dart index b9ef3b2..f8a07f8 100644 --- a/refilc_mobile_ui/lib/screens/login/login_screen.i18n.dart +++ b/refilc_mobile_ui/lib/screens/login/login_screen.i18n.dart @@ -33,6 +33,7 @@ extension Localization on String { "welcome_title_4": "Take as many notes as you want.", "welcome_text_4": "You can also organise your notes by lesson in the built-in notebook, so you can find everything in one app.", + "login_w_kreta_acc": "Log in with\ne-KRÉTA account", }, "hu_hu": { "username": "Felhasználónév", @@ -64,6 +65,7 @@ extension Localization on String { "welcome_title_4": "Füzetelj annyit, amennyit csak szeretnél.", "welcome_text_4": "A beépített jegyzetfüzetbe órák szerint is rendezheted a jegyzeteidet, így mindent megtalálsz egy appban.", + "login_w_kreta_acc": "Belépés e-KRÉTA\nfiókkal", }, "de_de": { "username": "Benutzername", @@ -95,6 +97,7 @@ extension Localization on String { "welcome_title_4": "Take as many notes as you want.", "welcome_text_4": "You can also organise your notes by lesson in the built-in notebook, so you can find everything in one app.", + "login_w_kreta_acc": "Mit e-KRÉTA-Konto\nanmelden", }, };