Skip to content

Commit

Permalink
feature: Create NFC-Pass Device Connect (#279)
Browse files Browse the repository at this point in the history
* Add deferredLoading

* feature: Create NFC-Pass Device Connect

* feat: Create Nfc Fetcher and refactor auth bloc

* Удалён AuthBloc. Его функции перенесены в Profile Bloc
* Теперь в события не передаются токены доступа к апи. Управление токенами делегируется на репозиторий/remote data-source или отдельные пакеты для управления сетевыми запросами
* Создан `fetchNfcCode` event для получения кода пропуск

* feat: Add NFC fetching and errors feedback

* Update AndroidManifest.xml
0niel authored Feb 15, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 7754bb6 commit 6a07951
Showing 61 changed files with 5,921 additions and 653 deletions.
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
package="ninja.mirea.mireaapp">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.NFC" />

<application
android:label="Ninja Mirea"
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.8'
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
4 changes: 4 additions & 0 deletions lib/common/errors/exceptions.dart
Original file line number Diff line number Diff line change
@@ -3,6 +3,10 @@ class ServerException implements Exception {
ServerException(this.cause);
}

class NfcStaffnodeNotExistException extends ServerException {
NfcStaffnodeNotExistException() : super("NfcStaffnodeNotExistException");
}

class CacheException implements Exception {
final String cause;
CacheException(this.cause);
4 changes: 4 additions & 0 deletions lib/common/errors/failures.dart
Original file line number Diff line number Diff line change
@@ -13,6 +13,10 @@ class ServerFailure extends Failure {
const ServerFailure([String? cause]) : super(cause);
}

class NfcStaffnodeNotExistFailure extends ServerFailure {
const NfcStaffnodeNotExistFailure();
}

class CacheFailure extends Failure {
const CacheFailure([String? cause]) : super(cause);
}
40 changes: 35 additions & 5 deletions lib/data/datasources/user_local.dart
Original file line number Diff line number Diff line change
@@ -1,31 +1,61 @@
import 'dart:core';

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:rtu_mirea_app/common/errors/exceptions.dart';
import 'package:shared_preferences/shared_preferences.dart';

abstract class UserLocalData {
Future<void> setTokenToCache(String token);
Future<String> getTokenFromCache();
Future<void> removeTokenFromCache();

Future<int> getNfcCodeFromCache();
Future<void> setNfcCodeToCache(int code);
Future<void> removeNfcCodeFromCache();
}

class UserLocalDataImpl implements UserLocalData {
final SharedPreferences sharedPreferences;
final FlutterSecureStorage secureStorage;

UserLocalDataImpl({required this.sharedPreferences});
UserLocalDataImpl({
required this.sharedPreferences,
required this.secureStorage,
});

@override
Future<void> setTokenToCache(String token) {
return sharedPreferences.setString('auth_token', token);
return secureStorage.write(key: 'lks_access_token', value: token);
}

@override
Future<String> getTokenFromCache() {
String? token = sharedPreferences.getString('auth_token');
Future<String> getTokenFromCache() async {
String? token = await secureStorage.read(key: 'lks_access_token');
if (token == null) throw CacheException('Auth token are not set');
return Future.value(token);
}

@override
Future<void> removeTokenFromCache() {
return sharedPreferences.remove('auth_token');
return secureStorage.delete(key: 'lks_access_token');
}

@override
Future<int> getNfcCodeFromCache() async {
String? value = await secureStorage.read(key: 'nfc_code');

if (value == null) throw CacheException('NFC code are not set');

return Future.value(int.parse(value));
}

@override
Future<void> setNfcCodeToCache(int code) async {
await secureStorage.write(key: 'nfc_code', value: code.toString());
}

@override
Future<void> removeNfcCodeFromCache() async {
await secureStorage.delete(key: 'nfc_code');
}
}
182 changes: 155 additions & 27 deletions lib/data/datasources/user_remote.dart
Original file line number Diff line number Diff line change
@@ -6,22 +6,29 @@ import 'package:rtu_mirea_app/common/oauth.dart';
import 'package:rtu_mirea_app/data/models/announce_model.dart';
import 'package:rtu_mirea_app/data/models/attendance_model.dart';
import 'package:rtu_mirea_app/data/models/employee_model.dart';
import 'package:rtu_mirea_app/data/models/nfc_pass_model.dart';
import 'package:rtu_mirea_app/data/models/score_model.dart';
import 'package:rtu_mirea_app/data/models/user_model.dart';

abstract class UserRemoteData {
Future<String> auth();
Future<void> logOut();
Future<UserModel> getProfileData(String token);
Future<List<AnnounceModel>> getAnnounces(String token);
Future<List<EmployeeModel>> getEmployees(String token, String name);
Future<List<AttendanceModel>> getAttendance(
String token, String dateStart, String dateEnd);
Future<Map<String, List<ScoreModel>>> getScores(String token);
Future<UserModel> getProfileData();
Future<List<AnnounceModel>> getAnnounces();
Future<List<EmployeeModel>> getEmployees(String name);
Future<List<AttendanceModel>> getAttendance(String dateStart, String dateEnd);
Future<Map<String, List<ScoreModel>>> getScores();
Future<List<NfcPassModel>> getNfcPasses(
String code, String studentId, String deviceId);
Future<int> getNfcCode(String code, String studentId, String deviceId);
Future<void> connectNfcPass(
String code, String studentId, String deviceId, String deviceName);
Future<void> sendNfcNotExistFeedback(
String fullName, String group, String personalNumber, String studentId);
}

class UserRemoteDataImpl implements UserRemoteData {
static const _apiUrl = 'https://lks.mirea.ninja/api/';
static const _apiUrl = 'https://lks.mirea.ninja/api';

final Dio httpClient;
final LksOauth2 lksOauth2;
@@ -45,31 +52,33 @@ class UserRemoteDataImpl implements UserRemoteData {
}

@override
Future<UserModel> getProfileData(String token) async {
Future<UserModel> getProfileData() async {
final response = await lksOauth2.oauth2Helper.get(
'$_apiUrl?action=getData&url=https://lk.mirea.ru/profile/',
'$_apiUrl/?action=getData&url=https://lk.mirea.ru/profile/',
);
var jsonResponse = json.decode(response.body);

log('Status code: ${response.statusCode}, Response: ${response.body}');
log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'getProfileData');

if (jsonResponse.containsKey('errors')) {
throw ServerException(jsonResponse['errors'][0]);
}
if (response.statusCode == 200) {
return UserModel.fromRawJson(response.body);
} else {
throw ServerException('Response status code is $response.statusCode');
throw ServerException('Response status code is ${response.statusCode}');
}
}

@override
Future<List<AnnounceModel>> getAnnounces(String token) async {
Future<List<AnnounceModel>> getAnnounces() async {
final response = await lksOauth2.oauth2Helper.get(
'$_apiUrl?action=getData&url=https://lk.mirea.ru/livestream/',
'$_apiUrl/?action=getData&url=https://lk.mirea.ru/livestream/',
);

log('Status code: ${response.statusCode}, Response: ${response.body}');
log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'getAnnounces');

var jsonResponse = json.decode(response.body);
if (jsonResponse.containsKey('errors')) {
@@ -83,17 +92,18 @@ class UserRemoteDataImpl implements UserRemoteData {
}
return announces;
} else {
throw ServerException('Response status code is $response.statusCode');
throw ServerException('Response status code is ${response.statusCode}');
}
}

@override
Future<List<EmployeeModel>> getEmployees(String token, String name) async {
Future<List<EmployeeModel>> getEmployees(String name) async {
final response = await lksOauth2.oauth2Helper.get(
'$_apiUrl?action=getData&url=https://lk.mirea.ru/lectors/&page=undefined&findname=$name',
'$_apiUrl/?action=getData&url=https://lk.mirea.ru/lectors/&page=undefined&findname=$name',
);

log('Status code: ${response.statusCode}, Response: ${response.body}');
log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'getEmployees');

var jsonResponse = json.decode(response.body);
if (jsonResponse.containsKey('errors')) {
@@ -109,17 +119,18 @@ class UserRemoteDataImpl implements UserRemoteData {
}
return employees;
} else {
throw ServerException('Response status code is $response.statusCode');
throw ServerException('Response status code is ${response.statusCode}');
}
}

@override
Future<Map<String, List<ScoreModel>>> getScores(String token) async {
Future<Map<String, List<ScoreModel>>> getScores() async {
final response = await lksOauth2.oauth2Helper.get(
'$_apiUrl?action=getData&url=https://lk.mirea.ru/learning/scores/',
'$_apiUrl/?action=getData&url=https://lk.mirea.ru/learning/scores/',
);

log('Status code: ${response.statusCode}, Response: ${response.body}');
log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'getScores');

var jsonResponse = json.decode(response.body);
if (jsonResponse.containsKey('errors')) {
@@ -140,18 +151,74 @@ class UserRemoteDataImpl implements UserRemoteData {

return scores;
} else {
throw ServerException('Response status code is $response.statusCode');
throw ServerException('Response status code is ${response.statusCode}');
}
}

@override
Future<List<NfcPassModel>> getNfcPasses(
String code, String studentId, String deviceId) async {
final response = await lksOauth2.oauth2Helper
.get('$_apiUrl/cms/nfc-passes/$code/$studentId/$deviceId');

log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'getScores');

if (response.statusCode != 200) {
try {
var jsonResponse = json.decode(response.body);
if (jsonResponse.containsKey('message')) {
throw ServerException(jsonResponse['message']);
}
} catch (e) {
throw ServerException('Response status code is ${response.statusCode}');
}
}

var jsonResponse = json.decode(response.body);

List<NfcPassModel> userNfcPasses = [];
userNfcPasses = List<NfcPassModel>.from(jsonResponse['data']
.map((x) => NfcPassModel.fromJson(x['attributes'])));
return userNfcPasses;
}

@override
Future<void> connectNfcPass(
String code, String studentId, String deviceId, String deviceName) async {
final data = {
'code': code,
'studentId': studentId,
'deviceId': deviceId,
'deviceName': deviceName,
};

final response = await lksOauth2.oauth2Helper.post(
'$_apiUrl/cms/nfc-passes',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: json.encode(data),
);

log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'connectNfcPass');

if (response.statusCode != 200) {
throw ServerException('Response status code is ${response.statusCode}');
}
}

@override
Future<List<AttendanceModel>> getAttendance(
String token, String dateStart, String dateEnd) async {
String dateStart, String dateEnd) async {
final response = await lksOauth2.oauth2Helper.get(
'$_apiUrl?action=getData&url=https://lk.mirea.ru/schedule/attendance/&startDate=$dateStart&endDate=$dateEnd',
'$_apiUrl/?action=getData&url=https://lk.mirea.ru/schedule/attendance/&startDate=$dateStart&endDate=$dateEnd',
);

log('Status code: ${response.statusCode}, Response: ${response.body}');
log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'getAttendance');

var jsonResponse = json.decode(response.body);
if (jsonResponse.containsKey('errors')) {
@@ -174,7 +241,68 @@ class UserRemoteDataImpl implements UserRemoteData {
}
return attendance;
} else {
throw ServerException('Response status code is $response.statusCode');
throw ServerException('Response status code is ${response.statusCode}');
}
}

@override
Future<int> getNfcCode(String code, String studentId, String deviceId) async {
final response = await lksOauth2.oauth2Helper
.get('$_apiUrl/get-nfc-code/$code/$studentId/$deviceId');

log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'getNfcCode');

var jsonResponse = json.decode(response.body);

if (jsonResponse.containsKey('code')) {
return jsonResponse['code'];
} else {
// Local api error
if (jsonResponse.containsKey('message')) {
throw ServerException(jsonResponse['message']);
}
// LKS api error
else if (jsonResponse.containsKey('error') &&
jsonResponse['error'] == 'StaffnodeNotExist') {
throw NfcStaffnodeNotExistException();
} else {
throw ServerException('${jsonResponse['error']}');
}
}
}

@override
Future<void> sendNfcNotExistFeedback(String fullName, String group,
String personalNumber, String studentId) async {
final data = {
'fullName': fullName,
'group': group,
'personalNumber': personalNumber,
'studentId': studentId,
};

final response = await lksOauth2.oauth2Helper.post(
'$_apiUrl/cms/nfc-pass-not-exist-feedback',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: json.encode(data),
);

log('Status code: ${response.statusCode}, Response: ${response.body}',
name: 'sendNfcNotExistFeedback');

if (response.statusCode != 200) {
if (response.statusCode == 400) {
if (response.body.contains("This attribute must be unique")) {
throw ServerException('Уже отправлено. Пожалуйста, подождите. '
'Время обработки заявки - до 7 рабочих дней.');
}
} else {
throw ServerException('Response status code is ${response.statusCode}');
}
}
}
}
Loading

0 comments on commit 6a07951

Please sign in to comment.