forked from eatvio/eatvio_app
Compare commits
21 Commits
main
...
android-ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e1633d1c | ||
| 5b2ac4f228 | |||
| 51aff6c175 | |||
| cba2689406 | |||
| 5f0b7e026b | |||
| 3a41a85f9d | |||
| 194b8d5b16 | |||
|
|
508c321913 | ||
|
|
4a5c67632e | ||
| e2d287eb9e | |||
| cf5c42ff4b | |||
|
|
5e6e114d5d | ||
|
|
e9cfcb9637 | ||
| 28c62bf775 | |||
| dedb55ecca | |||
| 04dab02a6d | |||
| db293bf13f | |||
| fe54037cef | |||
| c70c9beb20 | |||
| 37a787c8d0 | |||
| 3b8d1bf651 |
@@ -14,7 +14,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: [ubuntu-latest]
|
runs-on: [debian-12]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v3
|
- uses: https://github.com/actions/checkout@v3
|
||||||
@@ -155,10 +155,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: |
|
run: |
|
||||||
flutter clean
|
flutter clean
|
||||||
flutter pub cache repair
|
flutter pub cache repair
|
||||||
flutter pub get
|
flutter pub get
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
- name: Native Analyze
|
- name: Native Analyze
|
||||||
run: flutter analyze
|
run: flutter analyze
|
||||||
|
|||||||
@@ -437,73 +437,21 @@ import flutter_foreground_task
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bringAppToForeground() {
|
/// The answer callback runs before the scene is visible. A custom-scheme
|
||||||
DispatchQueue.main.async {
|
/// open from here is an untrusted user action and SpringBoard denies it.
|
||||||
for scene in UIApplication.shared.connectedScenes {
|
/// CallKit foregrounds a video call itself after `fulfill`. Flutter reads
|
||||||
guard let windowScene = scene as? UIWindowScene else { continue }
|
/// the stored payload once that scene is actually active.
|
||||||
for window in windowScene.windows {
|
func deliverStoredAcceptedCall() {
|
||||||
window.makeKeyAndVisible()
|
guard voipDiagnosticsChannel != nil else { return }
|
||||||
}
|
guard let payload = UserDefaults.standard.dictionary(forKey: pendingAcceptedVoipCallKey) else {
|
||||||
}
|
|
||||||
self.window?.makeKeyAndVisible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func openAppForAcceptedCall(_ call: Call) {
|
|
||||||
guard let conversationUuid = conversationUuid(fromCall: call), !conversationUuid.isEmpty else {
|
|
||||||
recordVoipDiagnostic(
|
|
||||||
"accepted_call_missing_conversation_uuid",
|
|
||||||
data: ["callKitId": call.data.uuid]
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let acceptedAt = (payload[pendingAcceptedAtKey] as? NSNumber)?.doubleValue
|
||||||
var components = URLComponents()
|
if let acceptedAt, Date().timeIntervalSince1970 - acceptedAt > pendingAcceptedCallTtl {
|
||||||
components.scheme = appUrlScheme
|
UserDefaults.standard.removeObject(forKey: pendingAcceptedVoipCallKey)
|
||||||
components.host = "video-call"
|
|
||||||
var queryItems = [
|
|
||||||
URLQueryItem(name: "callKitId", value: call.data.uuid),
|
|
||||||
URLQueryItem(name: "conversationUuid", value: conversationUuid)
|
|
||||||
]
|
|
||||||
let serverCallId = extraServerCallId(call.data.extra)
|
|
||||||
if !serverCallId.isEmpty {
|
|
||||||
queryItems.append(URLQueryItem(name: "call_id", value: serverCallId))
|
|
||||||
}
|
|
||||||
components.queryItems = queryItems
|
|
||||||
|
|
||||||
guard let url = components.url else {
|
|
||||||
recordVoipDiagnostic(
|
|
||||||
"accepted_call_url_build_failed",
|
|
||||||
data: [
|
|
||||||
"callKitId": call.data.uuid,
|
|
||||||
"conversationUuid": conversationUuid,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: payload)
|
||||||
recordVoipDiagnostic(
|
|
||||||
"accepted_call_open_app_requested",
|
|
||||||
data: [
|
|
||||||
"callKitId": call.data.uuid,
|
|
||||||
"conversationUuid": conversationUuid,
|
|
||||||
"url": url.absoluteString,
|
|
||||||
"appState": UIApplication.shared.applicationState.rawValue,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
bringAppToForeground()
|
|
||||||
|
|
||||||
UIApplication.shared.open(url, options: [:]) { [weak self] success in
|
|
||||||
self?.recordVoipDiagnostic(
|
|
||||||
success ? "accepted_call_open_app_succeeded" : "accepted_call_open_app_failed",
|
|
||||||
data: [
|
|
||||||
"callKitId": call.data.uuid,
|
|
||||||
"conversationUuid": conversationUuid,
|
|
||||||
"url": url.absoluteString,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sanitizedDiagnosticValue(_ value: Any?) -> Any {
|
private func sanitizedDiagnosticValue(_ value: Any?) -> Any {
|
||||||
@@ -1177,6 +1125,8 @@ import flutter_foreground_task
|
|||||||
// activation and can start I/O before didActivate.
|
// activation and can start I/O before didActivate.
|
||||||
prepareAvAudioSessionCategory()
|
prepareAvAudioSessionCategory()
|
||||||
storePendingAcceptedCall(call)
|
storePendingAcceptedCall(call)
|
||||||
|
let extra = call.data.extra as? [String: Any] ?? [:]
|
||||||
|
VideoCallDeclineClient.sendAccept(extra: extra, handle: call.data.handle)
|
||||||
recordVoipDiagnostic(
|
recordVoipDiagnostic(
|
||||||
"callkit_accept_native",
|
"callkit_accept_native",
|
||||||
data: [
|
data: [
|
||||||
@@ -1185,11 +1135,7 @@ import flutter_foreground_task
|
|||||||
"data": call.data.toJSON(),
|
"data": call.data.toJSON(),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: call.data.toJSON())
|
deliverStoredAcceptedCall()
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.bringAppToForeground()
|
|
||||||
self?.openAppForAcceptedCall(call)
|
|
||||||
}
|
|
||||||
action.fulfill()
|
action.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ class SceneDelegate: FlutterSceneDelegate {
|
|||||||
override func sceneDidBecomeActive(_ scene: UIScene) {
|
override func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
super.sceneDidBecomeActive(scene)
|
super.sceneDidBecomeActive(scene)
|
||||||
registerCallChannels()
|
registerCallChannels()
|
||||||
|
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appDelegate.deliverStoredAcceptedCall()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
||||||
|
|||||||
@@ -8,25 +8,37 @@ enum VideoCallDeclineClient {
|
|||||||
/// `POST /video-call/decline` from the CallKit callback. The process is
|
/// `POST /video-call/decline` from the CallKit callback. The process is
|
||||||
/// awake here even when the user never opens the app afterwards.
|
/// awake here even when the user never opens the app afterwards.
|
||||||
static func send(extra: [String: Any], handle: String) {
|
static func send(extra: [String: Any], handle: String) {
|
||||||
|
post(action: "decline", extra: extra, handle: handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `POST /video-call/accept` from the CallKit answer callback. Flutter may
|
||||||
|
/// still be frozen on the splash snapshot, so the other device must be
|
||||||
|
/// told from here. A later Dart retry with the same hw_id is idempotent.
|
||||||
|
static func sendAccept(extra: [String: Any], handle: String) {
|
||||||
|
post(action: "accept", extra: extra, handle: handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func post(action: String, extra: [String: Any], handle: String) {
|
||||||
let fields = declineFields(extra: extra, handle: handle)
|
let fields = declineFields(extra: extra, handle: handle)
|
||||||
guard let request = makeRequest(
|
guard let request = makeRequest(
|
||||||
baseUrl: prefsString("apiBaseUrl") ?? prefsString("customBaseUrl"),
|
baseUrl: prefsString("apiBaseUrl") ?? prefsString("customBaseUrl"),
|
||||||
token: prefsString("token"),
|
token: prefsString("token"),
|
||||||
hwId: UIDevice.current.identifierForVendor?.uuidString,
|
hwId: UIDevice.current.identifierForVendor?.uuidString,
|
||||||
fields: fields
|
fields: fields,
|
||||||
|
action: action
|
||||||
) else {
|
) else {
|
||||||
NSLog(
|
NSLog(
|
||||||
"[EatvioVoIP] decline POST skipped conversation=\(fields.conversationUuid) ended=\(fields.endedPush)"
|
"[EatvioVoIP] \(action) POST skipped conversation=\(fields.conversationUuid) ended=\(fields.endedPush)"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let task = EatvioBackgroundTask()
|
let task = EatvioBackgroundTask()
|
||||||
task.begin(name: "video-call-decline")
|
task.begin(name: "video-call-\(action)")
|
||||||
let urlTask = URLSession.shared.dataTask(with: request) { _, response, error in
|
let urlTask = URLSession.shared.dataTask(with: request) { _, response, error in
|
||||||
let code = (response as? HTTPURLResponse)?.statusCode ?? -1
|
let code = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||||
NSLog(
|
NSLog(
|
||||||
"[EatvioVoIP] decline POST status=\(code) conversation=\(fields.conversationUuid) error=\(error?.localizedDescription ?? "-")"
|
"[EatvioVoIP] \(action) POST status=\(code) conversation=\(fields.conversationUuid) error=\(error?.localizedDescription ?? "-")"
|
||||||
)
|
)
|
||||||
task.end()
|
task.end()
|
||||||
}
|
}
|
||||||
@@ -37,7 +49,8 @@ enum VideoCallDeclineClient {
|
|||||||
baseUrl: String?,
|
baseUrl: String?,
|
||||||
token: String?,
|
token: String?,
|
||||||
hwId: String?,
|
hwId: String?,
|
||||||
fields: VideoCallDeclineFields
|
fields: VideoCallDeclineFields,
|
||||||
|
action: String = "decline"
|
||||||
) -> URLRequest? {
|
) -> URLRequest? {
|
||||||
if fields.endedPush { return nil }
|
if fields.endedPush { return nil }
|
||||||
let conversation = fields.conversationUuid.trimmingCharacters(in: .whitespacesAndNewlines)
|
let conversation = fields.conversationUuid.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
@@ -50,8 +63,9 @@ enum VideoCallDeclineClient {
|
|||||||
guard !conversation.isEmpty, !authToken.isEmpty else { return nil }
|
guard !conversation.isEmpty, !authToken.isEmpty else { return nil }
|
||||||
guard deviceId.count >= 8, deviceId.count <= 64 else { return nil }
|
guard deviceId.count >= 8, deviceId.count <= 64 else { return nil }
|
||||||
guard root.hasPrefix("http://") || root.hasPrefix("https://") else { return nil }
|
guard root.hasPrefix("http://") || root.hasPrefix("https://") else { return nil }
|
||||||
|
guard action == "accept" || action == "decline" else { return nil }
|
||||||
guard let encoded = conversation.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
|
guard let encoded = conversation.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
|
||||||
let url = URL(string: "\(root)/api/v2/chat/\(encoded)/video-call/decline")
|
let url = URL(string: "\(root)/api/v2/chat/\(encoded)/video-call/\(action)")
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
var body: [String: String] = ["hw_id": deviceId]
|
var body: [String: String] = ["hw_id": deviceId]
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
|
|||||||
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
|
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
|
||||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||||
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
||||||
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
|
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart'
|
||||||
|
as profile_plans;
|
||||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||||
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
||||||
@@ -70,69 +71,174 @@ class CubitRegistrations {
|
|||||||
/// Регистрация Cubit'ов как синглтонов
|
/// Регистрация Cubit'ов как синглтонов
|
||||||
static void _registerSingletonCubits() {
|
static void _registerSingletonCubits() {
|
||||||
sl.registerLazySingleton<NewMessagesCubit>(() => NewMessagesCubit());
|
sl.registerLazySingleton<NewMessagesCubit>(() => NewMessagesCubit());
|
||||||
sl.registerLazySingleton<NotificationsCubit>(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()));
|
sl.registerLazySingleton<NotificationsCubit>(
|
||||||
sl.registerLazySingleton<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl()));
|
() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()),
|
||||||
sl.registerLazySingleton<ChatsCubit>(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()));
|
);
|
||||||
sl.registerLazySingleton<InternetConnectionCubit>(() => InternetConnectionCubit());
|
sl.registerLazySingleton<ProfileAvatarCubit>(
|
||||||
sl.registerLazySingleton<VideoCallManager>(
|
() => ProfileAvatarCubit(profileRepository: sl()),
|
||||||
() => VideoCallManager(
|
);
|
||||||
videoCallRepository: sl(),
|
sl.registerLazySingleton<ChatsCubit>(
|
||||||
|
() => ChatsCubit(
|
||||||
|
chatRepository: sl(),
|
||||||
|
wsManager: sl(),
|
||||||
profileRepository: sl(),
|
profileRepository: sl(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
sl.registerLazySingleton<InternetConnectionCubit>(
|
||||||
|
() => InternetConnectionCubit(),
|
||||||
|
);
|
||||||
|
sl.registerLazySingleton<VideoCallManager>(
|
||||||
|
() =>
|
||||||
|
VideoCallManager(videoCallRepository: sl(), profileRepository: sl()),
|
||||||
|
);
|
||||||
sl.registerLazySingleton<VideoCallSessionCubit>(
|
sl.registerLazySingleton<VideoCallSessionCubit>(
|
||||||
() => VideoCallSessionCubit(
|
() => VideoCallSessionCubit(
|
||||||
videoCallManager: sl(),
|
videoCallManager: sl(),
|
||||||
systemPipCoordinator: sl(),
|
systemPipCoordinator: sl(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
sl.registerLazySingleton<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Регистрация Cubit'ов как фабрик
|
/// Регистрация Cubit'ов как фабрик
|
||||||
static void _registerFactoryCubits() {
|
static void _registerFactoryCubits() {
|
||||||
// Auth related cubits
|
// Auth related cubits
|
||||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
|
sl.registerFactory<AuthCubit>(
|
||||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
() => AuthCubit(
|
||||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
authRepository: sl(),
|
||||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()));
|
firebaseApi: sl(),
|
||||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
prefs: sl(),
|
||||||
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
|
profileRepository: sl(),
|
||||||
|
sessionLifecycleService: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sl.registerFactory<RegistrationCubit>(
|
||||||
|
() => RegistrationCubit(
|
||||||
|
registrationRepository: sl(),
|
||||||
|
prefs: sl(),
|
||||||
|
sessionLifecycleService: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sl.registerFactory<ResetPasswordCubit>(
|
||||||
|
() => ResetPasswordCubit(resetPasswordRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<VerifyEmailCubit>(
|
||||||
|
() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<OAuthCubit>(
|
||||||
|
() => OAuthCubit(
|
||||||
|
authRepository: sl(),
|
||||||
|
firebaseApi: sl(),
|
||||||
|
prefs: sl(),
|
||||||
|
sessionLifecycleService: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sl.registerFactory<RegistrationCubitV2>(
|
||||||
|
() => RegistrationCubitV2(
|
||||||
|
registrationRepository: sl(),
|
||||||
|
firebaseApi: sl(),
|
||||||
|
sessionLifecycleService: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
// Feed related cubits
|
// Feed related cubits
|
||||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
sl.registerFactory<RecipesCubit>(
|
||||||
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
|
() => RecipesCubit(
|
||||||
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
|
feedRepository: sl(),
|
||||||
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
|
recipeRepository: sl(),
|
||||||
sl.registerFactory<ConsultantsCubit>(() => ConsultantsCubit(feedRepository: sl()));
|
aboutRecipeRepository: sl(),
|
||||||
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
|
),
|
||||||
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
|
);
|
||||||
|
sl.registerFactory<DietsCubit>(
|
||||||
|
() => DietsCubit(feedRepository: sl(), dietsRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<PlansCubit>(
|
||||||
|
() => PlansCubit(feedRepository: sl(), planRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<ConsultantsCubit>(
|
||||||
|
() => ConsultantsCubit(feedRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<CommentsCubit>(
|
||||||
|
() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<FeedProfileCubit>(
|
||||||
|
() => FeedProfileCubit(profileRepository: sl()),
|
||||||
|
);
|
||||||
|
|
||||||
// Chat related cubits
|
// Chat related cubits
|
||||||
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()));
|
sl.registerFactory<ChatRoomCubit>(
|
||||||
|
() => ChatRoomCubit(
|
||||||
|
cardsRepository: sl(),
|
||||||
|
chatRepository: sl(),
|
||||||
|
wsManager: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Profile related cubits
|
// Profile related cubits
|
||||||
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
|
sl.registerFactory<ProfileCubit>(
|
||||||
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
|
() => ProfileCubit(
|
||||||
|
profileRepository: sl(),
|
||||||
|
chatRepository: sl(),
|
||||||
|
consultantRepository: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sl.registerFactory<profile_plans.PlansCubit>(
|
||||||
|
() => profile_plans.PlansCubit(plansRepository: sl()),
|
||||||
|
);
|
||||||
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
|
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
|
||||||
|
|
||||||
// Content creation cubits
|
// Content creation cubits
|
||||||
sl.registerFactory<CreateDietCubit>(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()));
|
sl.registerFactory<CreateDietCubit>(
|
||||||
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit(createPlanRepository: sl()));
|
() => CreateDietCubit(
|
||||||
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()));
|
profileRepository: sl(),
|
||||||
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
|
createDietRepository: sl(),
|
||||||
|
glossaryRepository: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sl.registerFactory<CreatePlanCubit>(
|
||||||
|
() => CreatePlanCubit(createPlanRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<CreateRecipeCubit>(
|
||||||
|
() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<BlogPostCreateCubit>(
|
||||||
|
() => BlogPostCreateCubit(blogPostRepository: sl()),
|
||||||
|
);
|
||||||
|
|
||||||
// Content viewing cubits
|
// Content viewing cubits
|
||||||
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
|
sl.registerFactory<AboutDietCubit>(
|
||||||
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl(), profileRepository: sl(), dietRepository: sl()));
|
() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()),
|
||||||
sl.registerFactory<AboutRecipeCubit>(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl()));
|
);
|
||||||
|
sl.registerFactory<AboutPlanCubit>(
|
||||||
|
() => AboutPlanCubit(
|
||||||
|
planRepository: sl(),
|
||||||
|
profileRepository: sl(),
|
||||||
|
dietRepository: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sl.registerFactory<AboutRecipeCubit>(
|
||||||
|
() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl()),
|
||||||
|
);
|
||||||
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
|
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
|
||||||
|
|
||||||
// User content cubits
|
// User content cubits
|
||||||
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
|
sl.registerFactory<MyDietsCubit>(
|
||||||
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
|
() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()),
|
||||||
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl(), recipeRepository: sl()));
|
);
|
||||||
|
sl.registerFactory<MyPlansCubit>(
|
||||||
|
() => MyPlansCubit(myPlansRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<MyRecipesCubit>(
|
||||||
|
() => MyRecipesCubit(
|
||||||
|
myRecipesRepository: sl(),
|
||||||
|
aboutRecipeRepository: sl(),
|
||||||
|
recipeRepository: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Other feature cubits
|
// Other feature cubits
|
||||||
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
sl.registerFactory<CalendarCubit>(
|
||||||
|
() => CalendarCubit(calendarRepository: sl()),
|
||||||
|
);
|
||||||
sl.registerFactory<DiaryCubit>(
|
sl.registerFactory<DiaryCubit>(
|
||||||
() => DiaryCubit(
|
() => DiaryCubit(
|
||||||
chatRepository: sl(),
|
chatRepository: sl(),
|
||||||
@@ -144,28 +250,74 @@ class CubitRegistrations {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
sl.registerFactory<SettingsCubit>(
|
sl.registerFactory<SettingsCubit>(
|
||||||
() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()),
|
() => SettingsCubit(
|
||||||
|
profileRepository: sl(),
|
||||||
|
settingsRepository: sl(),
|
||||||
|
consultantRepository: sl(),
|
||||||
|
consultationsRepository: sl(),
|
||||||
|
sessionLifecycleService: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sl.registerFactory<RegisterConsultantCubit>(
|
||||||
|
() => RegisterConsultantCubit(consultantRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<ComplainingCubit>(
|
||||||
|
() => ComplainingCubit(repository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<UserCardsCubit>(
|
||||||
|
() => UserCardsCubit(cardsRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<OrderInfoCubit>(
|
||||||
|
() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl()),
|
||||||
);
|
);
|
||||||
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
|
|
||||||
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
|
|
||||||
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
|
|
||||||
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl()));
|
|
||||||
sl.registerFactory<ConsultationPaymentCubit>(
|
sl.registerFactory<ConsultationPaymentCubit>(
|
||||||
() => ConsultationPaymentCubit(paymentRepository: sl(), sbpRepository: sl()),
|
() => ConsultationPaymentCubit(
|
||||||
|
paymentRepository: sl(),
|
||||||
|
sbpRepository: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sl.registerFactory<ConsultationsCubit>(
|
||||||
|
() => ConsultationsCubit(consultationsRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<ConsultantCommentsCubit>(
|
||||||
|
() => ConsultantCommentsCubit(diaryRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<CancelPlanCubit>(
|
||||||
|
() => CancelPlanCubit(cancelPlanRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<ConsultationsArchiveCubit>(
|
||||||
|
() => ConsultationsArchiveCubit(consultationsRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<ConsultantFinancesCubit>(
|
||||||
|
() => ConsultantFinancesCubit(consultationsRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<UserPurchasesCubit>(
|
||||||
|
() => UserPurchasesCubit(consultationsRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<BlockedUsersCubit>(
|
||||||
|
() => BlockedUsersCubit(blockedUsersRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<NotificationSettingsCubit>(
|
||||||
|
() => NotificationSettingsCubit(notificationSettingsRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<ConsultantRatingCubit>(
|
||||||
|
() => ConsultantRatingCubit(
|
||||||
|
feedRepository: sl(),
|
||||||
|
repository: sl(),
|
||||||
|
profileRepository: sl(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
|
|
||||||
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
|
||||||
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
|
||||||
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
|
|
||||||
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
|
|
||||||
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
|
|
||||||
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
|
|
||||||
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
|
|
||||||
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(feedRepository: sl(), repository: sl(), profileRepository: sl()));
|
|
||||||
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
||||||
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
sl.registerFactory<UserDetailsCubit>(
|
||||||
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
|
() => UserDetailsCubit(profileRepository: sl()),
|
||||||
|
);
|
||||||
|
sl.registerFactory<MenuCubit>(() => MenuCubit(sl(), sl()));
|
||||||
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
|
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
|
||||||
sl.registerFactory<PersonalDietSuggestionCubit>(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
|
sl.registerFactory<PersonalDietSuggestionCubit>(
|
||||||
|
() => PersonalDietSuggestionCubit(
|
||||||
|
dietRepository: sl(),
|
||||||
|
personalDietSuggestionService: sl(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
lib/core/di/session_registrations.dart
Normal file
15
lib/core/di/session_registrations.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
|
|
||||||
|
class SessionRegistrations {
|
||||||
|
static void registerAll() {
|
||||||
|
sl.registerLazySingleton<SessionLifecycleService>(
|
||||||
|
() => SessionLifecycleService(
|
||||||
|
wsManager: sl<WsManager>(),
|
||||||
|
errorReporter: sl<ErrorReporter>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
class TokenInterceptor extends Interceptor {
|
class TokenInterceptor extends Interceptor {
|
||||||
@override
|
@override
|
||||||
@@ -14,6 +12,7 @@ class TokenInterceptor extends Interceptor {
|
|||||||
final token = await TokenStorage.getTokenFromStorage();
|
final token = await TokenStorage.getTokenFromStorage();
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
options.extra['authToken'] = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onRequest(options, handler);
|
super.onRequest(options, handler);
|
||||||
@@ -25,9 +24,18 @@ class TokenInterceptor extends Interceptor {
|
|||||||
ErrorInterceptorHandler handler,
|
ErrorInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
if (err.response?.statusCode == 401) {
|
if (err.response?.statusCode == 401) {
|
||||||
await sl<WsManager>().clear();
|
final requestCarriedAuth = err.requestOptions.headers.containsKey(
|
||||||
await TokenStorage.removeTokenInStorage();
|
'Authorization',
|
||||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
);
|
||||||
|
final requestToken = err.requestOptions.extra['authToken'];
|
||||||
|
final currentToken = await TokenStorage.getTokenFromStorage();
|
||||||
|
if (requestCarriedAuth &&
|
||||||
|
requestToken != null &&
|
||||||
|
requestToken == currentToken) {
|
||||||
|
await sl<SessionLifecycleService>().endSession(
|
||||||
|
reason: SessionEndReason.unauthorized,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.onError(err, handler);
|
super.onError(err, handler);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
|||||||
import 'package:eatvio_app/core/di/external_registrations.dart';
|
import 'package:eatvio_app/core/di/external_registrations.dart';
|
||||||
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
||||||
import 'package:eatvio_app/core/di/service_registrations.dart';
|
import 'package:eatvio_app/core/di/service_registrations.dart';
|
||||||
|
import 'package:eatvio_app/core/di/session_registrations.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
@@ -38,4 +39,6 @@ Future<void> init({required AppBuildType buildType}) async {
|
|||||||
sl.registerLazySingleton<WsManager>(
|
sl.registerLazySingleton<WsManager>(
|
||||||
() => WsManager(wsService: sl(), errorReporter: sl()),
|
() => WsManager(wsService: sl(), errorReporter: sl()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
SessionRegistrations.registerAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
|
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
|
||||||
@@ -24,14 +23,10 @@ class MainNavigationRoutes {
|
|||||||
return [
|
return [
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, state, child) {
|
builder: (context, state, child) {
|
||||||
// Инициализация ProfileAvatarCubit при первом использовании ShellRoute
|
|
||||||
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
|
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
|
||||||
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// Вызываем getMyProfile() только если состояние loading (первый запуск)
|
profileAvatarCubit.ensureFreshForCurrentSession();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
});
|
||||||
profileAvatarCubit.getMyProfile();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
|
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
|
||||||
},
|
},
|
||||||
|
|||||||
122
lib/core/session/session_lifecycle_service.dart
Normal file
122
lib/core/session/session_lifecycle_service.dart
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
|
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
|
||||||
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||||
|
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||||
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
enum SessionEndReason { logout, unauthorized, accountDeleted }
|
||||||
|
|
||||||
|
class SessionLifecycleService {
|
||||||
|
SessionLifecycleService({
|
||||||
|
required WsManager wsManager,
|
||||||
|
required ErrorReporter errorReporter,
|
||||||
|
}) : _wsManager = wsManager,
|
||||||
|
_errorReporter = errorReporter;
|
||||||
|
|
||||||
|
final WsManager _wsManager;
|
||||||
|
final ErrorReporter _errorReporter;
|
||||||
|
|
||||||
|
int? _authenticatedFingerprint;
|
||||||
|
bool _endingSession = false;
|
||||||
|
|
||||||
|
Future<void> endSession({required SessionEndReason reason}) async {
|
||||||
|
if (_endingSession) return;
|
||||||
|
_endingSession = true;
|
||||||
|
try {
|
||||||
|
await _run(() => _wsManager.clear());
|
||||||
|
await _run(TokenStorage.removeTokenInStorage);
|
||||||
|
await _run(SocketIdStorage.removeSocketIdInStorage);
|
||||||
|
await _run(() => sl<ProfileAvatarCubit>().reset());
|
||||||
|
await _run(() => sl<ChatsCubit>().reset());
|
||||||
|
await _run(() => sl<NotificationsCubit>().reset());
|
||||||
|
await _run(() => sl<NewMessagesCubit>().reset());
|
||||||
|
await _run(() => sl<FeedCubit>().reset());
|
||||||
|
await _run(() async => ProfileCubit.clearMyProfileReference());
|
||||||
|
await _run(() => sl<VideoCallSessionCubit>().endCall());
|
||||||
|
await _run(() async => AppMetrica.setUserProfileID(null));
|
||||||
|
await _run(
|
||||||
|
() => sl<Prefs>().remove(PrefsKeys.isEmailVerificationSkipped),
|
||||||
|
);
|
||||||
|
await _run(() => sl<Prefs>().remove(PrefsKeys.registrationDate));
|
||||||
|
_authenticatedFingerprint = null;
|
||||||
|
await _run(() async {
|
||||||
|
final context = AppRouter.navigatorKey.currentContext;
|
||||||
|
if (context == null) return;
|
||||||
|
final path =
|
||||||
|
AppRouter.appRouter.routeInformationProvider.value.uri.path;
|
||||||
|
if (path != RouteConstants.authScreen) {
|
||||||
|
context.go(RouteConstants.authScreen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
_endingSession = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onAuthenticated() async {
|
||||||
|
String? token;
|
||||||
|
try {
|
||||||
|
token = await TokenStorage.getTokenFromStorage();
|
||||||
|
} catch (e, st) {
|
||||||
|
await _report(e, st);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (token == null || token.isEmpty) return;
|
||||||
|
final fingerprint = token.hashCode;
|
||||||
|
if (_authenticatedFingerprint == fingerprint) return;
|
||||||
|
_authenticatedFingerprint = fingerprint;
|
||||||
|
|
||||||
|
await _run(() async {
|
||||||
|
await _wsManager.clear();
|
||||||
|
await _wsManager.initialize();
|
||||||
|
});
|
||||||
|
await _run(() => sl<ProfileAvatarCubit>().getMyProfile());
|
||||||
|
await _run(() => sl<ChatsCubit>().getChatRoomsWithLoading());
|
||||||
|
await _run(() => sl<NotificationsCubit>().getNotifications());
|
||||||
|
await _run(() => sl<FirebaseApi>().registerPushToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
try {
|
||||||
|
await _run(() => sl<FirebaseApi>().unregisterPushTokens());
|
||||||
|
await _run(
|
||||||
|
() => sl<Dio>().post('${sl<AppConfig>().baseUrl}/api/profile/logout'),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await endSession(reason: SessionEndReason.logout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _run(FutureOr<void> Function() action) async {
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
} catch (e, st) {
|
||||||
|
await _report(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _report(Object error, StackTrace stackTrace) {
|
||||||
|
return _errorReporter.report(
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
origin: 'SessionLifecycleService',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
|||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
|
||||||
@@ -17,15 +18,26 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
final ProfileRepository profileRepository;
|
final ProfileRepository profileRepository;
|
||||||
final FirebaseApi firebaseApi;
|
final FirebaseApi firebaseApi;
|
||||||
final Prefs prefs;
|
final Prefs prefs;
|
||||||
|
final SessionLifecycleService sessionLifecycleService;
|
||||||
|
|
||||||
AuthCubit({required this.authRepository, required this.prefs, required this.profileRepository, required this.firebaseApi}) : super(const AuthState());
|
AuthCubit({
|
||||||
|
required this.authRepository,
|
||||||
|
required this.prefs,
|
||||||
|
required this.profileRepository,
|
||||||
|
required this.firebaseApi,
|
||||||
|
required this.sessionLifecycleService,
|
||||||
|
}) : super(const AuthState());
|
||||||
|
|
||||||
Future<bool> getTokenByCredentials(String email, String password) async {
|
Future<bool> getTokenByCredentials(String email, String password) async {
|
||||||
emit(const AuthState.loading());
|
emit(const AuthState.loading());
|
||||||
try {
|
try {
|
||||||
final token = (await authRepository.getTokenByCredentials(email, password)).token;
|
final token = (await authRepository.getTokenByCredentials(
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
)).token;
|
||||||
emit(AuthState.loaded(token: token));
|
emit(AuthState.loaded(token: token));
|
||||||
await TokenStorage.saveTokenInStorage(token);
|
await TokenStorage.saveTokenInStorage(token);
|
||||||
|
await sessionLifecycleService.onAuthenticated();
|
||||||
return true;
|
return true;
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
@@ -48,7 +60,8 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
|
|
||||||
Future<void> registerAnonymousPushToken() async {
|
Future<void> registerAnonymousPushToken() async {
|
||||||
try {
|
try {
|
||||||
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
final hasAnonymousTokenBeenSent =
|
||||||
|
prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
||||||
if (!hasAnonymousTokenBeenSent) {
|
if (!hasAnonymousTokenBeenSent) {
|
||||||
await firebaseApi.registerAnonymousPushToken();
|
await firebaseApi.registerAnonymousPushToken();
|
||||||
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
|
||||||
@@ -11,12 +13,24 @@ import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
|||||||
|
|
||||||
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
||||||
final AuthRepository authRepository;
|
final AuthRepository authRepository;
|
||||||
|
final FirebaseApi firebaseApi;
|
||||||
|
final SessionLifecycleService sessionLifecycleService;
|
||||||
|
|
||||||
OAuthCubit({required this.authRepository, required Prefs prefs}) : super(const OAuthState()) {
|
OAuthCubit({
|
||||||
|
required this.authRepository,
|
||||||
|
required this.firebaseApi,
|
||||||
|
required this.sessionLifecycleService,
|
||||||
|
required Prefs prefs,
|
||||||
|
}) : super(const OAuthState()) {
|
||||||
this.prefs = prefs;
|
this.prefs = prefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
|
Future<void> signInViaSocialMedia({
|
||||||
|
required String provider,
|
||||||
|
required String accessToken,
|
||||||
|
required String refreshToken,
|
||||||
|
required String clientId,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
emit(const OAuthState.loading());
|
emit(const OAuthState.loading());
|
||||||
|
|
||||||
@@ -28,7 +42,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
);
|
);
|
||||||
|
|
||||||
log('OAuthCubit: Got token model: ${tokenModel.token}');
|
log('OAuthCubit: Got token model: ${tokenModel.token}');
|
||||||
TokenStorage.saveTokenInStorage(tokenModel.token);
|
await TokenStorage.saveTokenInStorage(tokenModel.token);
|
||||||
|
await sessionLifecycleService.onAuthenticated();
|
||||||
emit(OAuthState.loaded(token: tokenModel.token));
|
emit(OAuthState.loaded(token: tokenModel.token));
|
||||||
} on DioException catch (e, st) {
|
} on DioException catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
@@ -45,7 +60,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
if (errorStr.contains('Некорректный формат email')) {
|
if (errorStr.contains('Некорректный формат email')) {
|
||||||
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||||
}
|
}
|
||||||
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
|
if (errorStr.contains('почта уже существует') ||
|
||||||
|
errorStr.contains('Эл. почта уже существует')) {
|
||||||
errorsList.add(RegistrationValidationErrors.emailExist);
|
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,14 +69,20 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
||||||
for (final error in phoneErrors) {
|
for (final error in phoneErrors) {
|
||||||
final errorStr = error.toString();
|
final errorStr = error.toString();
|
||||||
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
|
if (errorStr.contains('phone уже существует') ||
|
||||||
|
errorStr.contains('телефон уже существует')) {
|
||||||
errorsList.add(RegistrationValidationErrors.phoneExist);
|
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorsList.isNotEmpty) {
|
if (errorsList.isNotEmpty) {
|
||||||
emit(OAuthState.error(errorMessage: 'validation_error', registrationValidationErrors: errorsList));
|
emit(
|
||||||
|
OAuthState.error(
|
||||||
|
errorMessage: 'validation_error',
|
||||||
|
registrationValidationErrors: errorsList,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
emit(OAuthState.error(errorMessage: e.toString()));
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
}
|
}
|
||||||
@@ -103,7 +125,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
height: height,
|
height: height,
|
||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
);
|
);
|
||||||
TokenStorage.saveTokenInStorage(token);
|
await TokenStorage.saveTokenInStorage(token);
|
||||||
|
await sessionLifecycleService.onAuthenticated();
|
||||||
cacheRegistrationDate(DateTime.now());
|
cacheRegistrationDate(DateTime.now());
|
||||||
emit(OAuthState.loaded(token: token));
|
emit(OAuthState.loaded(token: token));
|
||||||
} on DioException catch (e, st) {
|
} on DioException catch (e, st) {
|
||||||
@@ -120,7 +143,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
if (errorStr.contains('Некорректный формат email')) {
|
if (errorStr.contains('Некорректный формат email')) {
|
||||||
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||||
}
|
}
|
||||||
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
|
if (errorStr.contains('почта уже существует') ||
|
||||||
|
errorStr.contains('Эл. почта уже существует')) {
|
||||||
errorsList.add(RegistrationValidationErrors.emailExist);
|
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,14 +152,17 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
||||||
for (final error in phoneErrors) {
|
for (final error in phoneErrors) {
|
||||||
final errorStr = error.toString();
|
final errorStr = error.toString();
|
||||||
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
|
if (errorStr.contains('phone уже существует') ||
|
||||||
|
errorStr.contains('телефон уже существует')) {
|
||||||
errorsList.add(RegistrationValidationErrors.phoneExist);
|
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
||||||
if (dateOfBirth.contains('Некорректный формат даты рождения')) {
|
if (dateOfBirth.contains('Некорректный формат даты рождения')) {
|
||||||
errorsList.add(RegistrationValidationErrors.incorrectDateOfBirthFormat);
|
errorsList.add(
|
||||||
|
RegistrationValidationErrors.incorrectDateOfBirthFormat,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errorsList.isNotEmpty) {
|
if (errorsList.isNotEmpty) {
|
||||||
@@ -146,13 +173,23 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
|
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({
|
||||||
|
String? email,
|
||||||
|
String? phone,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
emit(const OAuthState.loading());
|
emit(const OAuthState.loading());
|
||||||
log('OAuthCubit: Requesting verification code for email: $email, phone: $phone');
|
log(
|
||||||
final response = await authRepository.requestOAuthVerification(email: email, phone: phone);
|
'OAuthCubit: Requesting verification code for email: $email, phone: $phone',
|
||||||
|
);
|
||||||
|
final response = await authRepository.requestOAuthVerification(
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}');
|
log(
|
||||||
|
'OAuthCubit: Verification code requested successfully, masked email: ${response.email}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Сбрасываем состояние после успешного запроса кода
|
// Сбрасываем состояние после успешного запроса кода
|
||||||
emit(const OAuthState());
|
emit(const OAuthState());
|
||||||
@@ -184,7 +221,9 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
emit(const OAuthState.loading());
|
emit(const OAuthState.loading());
|
||||||
log('OAuthCubit: Confirming OAuth verification - provider: $provider, verificationType: $verificationType, code: $code');
|
log(
|
||||||
|
'OAuthCubit: Confirming OAuth verification - provider: $provider, verificationType: $verificationType, code: $code',
|
||||||
|
);
|
||||||
final response = await authRepository.confirmOAuthVerification(
|
final response = await authRepository.confirmOAuthVerification(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
@@ -202,8 +241,11 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
weight: weight,
|
weight: weight,
|
||||||
height: height,
|
height: height,
|
||||||
);
|
);
|
||||||
log('OAuthCubit: OAuth verification confirmed successfully, token received');
|
log(
|
||||||
TokenStorage.saveTokenInStorage(response.token);
|
'OAuthCubit: OAuth verification confirmed successfully, token received',
|
||||||
|
);
|
||||||
|
await TokenStorage.saveTokenInStorage(response.token);
|
||||||
|
await sessionLifecycleService.onAuthenticated();
|
||||||
emit(OAuthState.loaded(token: response.token));
|
emit(OAuthState.loaded(token: response.token));
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
@@ -212,4 +254,3 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import 'package:eatvio_app/app_keys.dart';
|
|||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
import 'package:eatvio_app/core/const.dart';
|
import 'package:eatvio_app/core/const.dart';
|
||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
|
||||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
||||||
@@ -18,8 +16,6 @@ import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cub
|
|||||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
|
||||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||||
import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
|
||||||
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
@@ -68,23 +64,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _reconnectWebSocket(BuildContext context) async {
|
Future<void> _reconnectWebSocket() async {
|
||||||
try {
|
showSuccessToast('Успешный вход');
|
||||||
await sl<WsManager>().clear();
|
|
||||||
await sl<WsManager>().initialize();
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
if (context.mounted) {
|
|
||||||
try {
|
|
||||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
|
||||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
|
||||||
await sl<FirebaseApi>().registerPushToken();
|
|
||||||
showSuccessToast('Успешный вход');
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,18 +183,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _reconnectAfterAuth() async {
|
Future<void> _reconnectAfterAuth() async {
|
||||||
final context = AppRouter.navigatorKey.currentContext;
|
showSuccessToast('Успешный вход');
|
||||||
if (context == null) return;
|
|
||||||
try {
|
|
||||||
await sl<WsManager>().clear();
|
|
||||||
await sl<WsManager>().initialize();
|
|
||||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
|
||||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
|
||||||
await sl<FirebaseApi>().registerPushToken();
|
|
||||||
showSuccessToast('Успешный вход');
|
|
||||||
} catch (e) {
|
|
||||||
log(e.toString());
|
|
||||||
}
|
|
||||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +365,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
|
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await _reconnectWebSocket(context);
|
await _reconnectWebSocket();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_sta
|
|||||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
|
||||||
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
|
||||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||||
import 'package:eatvio_app/core/utils/validation_utils.dart';
|
import 'package:eatvio_app/core/utils/validation_utils.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@@ -62,23 +58,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
_initializeFieldsFromAuthResult();
|
_initializeFieldsFromAuthResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _reconnectWebSocket(BuildContext context) async {
|
Future<void> _reconnectWebSocket() async {
|
||||||
try {
|
showSuccessToast('Успешная регистрация');
|
||||||
await sl<WsManager>().clear();
|
|
||||||
await sl<WsManager>().initialize();
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
if (context.mounted) {
|
|
||||||
try {
|
|
||||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
|
||||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
|
||||||
await sl<FirebaseApi>().registerPushToken();
|
|
||||||
showSuccessToast('Успешная регистрация');
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppRouter.appRouter.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
AppRouter.appRouter.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +432,7 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
Builder(
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_reconnectWebSocket(context);
|
_reconnectWebSocket();
|
||||||
});
|
});
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
@@ -795,7 +776,7 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
|
|
||||||
// Перезагружаем веб-сокет и переходим на главный экран
|
// Перезагружаем веб-сокет и переходим на главный экран
|
||||||
if (widgetContext.mounted) {
|
if (widgetContext.mounted) {
|
||||||
_reconnectWebSocket(widgetContext);
|
_reconnectWebSocket();
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
log('OAuth Verification Confirm Error: $e', error: e, stackTrace: stackTrace);
|
log('OAuth Verification Confirm Error: $e', error: e, stackTrace: stackTrace);
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import 'dart:io';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||||
@@ -39,7 +36,10 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Сканирование QR-кода'), centerTitle: true),
|
appBar: AppBar(
|
||||||
|
title: const Text('Сканирование QR-кода'),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -60,30 +60,33 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
|
|||||||
try {
|
try {
|
||||||
final scannedUrl = scanData.code;
|
final scannedUrl = scanData.code;
|
||||||
final uri = Uri.parse(scannedUrl ?? '');
|
final uri = Uri.parse(scannedUrl ?? '');
|
||||||
final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : '';
|
final scannedBaseUrl = (uri.hasAuthority)
|
||||||
|
? '${uri.scheme}://${uri.host}'
|
||||||
|
: '';
|
||||||
sl<AppConfig>().setBaseUrl(scannedBaseUrl);
|
sl<AppConfig>().setBaseUrl(scannedBaseUrl);
|
||||||
sl<Dio>().options.baseUrl = scannedBaseUrl;
|
sl<Dio>().options.baseUrl = scannedBaseUrl;
|
||||||
final response = await Dio().getUri(uri, options: Options(headers: {'accept': 'application/json', 'content-type': 'application/json'}));
|
final response = await Dio().getUri(
|
||||||
|
uri,
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
final token = response.data['token'];
|
final token = response.data['token'];
|
||||||
await TokenStorage.saveTokenInStorage(token);
|
await TokenStorage.saveTokenInStorage(token);
|
||||||
try {
|
await sl<SessionLifecycleService>().onAuthenticated();
|
||||||
await sl<WsManager>().clear();
|
AppRouter.navigatorKey.currentContext?.go(
|
||||||
await sl<WsManager>().initialize();
|
'/feed-screen',
|
||||||
} catch (e) {
|
extra: {'needToInitializeWebSocket': false},
|
||||||
rethrow;
|
);
|
||||||
}
|
|
||||||
try {
|
|
||||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
|
||||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
|
||||||
await sl<FirebaseApi>().registerPushToken();
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
|
||||||
showSuccessToast('Добро пожаловать!');
|
showSuccessToast('Добро пожаловать!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await controller.resumeCamera();
|
await controller.resumeCamera();
|
||||||
showErrorToast('Что-то пошло не так. Попробуйте обновить страницу с QR-кодом или войдите с помощью логина и пароля.');
|
showErrorToast(
|
||||||
|
'Что-то пошло не так. Попробуйте обновить страницу с QR-кодом или войдите с помощью логина и пароля.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ class NewMessagesCubit extends Cubit<NewMessagesState> {
|
|||||||
|
|
||||||
NewMessagesCubit() : super(const NewMessagesState.empty());
|
NewMessagesCubit() : super(const NewMessagesState.empty());
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_messagesCount = 0;
|
||||||
|
if (!isClosed) {
|
||||||
|
emit(const NewMessagesState.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void incrementCounter() {
|
void incrementCounter() {
|
||||||
_messagesCount++;
|
_messagesCount++;
|
||||||
emit(NewMessagesState.loaded(messagesCount: _messagesCount));
|
emit(NewMessagesState.loaded(messagesCount: _messagesCount));
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
|
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
|
class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
|
||||||
final ProfileRepository profileRepository;
|
final ProfileRepository profileRepository;
|
||||||
|
|
||||||
ProfileAvatarCubit({required this.profileRepository}) : super(const ProfileAvatarState.loading());
|
ProfileAvatarCubit({required this.profileRepository})
|
||||||
|
: super(const ProfileAvatarState.loading());
|
||||||
|
|
||||||
|
int _requestId = 0;
|
||||||
|
int? _sessionFingerprint;
|
||||||
|
bool _ensuring = false;
|
||||||
|
|
||||||
int _calculateAge(String birthdate) {
|
int _calculateAge(String birthdate) {
|
||||||
try {
|
try {
|
||||||
@@ -25,7 +29,8 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
|
|||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
int age = now.year - birthYear;
|
int age = now.year - birthYear;
|
||||||
|
|
||||||
if (now.month < birthMonth || (now.month == birthMonth && now.day < birthDay)) {
|
if (now.month < birthMonth ||
|
||||||
|
(now.month == birthMonth && now.day < birthDay)) {
|
||||||
age--;
|
age--;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,30 +43,82 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_requestId++;
|
||||||
|
_sessionFingerprint = null;
|
||||||
|
unawaited(AppMetrica.setUserProfileID(null).catchError((Object _) {}));
|
||||||
|
if (!isClosed) emit(const ProfileAvatarState.loading());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> ensureFreshForCurrentSession() async {
|
||||||
|
final token = await TokenStorage.getTokenFromStorage();
|
||||||
|
if (token == null || token.isEmpty) {
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fingerprint = token.hashCode;
|
||||||
|
if (_sessionFingerprint == fingerprint &&
|
||||||
|
state is ProfileAvatarStateLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ensuring) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensuring = true;
|
||||||
|
try {
|
||||||
|
await getMyProfile();
|
||||||
|
} finally {
|
||||||
|
_ensuring = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> getMyProfile() async {
|
Future<void> getMyProfile() async {
|
||||||
|
final requestId = ++_requestId;
|
||||||
|
final token = await TokenStorage.getTokenFromStorage();
|
||||||
|
final fingerprint = token?.hashCode;
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
|
|
||||||
emit(const ProfileAvatarState.loading());
|
emit(const ProfileAvatarState.loading());
|
||||||
try {
|
try {
|
||||||
final profile = await profileRepository.getMyProfile();
|
final profile = await profileRepository.getMyProfile();
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
|
|
||||||
final profileDetails = await profileRepository.getMyProfileDetails();
|
final profileDetails = await profileRepository.getMyProfileDetails();
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
|
|
||||||
|
_sessionFingerprint = fingerprint;
|
||||||
emit(ProfileAvatarState.loaded(profile: profile));
|
emit(ProfileAvatarState.loaded(profile: profile));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final notificationStatus = await Permission.notification.status;
|
final notificationStatus = await Permission.notification.status;
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
final isNotificationEnabled = notificationStatus.isGranted;
|
final isNotificationEnabled = notificationStatus.isGranted;
|
||||||
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
|
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
|
||||||
AppMetricaNameAttribute.withValue(profile.name),
|
AppMetricaNameAttribute.withValue(profile.name),
|
||||||
AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
|
AppMetricaGenderAttribute.withValue(
|
||||||
AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
|
profileDetails.gender == 'male'
|
||||||
AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
|
? AppMetricaGender.male
|
||||||
|
: AppMetricaGender.female,
|
||||||
|
),
|
||||||
|
AppMetricaBirthDateAttribute.withAge(
|
||||||
|
_calculateAge(profileDetails.birthdate),
|
||||||
|
),
|
||||||
|
AppMetricaNotificationEnabledAttribute.withValue(
|
||||||
|
isNotificationEnabled,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
AppMetrica.setUserProfileID(profile.slug);
|
AppMetrica.setUserProfileID(profile.slug);
|
||||||
AppMetrica.reportUserProfile(userProfile);
|
AppMetrica.reportUserProfile(userProfile);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
// Игнорируем ошибки плагинов, профиль уже загружен
|
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
if (isClosed || requestId != _requestId) return;
|
||||||
emit(ProfileAvatarState.error(errorMessage: e.toString()));
|
emit(ProfileAvatarState.error(errorMessage: e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class BottomNavigationBarWidget extends StatefulWidget {
|
|||||||
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||||
int currentBottomNavBarIndex = 1;
|
int currentBottomNavBarIndex = 1;
|
||||||
List<String>? pages;
|
List<String>? pages;
|
||||||
|
int? _pagesProfileId;
|
||||||
|
bool? _pagesIsConsultant;
|
||||||
int tapsCount = 0;
|
int tapsCount = 0;
|
||||||
|
|
||||||
List<String> _getPages(bool isConsultant) {
|
List<String> _getPages(bool isConsultant) {
|
||||||
@@ -179,7 +181,16 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
final isConsultant = profileModel.roles.contains('consultant');
|
final isConsultant = profileModel.roles.contains('consultant');
|
||||||
|
|
||||||
// Инициализируем pages на основе profileModel
|
// Инициализируем pages на основе profileModel
|
||||||
pages ??= _getPages(isConsultant);
|
if (pages == null ||
|
||||||
|
_pagesProfileId != profileModel.id ||
|
||||||
|
_pagesIsConsultant != isConsultant) {
|
||||||
|
_pagesProfileId = profileModel.id;
|
||||||
|
_pagesIsConsultant = isConsultant;
|
||||||
|
pages = _getPages(isConsultant);
|
||||||
|
if (currentBottomNavBarIndex >= pages!.length) {
|
||||||
|
currentBottomNavBarIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Expose tab switching to descendants (e.g. notification widget)
|
// Expose tab switching to descendants (e.g. notification widget)
|
||||||
final controller = BottomNavScope.maybeOf(context);
|
final controller = BottomNavScope.maybeOf(context);
|
||||||
|
|||||||
@@ -57,6 +57,17 @@ class ChatsCubit extends Cubit<ChatsState> {
|
|||||||
int page = 1;
|
int page = 1;
|
||||||
late int lastPage;
|
late int lastPage;
|
||||||
|
|
||||||
|
/// Clears all user-scoped state. WS subscriptions stay intact so that
|
||||||
|
/// `getChatRoomsWithLoading` can re-load the new user's chats.
|
||||||
|
void reset() {
|
||||||
|
_query = null;
|
||||||
|
page = 1;
|
||||||
|
lastPage = 1;
|
||||||
|
if (!isClosed) {
|
||||||
|
emit(ChatsState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> connectToWebSocket() async {
|
Future<void> connectToWebSocket() async {
|
||||||
try {
|
try {
|
||||||
await newIncomingMessagesSubscription?.cancel();
|
await newIncomingMessagesSubscription?.cancel();
|
||||||
|
|||||||
@@ -12,9 +12,19 @@ class FeedCubit extends Cubit<FeedState> {
|
|||||||
bool _hasMore = true;
|
bool _hasMore = true;
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
String? _query;
|
String? _query;
|
||||||
|
int _requestId = 0;
|
||||||
|
|
||||||
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
|
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_requestId++;
|
||||||
|
_nextCursor = null;
|
||||||
|
_hasMore = true;
|
||||||
|
isLoading = false;
|
||||||
|
_query = null;
|
||||||
|
if (!isClosed) emit(const FeedState.loading());
|
||||||
|
}
|
||||||
|
|
||||||
void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
|
void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
|
||||||
if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return;
|
if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return;
|
||||||
final List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
|
final List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
|
||||||
@@ -39,17 +49,18 @@ class FeedCubit extends Cubit<FeedState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadFeedBlogPostList({String? query, int? currentUserId}) async {
|
Future<void> loadFeedBlogPostList({String? query, int? currentUserId}) async {
|
||||||
|
final requestId = ++_requestId;
|
||||||
try {
|
try {
|
||||||
_query = query;
|
_query = query;
|
||||||
_nextCursor = null;
|
_nextCursor = null;
|
||||||
_hasMore = true;
|
_hasMore = true;
|
||||||
final feed = await feedRepository.getFeed(search: query, currentUserId: currentUserId);
|
final feed = await feedRepository.getFeed(search: query, currentUserId: currentUserId);
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
_nextCursor = feed.nextCursor;
|
_nextCursor = feed.nextCursor;
|
||||||
_hasMore = _nextCursor != null;
|
_hasMore = _nextCursor != null;
|
||||||
if (!isClosed) {
|
emit(FeedState.loaded(feedBlogPostList: feed.posts, isLoading: false));
|
||||||
emit(FeedState.loaded(feedBlogPostList: feed.posts, isLoading: false));
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
emit(FeedState.error(errorMessage: e.toString()));
|
emit(FeedState.error(errorMessage: e.toString()));
|
||||||
@@ -62,14 +73,16 @@ class FeedCubit extends Cubit<FeedState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final requestId = _requestId;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
emit((state as FeedStateLoaded).copyWith(isLoading: true));
|
emit((state as FeedStateLoaded).copyWith(isLoading: true));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final feed = await feedRepository.getFeed(search: _query, cursor: _nextCursor, currentUserId: currentUserId);
|
final feed = await feedRepository.getFeed(search: _query, cursor: _nextCursor, currentUserId: currentUserId);
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
_nextCursor = feed.nextCursor;
|
_nextCursor = feed.nextCursor;
|
||||||
_hasMore = _nextCursor != null;
|
_hasMore = _nextCursor != null;
|
||||||
if (!isClosed && state is FeedStateLoaded) {
|
if (state is FeedStateLoaded) {
|
||||||
emit(
|
emit(
|
||||||
FeedState.loaded(
|
FeedState.loaded(
|
||||||
feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feed.posts],
|
feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feed.posts],
|
||||||
@@ -78,14 +91,15 @@ class FeedCubit extends Cubit<FeedState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
|
if (isClosed || requestId != _requestId) return;
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
if (!isClosed && state is FeedStateLoaded) {
|
if (state is FeedStateLoaded) {
|
||||||
emit((state as FeedStateLoaded).copyWith(isLoading: false));
|
emit((state as FeedStateLoaded).copyWith(isLoading: false));
|
||||||
} else if (!isClosed) {
|
} else {
|
||||||
emit(FeedState.error(errorMessage: e.toString()));
|
emit(FeedState.error(errorMessage: e.toString()));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
if (requestId == _requestId) isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart';
|
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart';
|
||||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -7,7 +8,11 @@ import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
|||||||
|
|
||||||
class MenuCubit extends Cubit<MenuState> {
|
class MenuCubit extends Cubit<MenuState> {
|
||||||
final ProfileRepository profileRepository;
|
final ProfileRepository profileRepository;
|
||||||
MenuCubit(this.profileRepository) : super(const MenuState.initial());
|
final SessionLifecycleService sessionLifecycleService;
|
||||||
|
MenuCubit(this.profileRepository, this.sessionLifecycleService)
|
||||||
|
: super(const MenuState.initial());
|
||||||
|
|
||||||
|
Future<void> logout() => sessionLifecycleService.logout();
|
||||||
|
|
||||||
Future<void> getMyProfile() async {
|
Future<void> getMyProfile() async {
|
||||||
emit(const MenuState.loading());
|
emit(const MenuState.loading());
|
||||||
@@ -23,7 +28,10 @@ class MenuCubit extends Cubit<MenuState> {
|
|||||||
|
|
||||||
Future<String?> makePassKey() async {
|
Future<String?> makePassKey() async {
|
||||||
try {
|
try {
|
||||||
final passKey = await profileRepository.getPassKey(routeName: 'product_nutritional_value.index', routeParams: {});
|
final passKey = await profileRepository.getPassKey(
|
||||||
|
routeName: 'product_nutritional_value.index',
|
||||||
|
routeParams: {},
|
||||||
|
);
|
||||||
return passKey;
|
return passKey;
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
|
||||||
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
|
|
||||||
import 'package:eatvio_app/core/const.dart';
|
import 'package:eatvio_app/core/const.dart';
|
||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
|
||||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
|
||||||
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
|
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
|
||||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
||||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
|
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
|
||||||
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart';
|
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart';
|
||||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
|
||||||
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart';
|
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart';
|
||||||
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
||||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
||||||
@@ -155,6 +148,7 @@ class MenuScreen extends StatelessWidget {
|
|||||||
|
|
||||||
Future<Null> Function() _onTapLogout(BuildContext context) {
|
Future<Null> Function() _onTapLogout(BuildContext context) {
|
||||||
return () async {
|
return () async {
|
||||||
|
final menuCubit = context.read<MenuCubit>();
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => CupertinoAlertDialog(
|
builder: (context) => CupertinoAlertDialog(
|
||||||
@@ -162,25 +156,8 @@ class MenuScreen extends StatelessWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
Navigator.of(context).pop();
|
||||||
// Logout revokes the Sanctum token. Unregister first, or
|
await menuCubit.logout();
|
||||||
// FCM / VoIP tokens stay on the server and calls still ring.
|
|
||||||
await sl<FirebaseApi>().unregisterPushTokens();
|
|
||||||
} catch (e, s) {
|
|
||||||
await sl<ErrorReporter>().report(e, s, origin: 'MenuScreen');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await sl<Dio>().post('${sl<AppConfig>().baseUrl}/api/profile/logout');
|
|
||||||
} catch (e, s) {
|
|
||||||
await sl<ErrorReporter>().report(e, s, origin: 'MenuScreen');
|
|
||||||
} finally {
|
|
||||||
await sl<VideoCallSessionCubit>().endCall();
|
|
||||||
await sl<WsManager>().clear();
|
|
||||||
await TokenStorage.removeTokenInStorage();
|
|
||||||
if (context.mounted) {
|
|
||||||
GoRouter.of(context).go('/auth-screen', extra: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
isDestructiveAction: true,
|
isDestructiveAction: true,
|
||||||
child: const Text("Выйти"),
|
child: const Text("Выйти"),
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
|||||||
StreamSubscription? _newNotificationsSubscription;
|
StreamSubscription? _newNotificationsSubscription;
|
||||||
NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty());
|
NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty());
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_notifications.clear();
|
||||||
|
if (!isClosed) emit(const NotificationsState.empty());
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> connectToWebSocket() async {
|
Future<void> connectToWebSocket() async {
|
||||||
try {
|
try {
|
||||||
await _newNotificationsSubscription?.cancel();
|
await _newNotificationsSubscription?.cancel();
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class ProfileCubit extends Cubit<ProfileState> {
|
|||||||
/// Bottom-nav / own profile instance, used to refresh posts after create/update/delete.
|
/// Bottom-nav / own profile instance, used to refresh posts after create/update/delete.
|
||||||
static ProfileCubit? myProfileCubit;
|
static ProfileCubit? myProfileCubit;
|
||||||
|
|
||||||
|
static void clearMyProfileReference() {
|
||||||
|
myProfileCubit = null;
|
||||||
|
}
|
||||||
|
|
||||||
ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading());
|
ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading());
|
||||||
|
|
||||||
Future<void> getProfileBySlug(String slug) async {
|
Future<void> getProfileBySlug(String slug) async {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
|
||||||
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
|
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
|
||||||
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
|
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
|
||||||
@@ -18,7 +16,8 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
class RegistrationCubit extends Cubit<RegistrationState> with RegistrationDateCacheMixin {
|
class RegistrationCubit extends Cubit<RegistrationState> with RegistrationDateCacheMixin {
|
||||||
final RegistrationRepository registrationRepository;
|
final RegistrationRepository registrationRepository;
|
||||||
RegistrationCubit({required this.registrationRepository, required Prefs prefs})
|
final SessionLifecycleService sessionLifecycleService;
|
||||||
|
RegistrationCubit({required this.registrationRepository, required this.sessionLifecycleService, required Prefs prefs})
|
||||||
: super(
|
: super(
|
||||||
const RegistrationState(
|
const RegistrationState(
|
||||||
sheet: UserRegistrationSheet(
|
sheet: UserRegistrationSheet(
|
||||||
@@ -62,7 +61,7 @@ class RegistrationCubit extends Cubit<RegistrationState> with RegistrationDateCa
|
|||||||
state.sheet.copyWith(phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), responseBody: '{}'),
|
state.sheet.copyWith(phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), responseBody: '{}'),
|
||||||
);
|
);
|
||||||
await TokenStorage.saveTokenInStorage(token);
|
await TokenStorage.saveTokenInStorage(token);
|
||||||
await sl<FirebaseApi>().registerPushToken();
|
await sessionLifecycleService.onAuthenticated();
|
||||||
RegistrationAnalyticsService.reportRegistrationComplete();
|
RegistrationAnalyticsService.reportRegistrationComplete();
|
||||||
cacheRegistrationDate(DateTime.now());
|
cacheRegistrationDate(DateTime.now());
|
||||||
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
|
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||||
import 'package:eatvio_app/core/utils/validation_utils.dart';
|
import 'package:eatvio_app/core/utils/validation_utils.dart';
|
||||||
@@ -13,7 +15,13 @@ import 'package:gpassword/gpassword.dart';
|
|||||||
|
|
||||||
class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
|
class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
|
||||||
final RegistrationRepositoryV2 registrationRepository;
|
final RegistrationRepositoryV2 registrationRepository;
|
||||||
RegistrationCubitV2({required this.registrationRepository}) : super(const RegistrationStateV2());
|
final FirebaseApi firebaseApi;
|
||||||
|
final SessionLifecycleService sessionLifecycleService;
|
||||||
|
RegistrationCubitV2({
|
||||||
|
required this.registrationRepository,
|
||||||
|
required this.firebaseApi,
|
||||||
|
required this.sessionLifecycleService,
|
||||||
|
}) : super(const RegistrationStateV2());
|
||||||
|
|
||||||
void setFirstName(String firstName) {
|
void setFirstName(String firstName) {
|
||||||
emit(state.copyWith(firstName: firstName));
|
emit(state.copyWith(firstName: firstName));
|
||||||
@@ -194,9 +202,12 @@ class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
|
|||||||
bool didApiComplainOnValidation = false;
|
bool didApiComplainOnValidation = false;
|
||||||
String errorMessage = '';
|
String errorMessage = '';
|
||||||
try {
|
try {
|
||||||
final token = await registrationRepository.register(registrationModel: registrationModel);
|
final token = await registrationRepository.register(
|
||||||
|
registrationModel: registrationModel,
|
||||||
|
);
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
TokenStorage.saveTokenInStorage(token);
|
await TokenStorage.saveTokenInStorage(token);
|
||||||
|
await sessionLifecycleService.onAuthenticated();
|
||||||
emit(state.copyWith(registrationError: null));
|
emit(state.copyWith(registrationError: null));
|
||||||
return (true, false);
|
return (true, false);
|
||||||
}
|
}
|
||||||
@@ -214,11 +225,21 @@ class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
|
|||||||
for (var error in emailErrors) {
|
for (var error in emailErrors) {
|
||||||
allErrors.add(error.toString());
|
allErrors.add(error.toString());
|
||||||
if (error.toString() == 'Некорректный формат email') {
|
if (error.toString() == 'Некорректный формат email') {
|
||||||
emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Некорректный формат email'));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isEmailValid: false,
|
||||||
|
emailDisplayError: 'Некорректный формат email',
|
||||||
|
),
|
||||||
|
);
|
||||||
didApiComplainOnValidation = true;
|
didApiComplainOnValidation = true;
|
||||||
}
|
}
|
||||||
if (error.toString() == 'Такой Эл. почта уже существует.') {
|
if (error.toString() == 'Такой Эл. почта уже существует.') {
|
||||||
emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Такая Эл. почта уже существует.'));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isEmailValid: false,
|
||||||
|
emailDisplayError: 'Такая Эл. почта уже существует.',
|
||||||
|
),
|
||||||
|
);
|
||||||
didApiComplainOnValidation = true;
|
didApiComplainOnValidation = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +248,12 @@ class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
|
|||||||
for (var error in phoneErrors) {
|
for (var error in phoneErrors) {
|
||||||
allErrors.add(error.toString());
|
allErrors.add(error.toString());
|
||||||
if (error.toString() == 'Такой phone уже существует.') {
|
if (error.toString() == 'Такой phone уже существует.') {
|
||||||
emit(state.copyWith(isPhoneValid: false, phoneDisplayError: 'Такой номер телефона уже существует.'));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isPhoneValid: false,
|
||||||
|
phoneDisplayError: 'Такой номер телефона уже существует.',
|
||||||
|
),
|
||||||
|
);
|
||||||
didApiComplainOnValidation = true;
|
didApiComplainOnValidation = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +262,12 @@ class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
|
|||||||
for (var error in dateOfBirth) {
|
for (var error in dateOfBirth) {
|
||||||
allErrors.add(error.toString());
|
allErrors.add(error.toString());
|
||||||
if (error.toString() == 'Некорректный формат даты рождения') {
|
if (error.toString() == 'Некорректный формат даты рождения') {
|
||||||
emit(state.copyWith(isBirthdayValid: false, dateOfBirthDisplayError: 'Некорректный формат даты рождения'));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isBirthdayValid: false,
|
||||||
|
dateOfBirthDisplayError: 'Некорректный формат даты рождения',
|
||||||
|
),
|
||||||
|
);
|
||||||
didApiComplainOnValidation = true;
|
didApiComplainOnValidation = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
||||||
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
||||||
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
||||||
@@ -17,12 +19,14 @@ class SettingsCubit extends Cubit<SettingsState> {
|
|||||||
final SettingsRepository settingsRepository;
|
final SettingsRepository settingsRepository;
|
||||||
final ConsultantRepository consultantRepository;
|
final ConsultantRepository consultantRepository;
|
||||||
final ConsultationsRepository consultationsRepository;
|
final ConsultationsRepository consultationsRepository;
|
||||||
|
final SessionLifecycleService sessionLifecycleService;
|
||||||
|
|
||||||
SettingsCubit({
|
SettingsCubit({
|
||||||
required this.profileRepository,
|
required this.profileRepository,
|
||||||
required this.settingsRepository,
|
required this.settingsRepository,
|
||||||
required this.consultantRepository,
|
required this.consultantRepository,
|
||||||
required this.consultationsRepository,
|
required this.consultationsRepository,
|
||||||
|
required this.sessionLifecycleService,
|
||||||
}) : super(const SettingsState.loading());
|
}) : super(const SettingsState.loading());
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
@@ -30,10 +34,12 @@ class SettingsCubit extends Cubit<SettingsState> {
|
|||||||
late final ConsultationRequestModel? consultantInfo;
|
late final ConsultationRequestModel? consultantInfo;
|
||||||
late final bool hasActiveConsultation;
|
late final bool hasActiveConsultation;
|
||||||
late final OAuthAccountsDataModel? oauthAccounts;
|
late final OAuthAccountsDataModel? oauthAccounts;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (profile.phoneConfirmationState.contains('confirmed')) {
|
if (profile.phoneConfirmationState.contains('confirmed')) {
|
||||||
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
|
consultantInfo = await consultantRepository.getMyRequestStatus(
|
||||||
|
profile.slug,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
consultantInfo = null;
|
consultantInfo = null;
|
||||||
}
|
}
|
||||||
@@ -41,14 +47,14 @@ class SettingsCubit extends Cubit<SettingsState> {
|
|||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
consultantInfo = null;
|
consultantInfo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
hasActiveConsultation = await _checkActiveConsultation();
|
hasActiveConsultation = await _checkActiveConsultation();
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
hasActiveConsultation = false;
|
hasActiveConsultation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final oauthResponse = await settingsRepository.getOAuthAccounts();
|
final oauthResponse = await settingsRepository.getOAuthAccounts();
|
||||||
oauthAccounts = oauthResponse.data;
|
oauthAccounts = oauthResponse.data;
|
||||||
@@ -56,7 +62,7 @@ class SettingsCubit extends Cubit<SettingsState> {
|
|||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
oauthAccounts = null;
|
oauthAccounts = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
emit(
|
emit(
|
||||||
SettingsState.loaded(
|
SettingsState.loaded(
|
||||||
@@ -74,9 +80,13 @@ class SettingsCubit extends Cubit<SettingsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async {
|
Future<void> updateProfileDetails({
|
||||||
|
required ProfileDetailsModel profileDetailsModel,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
await settingsRepository.updateProfileDetails(profileDetailsModel: profileDetailsModel);
|
await settingsRepository.updateProfileDetails(
|
||||||
|
profileDetailsModel: profileDetailsModel,
|
||||||
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -84,9 +94,13 @@ class SettingsCubit extends Cubit<SettingsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async {
|
Future<void> updateProfileSettings({
|
||||||
|
required ProfileSettingsModel profileSettingsModel,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
await settingsRepository.updateProfileSettings(profileSettingsModel: profileSettingsModel);
|
await settingsRepository.updateProfileSettings(
|
||||||
|
profileSettingsModel: profileSettingsModel,
|
||||||
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -97,9 +111,15 @@ class SettingsCubit extends Cubit<SettingsState> {
|
|||||||
Future<bool> deleteAccount() async {
|
Future<bool> deleteAccount() async {
|
||||||
try {
|
try {
|
||||||
await profileRepository.deleteAccount();
|
await profileRepository.deleteAccount();
|
||||||
|
await sessionLifecycleService.endSession(
|
||||||
|
reason: SessionEndReason.accountDeleted,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
|
if (e is DioException && e.response?.statusCode == 401) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,11 +155,14 @@ class SettingsCubit extends Cubit<SettingsState> {
|
|||||||
|
|
||||||
Future<bool> _checkActiveConsultation() async {
|
Future<bool> _checkActiveConsultation() async {
|
||||||
try {
|
try {
|
||||||
final userPurchases = await consultationsRepository.getUserPurchases(null, null);
|
final userPurchases = await consultationsRepository.getUserPurchases(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
final activeStates = ChosenPlanStateEnum.getActiveStates();
|
final activeStates = ChosenPlanStateEnum.getActiveStates();
|
||||||
|
|
||||||
return userPurchases.any((purchase) =>
|
return userPurchases.any(
|
||||||
activeStates.contains(purchase.chosenPlanState)
|
(purchase) => activeStates.contains(purchase.chosenPlanState),
|
||||||
);
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
|
|||||||
@@ -698,7 +698,6 @@ class _DeleteAccountBottomSheet extends State<DeleteAccountBottomSheet> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final result = await widget.cubit.deleteAccount();
|
final result = await widget.cubit.deleteAccount();
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AppRouter.navigatorKey.currentContext?.pop();
|
AppRouter.navigatorKey.currentContext?.pop();
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class VideoCallCallKitCoordinator {
|
|||||||
|
|
||||||
_PendingIncomingAccept? _pendingIncomingAccept;
|
_PendingIncomingAccept? _pendingIncomingAccept;
|
||||||
_AndroidAcceptResumeObserver? _androidResumeObserver;
|
_AndroidAcceptResumeObserver? _androidResumeObserver;
|
||||||
|
_AppResumeObserver? _iosResumeObserver;
|
||||||
bool _pendingAcceptDrainScheduled = false;
|
bool _pendingAcceptDrainScheduled = false;
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
@@ -86,6 +87,12 @@ class VideoCallCallKitCoordinator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
await _firebaseApi.registerPushToken();
|
await _firebaseApi.registerPushToken();
|
||||||
|
_iosResumeObserver = _AppResumeObserver(
|
||||||
|
onResumed: () {
|
||||||
|
unawaited(_drainPendingNativeAcceptedVoipCall());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
WidgetsBinding.instance.addObserver(_iosResumeObserver!);
|
||||||
await _drainPendingNativeAcceptedVoipCall();
|
await _drainPendingNativeAcceptedVoipCall();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_scheduleNativeVoipDiagnosticsDrain();
|
_scheduleNativeVoipDiagnosticsDrain();
|
||||||
@@ -257,6 +264,11 @@ class VideoCallCallKitCoordinator {
|
|||||||
WidgetsBinding.instance.removeObserver(observer);
|
WidgetsBinding.instance.removeObserver(observer);
|
||||||
_androidResumeObserver = null;
|
_androidResumeObserver = null;
|
||||||
}
|
}
|
||||||
|
final iosObserver = _iosResumeObserver;
|
||||||
|
if (iosObserver != null) {
|
||||||
|
WidgetsBinding.instance.removeObserver(iosObserver);
|
||||||
|
_iosResumeObserver = null;
|
||||||
|
}
|
||||||
unawaited(_eventSubscription?.cancel());
|
unawaited(_eventSubscription?.cancel());
|
||||||
_eventSubscription = null;
|
_eventSubscription = null;
|
||||||
_callKitParamsById.clear();
|
_callKitParamsById.clear();
|
||||||
@@ -1228,6 +1240,19 @@ class _PendingIncomingAccept {
|
|||||||
final bool? counterpartIsConsultant;
|
final bool? counterpartIsConsultant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AppResumeObserver with WidgetsBindingObserver {
|
||||||
|
_AppResumeObserver({required this.onResumed});
|
||||||
|
|
||||||
|
final VoidCallback onResumed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
onResumed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AndroidAcceptResumeObserver with WidgetsBindingObserver {
|
class _AndroidAcceptResumeObserver with WidgetsBindingObserver {
|
||||||
_AndroidAcceptResumeObserver({required this.onResumed});
|
_AndroidAcceptResumeObserver({required this.onResumed});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: eatvio_app
|
name: eatvio_app
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
version: 1.3.67+156
|
version: 1.3.69+158
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
118
test/core/interceptors/token_interceptor_session_test.dart
Normal file
118
test/core/interceptors/token_interceptor_session_test.dart
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../fixtures/doubles/session_doubles.dart';
|
||||||
|
|
||||||
|
class _RecordingErrorInterceptorHandler extends ErrorInterceptorHandler {
|
||||||
|
int nextCalls = 0;
|
||||||
|
DioException? nextError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void next(DioException error) {
|
||||||
|
nextCalls++;
|
||||||
|
nextError = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void resolve(Response<dynamic> response) {
|
||||||
|
throw UnimplementedError('resolve should not be called');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reject(
|
||||||
|
DioException error, [
|
||||||
|
bool callFollowingErrorInterceptor = false,
|
||||||
|
]) {
|
||||||
|
throw UnimplementedError('reject should not be called');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DioException _unauthorized(RequestOptions options) {
|
||||||
|
return DioException(
|
||||||
|
requestOptions: options,
|
||||||
|
response: Response<dynamic>(requestOptions: options, statusCode: 401),
|
||||||
|
type: DioExceptionType.badResponse,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
late FakeSessionLifecycleService sessionLifecycleService;
|
||||||
|
late TokenInterceptor interceptor;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
await sl.reset();
|
||||||
|
|
||||||
|
sessionLifecycleService = FakeSessionLifecycleService();
|
||||||
|
sl.registerSingleton<SessionLifecycleService>(sessionLifecycleService);
|
||||||
|
interceptor = TokenInterceptor();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await sl.reset();
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('TokenInterceptor.onError 401', () {
|
||||||
|
test('request without Authorization does not end the session', () async {
|
||||||
|
await TokenStorage.saveTokenInStorage('current-token');
|
||||||
|
|
||||||
|
final options = RequestOptions(path: '/api/profile/index');
|
||||||
|
final error = _unauthorized(options);
|
||||||
|
final handler = _RecordingErrorInterceptorHandler();
|
||||||
|
|
||||||
|
await interceptor.onError(error, handler);
|
||||||
|
|
||||||
|
expect(handler.nextCalls, 1);
|
||||||
|
expect(handler.nextError, same(error));
|
||||||
|
expect(sessionLifecycleService.endSessionCalls, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stale request token does not end the new session', () async {
|
||||||
|
await TokenStorage.saveTokenInStorage('new-token');
|
||||||
|
|
||||||
|
final options = RequestOptions(path: '/api/profile/index');
|
||||||
|
options.headers['Authorization'] = 'Bearer old-token';
|
||||||
|
options.extra['authToken'] = 'old-token';
|
||||||
|
|
||||||
|
final error = _unauthorized(options);
|
||||||
|
final handler = _RecordingErrorInterceptorHandler();
|
||||||
|
|
||||||
|
await interceptor.onError(error, handler);
|
||||||
|
|
||||||
|
expect(handler.nextCalls, 1);
|
||||||
|
expect(sessionLifecycleService.endSessionCalls, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'current request token ends the session once as unauthorized',
|
||||||
|
() async {
|
||||||
|
await TokenStorage.saveTokenInStorage('current-token');
|
||||||
|
|
||||||
|
final options = RequestOptions(path: '/api/profile/index');
|
||||||
|
options.headers['Authorization'] = 'Bearer current-token';
|
||||||
|
options.extra['authToken'] = 'current-token';
|
||||||
|
|
||||||
|
final error = _unauthorized(options);
|
||||||
|
final handler = _RecordingErrorInterceptorHandler();
|
||||||
|
|
||||||
|
await interceptor.onError(error, handler);
|
||||||
|
|
||||||
|
expect(handler.nextCalls, 1);
|
||||||
|
expect(sessionLifecycleService.endSessionCalls, 1);
|
||||||
|
expect(
|
||||||
|
sessionLifecycleService.lastEndReason,
|
||||||
|
SessionEndReason.unauthorized,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../fixtures/doubles/session_doubles.dart';
|
||||||
|
|
||||||
class MockRequestOptions extends Mock implements RequestOptions {}
|
class MockRequestOptions extends Mock implements RequestOptions {}
|
||||||
|
|
||||||
class MockRequestInterceptorHandler extends Mock implements RequestInterceptorHandler {}
|
class MockRequestInterceptorHandler extends Mock
|
||||||
|
implements RequestInterceptorHandler {}
|
||||||
|
|
||||||
class MockErrorInterceptorHandler extends Mock implements ErrorInterceptorHandler {}
|
class MockErrorInterceptorHandler extends Mock
|
||||||
|
implements ErrorInterceptorHandler {}
|
||||||
|
|
||||||
class MockResponse extends Mock implements Response {}
|
class MockResponse extends Mock implements Response {}
|
||||||
|
|
||||||
class MockWsManager extends Mock implements WsManager {}
|
|
||||||
|
|
||||||
class MockDioException extends Mock implements DioException {}
|
class MockDioException extends Mock implements DioException {}
|
||||||
|
|
||||||
class FakeRequestOptions extends Fake implements RequestOptions {}
|
class FakeRequestOptions extends Fake implements RequestOptions {}
|
||||||
@@ -32,23 +34,18 @@ void main() {
|
|||||||
});
|
});
|
||||||
group('TokenInterceptor', () {
|
group('TokenInterceptor', () {
|
||||||
late TokenInterceptor interceptor;
|
late TokenInterceptor interceptor;
|
||||||
|
late FakeSessionLifecycleService fakeSession;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
TokenStorage.clearCache();
|
||||||
final mockWsManager = MockWsManager();
|
|
||||||
when(() => mockWsManager.clear()).thenAnswer((_) async {});
|
await sl.reset();
|
||||||
|
fakeSession = FakeSessionLifecycleService();
|
||||||
await sl.reset(); // Сбрасываем sl перед каждым тестом
|
sl.registerSingleton<SessionLifecycleService>(fakeSession);
|
||||||
sl.registerSingleton<WsManager>(mockWsManager);
|
|
||||||
|
|
||||||
interceptor = TokenInterceptor();
|
interceptor = TokenInterceptor();
|
||||||
// Сбрасываем токен перед каждым тестом
|
await TokenStorage.removeTokenInStorage();
|
||||||
try {
|
|
||||||
await TokenStorage.removeTokenInStorage();
|
|
||||||
} catch (e) {
|
|
||||||
// Игнорируем ошибки при очистке
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('onRequest', () {
|
group('onRequest', () {
|
||||||
@@ -59,6 +56,7 @@ void main() {
|
|||||||
final headers = <String, dynamic>{};
|
final headers = <String, dynamic>{};
|
||||||
final options = MockRequestOptions();
|
final options = MockRequestOptions();
|
||||||
when(() => options.headers).thenReturn(headers);
|
when(() => options.headers).thenReturn(headers);
|
||||||
|
when(() => options.extra).thenReturn(<String, dynamic>{});
|
||||||
final handler = MockRequestInterceptorHandler();
|
final handler = MockRequestInterceptorHandler();
|
||||||
|
|
||||||
when(() => handler.next(any())).thenReturn(null);
|
when(() => handler.next(any())).thenReturn(null);
|
||||||
@@ -73,6 +71,7 @@ void main() {
|
|||||||
final headers = <String, dynamic>{};
|
final headers = <String, dynamic>{};
|
||||||
final options = MockRequestOptions();
|
final options = MockRequestOptions();
|
||||||
when(() => options.headers).thenReturn(headers);
|
when(() => options.headers).thenReturn(headers);
|
||||||
|
when(() => options.extra).thenReturn(<String, dynamic>{});
|
||||||
final handler = MockRequestInterceptorHandler();
|
final handler = MockRequestInterceptorHandler();
|
||||||
|
|
||||||
when(() => handler.next(any())).thenReturn(null);
|
when(() => handler.next(any())).thenReturn(null);
|
||||||
@@ -89,6 +88,7 @@ void main() {
|
|||||||
final headers = <String, dynamic>{};
|
final headers = <String, dynamic>{};
|
||||||
final options = MockRequestOptions();
|
final options = MockRequestOptions();
|
||||||
when(() => options.headers).thenReturn(headers);
|
when(() => options.headers).thenReturn(headers);
|
||||||
|
when(() => options.extra).thenReturn(<String, dynamic>{});
|
||||||
final handler = MockRequestInterceptorHandler();
|
final handler = MockRequestInterceptorHandler();
|
||||||
|
|
||||||
when(() => handler.next(any())).thenReturn(null);
|
when(() => handler.next(any())).thenReturn(null);
|
||||||
@@ -101,11 +101,16 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('onError', () {
|
group('onError', () {
|
||||||
test('удаляет токен и перенаправляет на auth при 401', () async {
|
test('завершает сессию при 401 для текущего токена', () async {
|
||||||
const token = 'test-token';
|
const token = 'test-token';
|
||||||
await TokenStorage.saveTokenInStorage(token);
|
await TokenStorage.saveTokenInStorage(token);
|
||||||
|
|
||||||
final options = MockRequestOptions();
|
final options = MockRequestOptions();
|
||||||
|
when(
|
||||||
|
() => options.headers,
|
||||||
|
).thenReturn({'Authorization': 'Bearer $token'});
|
||||||
|
when(() => options.extra).thenReturn({'authToken': token});
|
||||||
|
|
||||||
final response = MockResponse();
|
final response = MockResponse();
|
||||||
when(() => response.statusCode).thenReturn(401);
|
when(() => response.statusCode).thenReturn(401);
|
||||||
|
|
||||||
@@ -116,14 +121,10 @@ void main() {
|
|||||||
final handler = MockErrorInterceptorHandler();
|
final handler = MockErrorInterceptorHandler();
|
||||||
when(() => handler.next(any())).thenReturn(null);
|
when(() => handler.next(any())).thenReturn(null);
|
||||||
|
|
||||||
// Проверяем, что токен был удален
|
|
||||||
final tokenBefore = await TokenStorage.getTokenFromStorage();
|
|
||||||
expect(tokenBefore, token);
|
|
||||||
|
|
||||||
await interceptor.onError(error, handler);
|
await interceptor.onError(error, handler);
|
||||||
|
|
||||||
final tokenAfter = await TokenStorage.getTokenFromStorage();
|
expect(fakeSession.endSessionCalls, 1);
|
||||||
expect(tokenAfter, null);
|
expect(fakeSession.lastEndReason, SessionEndReason.unauthorized);
|
||||||
verify(() => handler.next(error)).called(1);
|
verify(() => handler.next(error)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
223
test/core/session/session_lifecycle_service_test.dart
Normal file
223
test/core/session/session_lifecycle_service_test.dart
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class _FakeWsManager implements WsManager {
|
||||||
|
int clearCalls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {
|
||||||
|
clearCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) {
|
||||||
|
throw UnimplementedError('WsManager.${invocation.memberName}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeErrorReporter implements ErrorReporter {
|
||||||
|
int reportCalls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> report(
|
||||||
|
Object error,
|
||||||
|
StackTrace? stackTrace, {
|
||||||
|
String? origin,
|
||||||
|
Map<String, Object?>? extras,
|
||||||
|
}) async {
|
||||||
|
reportCalls++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeProfileAvatarCubit implements ProfileAvatarCubit {
|
||||||
|
int resetCalls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {
|
||||||
|
resetCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) {
|
||||||
|
throw UnimplementedError('ProfileAvatarCubit.${invocation.memberName}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeChatsCubit implements ChatsCubit {
|
||||||
|
int resetCalls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {
|
||||||
|
resetCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) {
|
||||||
|
throw UnimplementedError('ChatsCubit.${invocation.memberName}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeNotificationsCubit implements NotificationsCubit {
|
||||||
|
int resetCalls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {
|
||||||
|
resetCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) {
|
||||||
|
throw UnimplementedError('NotificationsCubit.${invocation.memberName}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeNewMessagesCubit implements NewMessagesCubit {
|
||||||
|
int resetCalls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {
|
||||||
|
resetCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) {
|
||||||
|
throw UnimplementedError('NewMessagesCubit.${invocation.memberName}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeFeedCubit implements FeedCubit {
|
||||||
|
int resetCalls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {
|
||||||
|
resetCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) {
|
||||||
|
throw UnimplementedError('FeedCubit.${invocation.memberName}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeVideoCallSessionCubit implements VideoCallSessionCubit {
|
||||||
|
int endCallCalls = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> endCall() async {
|
||||||
|
endCallCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) {
|
||||||
|
throw UnimplementedError('VideoCallSessionCubit.${invocation.memberName}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
late _FakeWsManager wsManager;
|
||||||
|
late _FakeProfileAvatarCubit profileAvatarCubit;
|
||||||
|
late _FakeChatsCubit chatsCubit;
|
||||||
|
late _FakeNotificationsCubit notificationsCubit;
|
||||||
|
late _FakeNewMessagesCubit newMessagesCubit;
|
||||||
|
late _FakeFeedCubit feedCubit;
|
||||||
|
late _FakeVideoCallSessionCubit videoCallSessionCubit;
|
||||||
|
late SessionLifecycleService service;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'token': 'token-a',
|
||||||
|
'socketId': 'socket-a',
|
||||||
|
PrefsKeys.customBaseUrl.name: 'https://old.example',
|
||||||
|
PrefsKeys.isEmailVerificationSkipped.name: true,
|
||||||
|
PrefsKeys.registrationDate.name: '2024-01-01',
|
||||||
|
PrefsKeys.welcomeScreenWasShowed.name: true,
|
||||||
|
});
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
await sl.reset();
|
||||||
|
|
||||||
|
wsManager = _FakeWsManager();
|
||||||
|
profileAvatarCubit = _FakeProfileAvatarCubit();
|
||||||
|
chatsCubit = _FakeChatsCubit();
|
||||||
|
notificationsCubit = _FakeNotificationsCubit();
|
||||||
|
newMessagesCubit = _FakeNewMessagesCubit();
|
||||||
|
feedCubit = _FakeFeedCubit();
|
||||||
|
videoCallSessionCubit = _FakeVideoCallSessionCubit();
|
||||||
|
|
||||||
|
sl.registerSingleton<ProfileAvatarCubit>(profileAvatarCubit);
|
||||||
|
sl.registerSingleton<ChatsCubit>(chatsCubit);
|
||||||
|
sl.registerSingleton<NotificationsCubit>(notificationsCubit);
|
||||||
|
sl.registerSingleton<NewMessagesCubit>(newMessagesCubit);
|
||||||
|
sl.registerSingleton<FeedCubit>(feedCubit);
|
||||||
|
sl.registerSingleton<VideoCallSessionCubit>(videoCallSessionCubit);
|
||||||
|
sl.registerSingleton<Prefs>(Prefs(await SharedPreferences.getInstance()));
|
||||||
|
|
||||||
|
service = SessionLifecycleService(
|
||||||
|
wsManager: wsManager,
|
||||||
|
errorReporter: _FakeErrorReporter(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await sl.reset();
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('SessionLifecycleService.endSession', () {
|
||||||
|
test(
|
||||||
|
'resets user-scoped state and clears only session-scoped keys',
|
||||||
|
() async {
|
||||||
|
await service.endSession(reason: SessionEndReason.logout);
|
||||||
|
|
||||||
|
expect(wsManager.clearCalls, 1);
|
||||||
|
expect(profileAvatarCubit.resetCalls, 1);
|
||||||
|
expect(chatsCubit.resetCalls, 1);
|
||||||
|
expect(notificationsCubit.resetCalls, 1);
|
||||||
|
expect(newMessagesCubit.resetCalls, 1);
|
||||||
|
expect(feedCubit.resetCalls, 1);
|
||||||
|
expect(videoCallSessionCubit.endCallCalls, 1);
|
||||||
|
|
||||||
|
expect(await TokenStorage.getTokenFromStorage(), isNull);
|
||||||
|
expect(await SocketIdStorage.getSocketIdFromStorage(), isNull);
|
||||||
|
|
||||||
|
final prefs = sl<Prefs>();
|
||||||
|
expect(prefs.getString(PrefsKeys.customBaseUrl), 'https://old.example');
|
||||||
|
expect(prefs.getBool(PrefsKeys.isEmailVerificationSkipped), isNull);
|
||||||
|
expect(prefs.getString(PrefsKeys.registrationDate), isNull);
|
||||||
|
expect(prefs.getBool(PrefsKeys.welcomeScreenWasShowed), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('is idempotent for concurrent calls', () async {
|
||||||
|
final first = service.endSession(reason: SessionEndReason.logout);
|
||||||
|
final second = service.endSession(reason: SessionEndReason.unauthorized);
|
||||||
|
|
||||||
|
await Future.wait([first, second]);
|
||||||
|
|
||||||
|
expect(wsManager.clearCalls, 1);
|
||||||
|
expect(profileAvatarCubit.resetCalls, 1);
|
||||||
|
expect(chatsCubit.resetCalls, 1);
|
||||||
|
expect(notificationsCubit.resetCalls, 1);
|
||||||
|
expect(newMessagesCubit.resetCalls, 1);
|
||||||
|
expect(feedCubit.resetCalls, 1);
|
||||||
|
expect(videoCallSessionCubit.endCallCalls, 1);
|
||||||
|
|
||||||
|
expect(await TokenStorage.getTokenFromStorage(), isNull);
|
||||||
|
expect(await SocketIdStorage.getSocketIdFromStorage(), isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import 'package:mockito/annotations.dart';
|
|||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import 'auth_cubit_test.mocks.dart';
|
import 'auth_cubit_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateNiceMocks([
|
@GenerateNiceMocks([
|
||||||
@@ -75,6 +76,7 @@ void main() {
|
|||||||
profileRepository: mockProfileRepository,
|
profileRepository: mockProfileRepository,
|
||||||
firebaseApi: mockFirebaseApi,
|
firebaseApi: mockFirebaseApi,
|
||||||
prefs: mockPrefs,
|
prefs: mockPrefs,
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(cubit!.state, isA<AuthState>());
|
expect(cubit!.state, isA<AuthState>());
|
||||||
@@ -86,6 +88,7 @@ void main() {
|
|||||||
profileRepository: mockProfileRepository,
|
profileRepository: mockProfileRepository,
|
||||||
firebaseApi: mockFirebaseApi,
|
firebaseApi: mockFirebaseApi,
|
||||||
prefs: mockPrefs,
|
prefs: mockPrefs,
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const email = 'test@example.com';
|
const email = 'test@example.com';
|
||||||
@@ -116,6 +119,7 @@ void main() {
|
|||||||
profileRepository: mockProfileRepository,
|
profileRepository: mockProfileRepository,
|
||||||
firebaseApi: mockFirebaseApi,
|
firebaseApi: mockFirebaseApi,
|
||||||
prefs: mockPrefs,
|
prefs: mockPrefs,
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const email = 'test@example.com';
|
const email = 'test@example.com';
|
||||||
@@ -140,6 +144,7 @@ void main() {
|
|||||||
profileRepository: mockProfileRepository,
|
profileRepository: mockProfileRepository,
|
||||||
firebaseApi: mockFirebaseApi,
|
firebaseApi: mockFirebaseApi,
|
||||||
prefs: mockPrefs,
|
prefs: mockPrefs,
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockPrefs.getBool(any)).thenReturn(false);
|
when(mockPrefs.getBool(any)).thenReturn(false);
|
||||||
@@ -159,6 +164,7 @@ void main() {
|
|||||||
profileRepository: mockProfileRepository,
|
profileRepository: mockProfileRepository,
|
||||||
firebaseApi: mockFirebaseApi,
|
firebaseApi: mockFirebaseApi,
|
||||||
prefs: mockPrefs,
|
prefs: mockPrefs,
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockPrefs.getBool(any)).thenReturn(true);
|
when(mockPrefs.getBool(any)).thenReturn(true);
|
||||||
@@ -174,6 +180,7 @@ void main() {
|
|||||||
profileRepository: mockProfileRepository,
|
profileRepository: mockProfileRepository,
|
||||||
firebaseApi: mockFirebaseApi,
|
firebaseApi: mockFirebaseApi,
|
||||||
prefs: mockPrefs,
|
prefs: mockPrefs,
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockFirebaseApi.registerPushToken())
|
when(mockFirebaseApi.registerPushToken())
|
||||||
@@ -190,6 +197,7 @@ void main() {
|
|||||||
profileRepository: mockProfileRepository,
|
profileRepository: mockProfileRepository,
|
||||||
firebaseApi: mockFirebaseApi,
|
firebaseApi: mockFirebaseApi,
|
||||||
prefs: mockPrefs,
|
prefs: mockPrefs,
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TokenStorage.haveTokenInStorage() вернет false
|
// TokenStorage.haveTokenInStorage() вернет false
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
|
|
||||||
import '../../../../fixtures/doubles/auth_doubles.dart';
|
import '../../../../fixtures/doubles/auth_doubles.dart';
|
||||||
import '../../../../fixtures/doubles/profile_doubles.dart';
|
import '../../../../fixtures/doubles/profile_doubles.dart';
|
||||||
|
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import '../../../../fixtures/fixture_reader.dart';
|
import '../../../../fixtures/fixture_reader.dart';
|
||||||
import 'auth_cubit_test.mocks.dart';
|
import 'auth_cubit_test.mocks.dart';
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ void main() {
|
|||||||
late FixtureProfileRepository profileRepository;
|
late FixtureProfileRepository profileRepository;
|
||||||
late MockPrefs mockPrefs;
|
late MockPrefs mockPrefs;
|
||||||
late MockFirebaseApi mockFirebaseApi;
|
late MockFirebaseApi mockFirebaseApi;
|
||||||
|
late FakeSessionLifecycleService fakeSession;
|
||||||
late Map<String, dynamic> credentialsFixture;
|
late Map<String, dynamic> credentialsFixture;
|
||||||
|
|
||||||
late TokenModel tokenFixture;
|
late TokenModel tokenFixture;
|
||||||
@@ -37,6 +39,7 @@ void main() {
|
|||||||
profileRepository = FixtureProfileRepository();
|
profileRepository = FixtureProfileRepository();
|
||||||
mockPrefs = MockPrefs();
|
mockPrefs = MockPrefs();
|
||||||
mockFirebaseApi = MockFirebaseApi();
|
mockFirebaseApi = MockFirebaseApi();
|
||||||
|
fakeSession = FakeSessionLifecycleService();
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
|
||||||
// Mock Fluttertoast plugin
|
// Mock Fluttertoast plugin
|
||||||
@@ -59,12 +62,12 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('начальное состояние - AuthState', () {
|
test('начальное состояние - AuthState', () {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
expect(authCubit!.state, const AuthState());
|
expect(authCubit!.state, const AuthState());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getTokenByCredentials - успешная авторизация', () async {
|
test('getTokenByCredentials - успешная авторизация', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
|
|
||||||
final result = await authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String);
|
final result = await authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String);
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('getTokenByCredentials - переход в состояние loading', () async {
|
test('getTokenByCredentials - переход в состояние loading', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
authRepository.responseDelay = const Duration(milliseconds: 50);
|
authRepository.responseDelay = const Duration(milliseconds: 50);
|
||||||
|
|
||||||
final future = authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String);
|
final future = authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String);
|
||||||
@@ -88,7 +91,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('getTokenByCredentials - обработка ошибки авторизации', () async {
|
test('getTokenByCredentials - обработка ошибки авторизации', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
authRepository.error = Exception('Неверный адрес электронной почты или пароль');
|
authRepository.error = Exception('Неверный адрес электронной почты или пароль');
|
||||||
|
|
||||||
final result = await authCubit!.getTokenByCredentials('wrong@example.com', 'wrongpassword');
|
final result = await authCubit!.getTokenByCredentials('wrong@example.com', 'wrongpassword');
|
||||||
@@ -102,7 +105,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('getTokenByCredentials - обработка различных типов ошибок', () async {
|
test('getTokenByCredentials - обработка различных типов ошибок', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
authRepository.error = const FormatException('Invalid credentials format');
|
authRepository.error = const FormatException('Invalid credentials format');
|
||||||
|
|
||||||
final result = await authCubit!.getTokenByCredentials('invalid', 'pass');
|
final result = await authCubit!.getTokenByCredentials('invalid', 'pass');
|
||||||
@@ -115,7 +118,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('getTokenByCredentials - проверка вызова с правильными параметрами', () async {
|
test('getTokenByCredentials - проверка вызова с правильными параметрами', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
|
|
||||||
await authCubit!.getTokenByCredentials('user@test.com', 'securePassword123');
|
await authCubit!.getTokenByCredentials('user@test.com', 'securePassword123');
|
||||||
|
|
||||||
@@ -124,7 +127,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('getTokenByCredentials - множественные вызовы', () async {
|
test('getTokenByCredentials - множественные вызовы', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
|
|
||||||
await authCubit!.getTokenByCredentials('first@test.com', 'password1');
|
await authCubit!.getTokenByCredentials('first@test.com', 'password1');
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
@@ -138,7 +141,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('registerAnonymousPushToken - регистрирует токен если не был отправлен', () async {
|
test('registerAnonymousPushToken - регистрирует токен если не был отправлен', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(false);
|
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(false);
|
||||||
when(mockFirebaseApi.registerAnonymousPushToken()).thenAnswer((_) async => {});
|
when(mockFirebaseApi.registerAnonymousPushToken()).thenAnswer((_) async => {});
|
||||||
when(mockPrefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true)).thenAnswer((_) async => {});
|
when(mockPrefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true)).thenAnswer((_) async => {});
|
||||||
@@ -150,7 +153,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('registerAnonymousPushToken - не регистрирует токен если уже был отправлен', () async {
|
test('registerAnonymousPushToken - не регистрирует токен если уже был отправлен', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(true);
|
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(true);
|
||||||
|
|
||||||
await authCubit!.registerAnonymousPushToken();
|
await authCubit!.registerAnonymousPushToken();
|
||||||
@@ -159,7 +162,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('registerPushToken - регистрирует push токен', () async {
|
test('registerPushToken - регистрирует push токен', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
when(mockFirebaseApi.registerPushToken()).thenAnswer((_) async => {});
|
when(mockFirebaseApi.registerPushToken()).thenAnswer((_) async => {});
|
||||||
|
|
||||||
await authCubit!.registerPushToken();
|
await authCubit!.registerPushToken();
|
||||||
@@ -168,7 +171,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('registerAnonymousPushToken - обрабатывает null значение hasAnonymousTokenBeenSent', () async {
|
test('registerAnonymousPushToken - обрабатывает null значение hasAnonymousTokenBeenSent', () async {
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||||
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(null);
|
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(null);
|
||||||
when(mockFirebaseApi.registerAnonymousPushToken()).thenAnswer((_) async => {});
|
when(mockFirebaseApi.registerAnonymousPushToken()).thenAnswer((_) async => {});
|
||||||
when(mockPrefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true)).thenAnswer((_) async => {});
|
when(mockPrefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true)).thenAnswer((_) async => {});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
|
||||||
@@ -13,9 +14,21 @@ import 'package:mockito/annotations.dart';
|
|||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import '../../../../../fixtures/fixture_reader.dart';
|
import '../../../../../fixtures/fixture_reader.dart';
|
||||||
import 'oauth_cubit_test.mocks.dart';
|
import 'oauth_cubit_test.mocks.dart';
|
||||||
|
|
||||||
|
class _RecordingFirebaseApi extends Fake implements FirebaseApi {
|
||||||
|
int registerPushTokenCalls = 0;
|
||||||
|
bool hadAuthToken = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> registerPushToken() async {
|
||||||
|
registerPushTokenCalls++;
|
||||||
|
hadAuthToken = await TokenStorage.haveTokenInStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateNiceMocks([MockSpec<AuthRepository>(), MockSpec<Prefs>()])
|
@GenerateNiceMocks([MockSpec<AuthRepository>(), MockSpec<Prefs>()])
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -24,26 +37,38 @@ void main() {
|
|||||||
OAuthCubit? oauthCubit;
|
OAuthCubit? oauthCubit;
|
||||||
late MockAuthRepository mockAuthRepository;
|
late MockAuthRepository mockAuthRepository;
|
||||||
late MockPrefs mockPrefs;
|
late MockPrefs mockPrefs;
|
||||||
|
late _RecordingFirebaseApi firebaseApi;
|
||||||
|
late FakeSessionLifecycleService fakeSession;
|
||||||
late TokenModel tokenFixture;
|
late TokenModel tokenFixture;
|
||||||
late Map<String, dynamic> signInRequestFixture;
|
late Map<String, dynamic> signInRequestFixture;
|
||||||
late Map<String, dynamic> signUpRequestFixture;
|
late Map<String, dynamic> signUpRequestFixture;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
tokenFixture = TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json'));
|
tokenFixture = TokenModel.fromJson(
|
||||||
signInRequestFixture = FixtureReader.readAsMap('auth/oauth_signin_request.json');
|
FixtureReader.readAsMap('auth/token_response.json'),
|
||||||
signUpRequestFixture = FixtureReader.readAsMap('auth/oauth_signup_request.json');
|
);
|
||||||
|
signInRequestFixture = FixtureReader.readAsMap(
|
||||||
|
'auth/oauth_signin_request.json',
|
||||||
|
);
|
||||||
|
signUpRequestFixture = FixtureReader.readAsMap(
|
||||||
|
'auth/oauth_signup_request.json',
|
||||||
|
);
|
||||||
|
|
||||||
mockAuthRepository = MockAuthRepository();
|
mockAuthRepository = MockAuthRepository();
|
||||||
mockPrefs = MockPrefs();
|
mockPrefs = MockPrefs();
|
||||||
|
firebaseApi = _RecordingFirebaseApi();
|
||||||
|
fakeSession = FakeSessionLifecycleService();
|
||||||
|
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
|
||||||
// Mock Fluttertoast plugin
|
// Mock Fluttertoast plugin
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('PonnamKarthik/fluttertoast'), (
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
MethodCall methodCall,
|
.setMockMethodCallHandler(
|
||||||
) async {
|
const MethodChannel('PonnamKarthik/fluttertoast'),
|
||||||
return null;
|
(MethodCall methodCall) async {
|
||||||
});
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
@@ -52,18 +77,31 @@ void main() {
|
|||||||
oauthCubit!.close();
|
oauthCubit!.close();
|
||||||
oauthCubit = null;
|
oauthCubit = null;
|
||||||
}
|
}
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
|
||||||
// Clear mock handlers
|
// Clear mock handlers
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('PonnamKarthik/fluttertoast'), null);
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(
|
||||||
|
const MethodChannel('PonnamKarthik/fluttertoast'),
|
||||||
|
null,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('начальное состояние - OAuthState', () {
|
test('начальное состояние - OAuthState', () {
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
expect(oauthCubit!.state, const OAuthState());
|
expect(oauthCubit!.state, const OAuthState());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('signInViaSocialMedia - успешный вход через соцсеть', () async {
|
test('signInViaSocialMedia - успешный вход через соцсеть', () async {
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
mockAuthRepository.signInViaSocialMedia(
|
mockAuthRepository.signInViaSocialMedia(
|
||||||
@@ -92,7 +130,11 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('signInViaSocialMedia - переход в состояние loading', () async {
|
test('signInViaSocialMedia - переход в состояние loading', () async {
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
mockAuthRepository.signInViaSocialMedia(
|
mockAuthRepository.signInViaSocialMedia(
|
||||||
@@ -119,7 +161,11 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('signInViaSocialMedia - обработка ошибки', () async {
|
test('signInViaSocialMedia - обработка ошибки', () async {
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
mockAuthRepository.signInViaSocialMedia(
|
mockAuthRepository.signInViaSocialMedia(
|
||||||
@@ -144,7 +190,11 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('signUpViaSocialMedia - успешная регистрация через соцсеть', () async {
|
test('signUpViaSocialMedia - успешная регистрация через соцсеть', () async {
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
mockAuthRepository.signUpViaSocialMedia(
|
mockAuthRepository.signUpViaSocialMedia(
|
||||||
@@ -164,7 +214,9 @@ void main() {
|
|||||||
),
|
),
|
||||||
).thenAnswer((_) async => tokenFixture.token);
|
).thenAnswer((_) async => tokenFixture.token);
|
||||||
|
|
||||||
when(mockPrefs.setString(any, any)).thenAnswer((_) async => Future.value());
|
when(
|
||||||
|
mockPrefs.setString(any, any),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
final result = await oauthCubit!.signUpViaSocialMedia(
|
final result = await oauthCubit!.signUpViaSocialMedia(
|
||||||
provider: signUpRequestFixture['provider'] as String,
|
provider: signUpRequestFixture['provider'] as String,
|
||||||
@@ -187,48 +239,61 @@ void main() {
|
|||||||
expect(oauthCubit!.state, isA<OAuthStateLoaded>());
|
expect(oauthCubit!.state, isA<OAuthStateLoaded>());
|
||||||
final loadedState = oauthCubit!.state as OAuthStateLoaded;
|
final loadedState = oauthCubit!.state as OAuthStateLoaded;
|
||||||
expect(loadedState.token, tokenFixture.token);
|
expect(loadedState.token, tokenFixture.token);
|
||||||
|
expect(await TokenStorage.getTokenFromStorage(), tokenFixture.token);
|
||||||
|
expect(fakeSession.onAuthenticatedCalls, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('signInViaSocialMedia - обработка ошибки валидации email/phone', () async {
|
test(
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
'signInViaSocialMedia - обработка ошибки валидации email/phone',
|
||||||
|
() async {
|
||||||
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
final dioException = DioException(
|
final dioException = DioException(
|
||||||
requestOptions: RequestOptions(path: '/api/auth/oauth/signin'),
|
|
||||||
response: Response(
|
|
||||||
statusCode: 422,
|
|
||||||
data: {
|
|
||||||
'errors': {
|
|
||||||
'email': ['Некорректный формат email'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
requestOptions: RequestOptions(path: '/api/auth/oauth/signin'),
|
requestOptions: RequestOptions(path: '/api/auth/oauth/signin'),
|
||||||
),
|
response: Response(
|
||||||
);
|
statusCode: 422,
|
||||||
|
data: {
|
||||||
|
'errors': {
|
||||||
|
'email': ['Некорректный формат email'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: '/api/auth/oauth/signin'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
mockAuthRepository.signInViaSocialMedia(
|
mockAuthRepository.signInViaSocialMedia(
|
||||||
provider: anyNamed('provider'),
|
provider: anyNamed('provider'),
|
||||||
accessToken: anyNamed('accessToken'),
|
accessToken: anyNamed('accessToken'),
|
||||||
refreshToken: anyNamed('refreshToken'),
|
refreshToken: anyNamed('refreshToken'),
|
||||||
clientId: anyNamed('clientId'),
|
clientId: anyNamed('clientId'),
|
||||||
),
|
),
|
||||||
).thenThrow(dioException);
|
).thenThrow(dioException);
|
||||||
|
|
||||||
await oauthCubit!.signInViaSocialMedia(
|
await oauthCubit!.signInViaSocialMedia(
|
||||||
provider: signInRequestFixture['provider'] as String,
|
provider: signInRequestFixture['provider'] as String,
|
||||||
accessToken: signInRequestFixture['access_token'] as String,
|
accessToken: signInRequestFixture['access_token'] as String,
|
||||||
refreshToken: signInRequestFixture['refresh_token'] as String,
|
refreshToken: signInRequestFixture['refresh_token'] as String,
|
||||||
clientId: signInRequestFixture['client_id'] as String,
|
clientId: signInRequestFixture['client_id'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
expect(oauthCubit!.state, isA<OAuthStateError>());
|
expect(oauthCubit!.state, isA<OAuthStateError>());
|
||||||
final errorState = oauthCubit!.state as OAuthStateError;
|
final errorState = oauthCubit!.state as OAuthStateError;
|
||||||
expect(errorState.errorMessage, 'validation_error');
|
expect(errorState.errorMessage, 'validation_error');
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('signUpViaSocialMedia - обработка ошибки валидации email', () async {
|
test('signUpViaSocialMedia - обработка ошибки валидации email', () async {
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
final dioException = DioException(
|
final dioException = DioException(
|
||||||
requestOptions: RequestOptions(path: '/api/auth/oauth/signup'),
|
requestOptions: RequestOptions(path: '/api/auth/oauth/signup'),
|
||||||
@@ -282,82 +347,129 @@ void main() {
|
|||||||
expect(result[0], RegistrationValidationErrors.incorrectEmailFormat);
|
expect(result[0], RegistrationValidationErrors.incorrectEmailFormat);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('requestOAuthVerification - успешный запрос кода верификации', () async {
|
test(
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
'requestOAuthVerification - успешный запрос кода верификации',
|
||||||
|
() async {
|
||||||
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
const verificationResponse = OAuthVerificationRequestResponse(email: 't***@example.com', status: 'sent');
|
const verificationResponse = OAuthVerificationRequestResponse(
|
||||||
|
email: 't***@example.com',
|
||||||
|
status: 'sent',
|
||||||
|
);
|
||||||
|
|
||||||
when(mockAuthRepository.requestOAuthVerification(email: anyNamed('email'), phone: anyNamed('phone'))).thenAnswer((_) async => verificationResponse);
|
when(
|
||||||
|
mockAuthRepository.requestOAuthVerification(
|
||||||
|
email: anyNamed('email'),
|
||||||
|
phone: anyNamed('phone'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => verificationResponse);
|
||||||
|
|
||||||
final result = await oauthCubit!.requestOAuthVerification(email: 'test@example.com');
|
final result = await oauthCubit!.requestOAuthVerification(
|
||||||
|
email: 'test@example.com',
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, isNotNull);
|
expect(result, isNotNull);
|
||||||
expect(result!.email, 't***@example.com');
|
expect(result!.email, 't***@example.com');
|
||||||
expect(oauthCubit!.state, isA<OAuthState>());
|
expect(oauthCubit!.state, isA<OAuthState>());
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('requestOAuthVerification - обработка ошибки', () async {
|
test('requestOAuthVerification - обработка ошибки', () async {
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
when(mockAuthRepository.requestOAuthVerification(email: anyNamed('email'), phone: anyNamed('phone'))).thenThrow(Exception('Ошибка запроса'));
|
when(
|
||||||
|
mockAuthRepository.requestOAuthVerification(
|
||||||
|
email: anyNamed('email'),
|
||||||
|
phone: anyNamed('phone'),
|
||||||
|
),
|
||||||
|
).thenThrow(Exception('Ошибка запроса'));
|
||||||
|
|
||||||
final result = await oauthCubit!.requestOAuthVerification(email: 'test@example.com');
|
final result = await oauthCubit!.requestOAuthVerification(
|
||||||
|
email: 'test@example.com',
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, isNull);
|
expect(result, isNull);
|
||||||
expect(oauthCubit!.state, isA<OAuthStateError>());
|
expect(oauthCubit!.state, isA<OAuthStateError>());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('confirmOAuthVerification - успешное подтверждение верификации', () async {
|
test(
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
'confirmOAuthVerification - успешное подтверждение верификации',
|
||||||
|
() async {
|
||||||
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
final verificationResponse = OAuthVerificationConfirmResponse(success: true, message: 'Success', token: tokenFixture.token, user: {}, isNewUser: false);
|
final verificationResponse = OAuthVerificationConfirmResponse(
|
||||||
|
success: true,
|
||||||
|
message: 'Success',
|
||||||
|
token: tokenFixture.token,
|
||||||
|
user: {},
|
||||||
|
isNewUser: false,
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
mockAuthRepository.confirmOAuthVerification(
|
mockAuthRepository.confirmOAuthVerification(
|
||||||
provider: anyNamed('provider'),
|
provider: anyNamed('provider'),
|
||||||
accessToken: anyNamed('accessToken'),
|
accessToken: anyNamed('accessToken'),
|
||||||
refreshToken: anyNamed('refreshToken'),
|
refreshToken: anyNamed('refreshToken'),
|
||||||
clientId: anyNamed('clientId'),
|
clientId: anyNamed('clientId'),
|
||||||
id: anyNamed('id'),
|
id: anyNamed('id'),
|
||||||
email: anyNamed('email'),
|
email: anyNamed('email'),
|
||||||
phone: anyNamed('phone'),
|
phone: anyNamed('phone'),
|
||||||
code: anyNamed('code'),
|
code: anyNamed('code'),
|
||||||
verificationType: anyNamed('verificationType'),
|
verificationType: anyNamed('verificationType'),
|
||||||
firstName: anyNamed('firstName'),
|
firstName: anyNamed('firstName'),
|
||||||
lastName: anyNamed('lastName'),
|
lastName: anyNamed('lastName'),
|
||||||
dateOfBirth: anyNamed('dateOfBirth'),
|
dateOfBirth: anyNamed('dateOfBirth'),
|
||||||
gender: anyNamed('gender'),
|
gender: anyNamed('gender'),
|
||||||
weight: anyNamed('weight'),
|
weight: anyNamed('weight'),
|
||||||
height: anyNamed('height'),
|
height: anyNamed('height'),
|
||||||
),
|
),
|
||||||
).thenAnswer((_) async => verificationResponse);
|
).thenAnswer((_) async => verificationResponse);
|
||||||
|
|
||||||
await oauthCubit!.confirmOAuthVerification(
|
await oauthCubit!.confirmOAuthVerification(
|
||||||
provider: 'google',
|
provider: 'google',
|
||||||
accessToken: 'access_token',
|
accessToken: 'access_token',
|
||||||
refreshToken: 'refresh_token',
|
refreshToken: 'refresh_token',
|
||||||
clientId: 'client_id',
|
clientId: 'client_id',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
phone: null,
|
phone: null,
|
||||||
code: '123456',
|
code: '123456',
|
||||||
verificationType: 'email',
|
verificationType: 'email',
|
||||||
firstName: 'Test',
|
firstName: 'Test',
|
||||||
lastName: 'User',
|
lastName: 'User',
|
||||||
dateOfBirth: '2000-01-01',
|
dateOfBirth: '2000-01-01',
|
||||||
gender: 'male',
|
gender: 'male',
|
||||||
weight: 70,
|
weight: 70,
|
||||||
height: 175,
|
height: 175,
|
||||||
);
|
);
|
||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
expect(oauthCubit!.state, isA<OAuthStateLoaded>());
|
expect(oauthCubit!.state, isA<OAuthStateLoaded>());
|
||||||
final loadedState = oauthCubit!.state as OAuthStateLoaded;
|
final loadedState = oauthCubit!.state as OAuthStateLoaded;
|
||||||
expect(loadedState.token, tokenFixture.token);
|
expect(loadedState.token, tokenFixture.token);
|
||||||
});
|
expect(await TokenStorage.getTokenFromStorage(), tokenFixture.token);
|
||||||
|
expect(fakeSession.onAuthenticatedCalls, 1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('confirmOAuthVerification - обработка ошибки', () async {
|
test('confirmOAuthVerification - обработка ошибки', () async {
|
||||||
oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs);
|
oauthCubit = OAuthCubit(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||||
|
prefs: mockPrefs,
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
mockAuthRepository.confirmOAuthVerification(
|
mockAuthRepository.confirmOAuthVerification(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
|
|
||||||
import '../../../../fixtures/doubles/auth_doubles.dart';
|
import '../../../../fixtures/doubles/auth_doubles.dart';
|
||||||
import '../../../../fixtures/doubles/profile_doubles.dart';
|
import '../../../../fixtures/doubles/profile_doubles.dart';
|
||||||
|
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import '../../../../fixtures/fixture_reader.dart';
|
import '../../../../fixtures/fixture_reader.dart';
|
||||||
import 'basic_widget_test.mocks.dart';
|
import 'basic_widget_test.mocks.dart';
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ void main() {
|
|||||||
mockFirebaseApi = MockFirebaseApi();
|
mockFirebaseApi = MockFirebaseApi();
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
|
||||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: FakeSessionLifecycleService());
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
@@ -55,7 +56,7 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('should create AuthCubit successfully', (WidgetTester tester) async {
|
testWidgets('should create AuthCubit successfully', (WidgetTester tester) async {
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
final cubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
final cubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: FakeSessionLifecycleService());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(cubit, isA<AuthCubit>());
|
expect(cubit, isA<AuthCubit>());
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||||
|
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||||
|
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class MockProfileRepository extends Mock implements ProfileRepository {}
|
||||||
|
|
||||||
|
const _profileA = ProfileModel(
|
||||||
|
id: 1,
|
||||||
|
slug: 'user-a',
|
||||||
|
name: 'User A',
|
||||||
|
email: 'a@example.com',
|
||||||
|
phoneConfirmationState: 'confirmed',
|
||||||
|
isPublicProfile: true,
|
||||||
|
isBlockedByMe: false,
|
||||||
|
isBlockedByAuthor: false,
|
||||||
|
isBlockedByUser: false,
|
||||||
|
isEntitySubscribedByMe: false,
|
||||||
|
profilePhotoUrl: 'avatars/a.png',
|
||||||
|
roles: [],
|
||||||
|
badges: [],
|
||||||
|
subscriptions: [],
|
||||||
|
subscribers: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const _profileB = ProfileModel(
|
||||||
|
id: 2,
|
||||||
|
slug: 'user-b',
|
||||||
|
name: 'User B',
|
||||||
|
email: 'b@example.com',
|
||||||
|
phoneConfirmationState: 'confirmed',
|
||||||
|
isPublicProfile: true,
|
||||||
|
isBlockedByMe: false,
|
||||||
|
isBlockedByAuthor: false,
|
||||||
|
isBlockedByUser: false,
|
||||||
|
isEntitySubscribedByMe: false,
|
||||||
|
profilePhotoUrl: 'avatars/b.png',
|
||||||
|
roles: [],
|
||||||
|
badges: [],
|
||||||
|
subscriptions: [],
|
||||||
|
subscribers: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const _details = ProfileDetailsModel(
|
||||||
|
firstName: 'User',
|
||||||
|
lastName: 'Test',
|
||||||
|
birthdate: '1990-01-01',
|
||||||
|
gender: 'male',
|
||||||
|
country: 'Russia',
|
||||||
|
city: 'Moscow',
|
||||||
|
lang: 'ru',
|
||||||
|
weight: 70.0,
|
||||||
|
height: 175,
|
||||||
|
chestCircumference: 100,
|
||||||
|
waistCircumference: 80,
|
||||||
|
hipCircumference: 90,
|
||||||
|
bloodPressureUp: 120,
|
||||||
|
bloodPressureDown: 80,
|
||||||
|
hasHypo: false,
|
||||||
|
hasHyper: false,
|
||||||
|
aboutAlergy: '-',
|
||||||
|
aboutTakingPils: '-',
|
||||||
|
aboutSportHobbies: '-',
|
||||||
|
favoriteFoods: '-',
|
||||||
|
unfavoriteFoods: '-',
|
||||||
|
additionalInfo: '-',
|
||||||
|
isMoodFillable: true,
|
||||||
|
isBedtimeFillable: true,
|
||||||
|
activityCoefficient: 1.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
late MockProfileRepository repository;
|
||||||
|
ProfileAvatarCubit? cubit;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await sl.reset();
|
||||||
|
SharedPreferences.setMockInitialValues({'token': 'token-a'});
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
repository = MockProfileRepository();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
if (cubit != null && !cubit!.isClosed) {
|
||||||
|
await cubit!.close();
|
||||||
|
}
|
||||||
|
cubit = null;
|
||||||
|
await sl.reset();
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
void stubSuccessfulProfile(ProfileModel profile) {
|
||||||
|
when(() => repository.getMyProfile()).thenAnswer((_) async => profile);
|
||||||
|
when(
|
||||||
|
() => repository.getMyProfileDetails(),
|
||||||
|
).thenAnswer((_) async => _details);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('reset emits loading and clears the cached profile', () async {
|
||||||
|
stubSuccessfulProfile(_profileA);
|
||||||
|
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||||
|
|
||||||
|
await cubit!.ensureFreshForCurrentSession();
|
||||||
|
expect(cubit!.state, isA<ProfileAvatarStateLoaded>());
|
||||||
|
|
||||||
|
cubit!.reset();
|
||||||
|
expect(cubit!.state, isA<ProfileAvatarStateLoading>());
|
||||||
|
|
||||||
|
await cubit!.ensureFreshForCurrentSession();
|
||||||
|
|
||||||
|
verify(() => repository.getMyProfile()).called(2);
|
||||||
|
expect(cubit!.state, isA<ProfileAvatarStateLoaded>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('late getMyProfile completion after reset is ignored', () async {
|
||||||
|
final profileCompleter = Completer<ProfileModel>();
|
||||||
|
when(
|
||||||
|
() => repository.getMyProfile(),
|
||||||
|
).thenAnswer((_) => profileCompleter.future);
|
||||||
|
when(
|
||||||
|
() => repository.getMyProfileDetails(),
|
||||||
|
).thenAnswer((_) async => _details);
|
||||||
|
|
||||||
|
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||||
|
|
||||||
|
final pendingRequest = cubit!.getMyProfile();
|
||||||
|
await pumpEventQueue();
|
||||||
|
expect(cubit!.state, isA<ProfileAvatarStateLoading>());
|
||||||
|
|
||||||
|
cubit!.reset();
|
||||||
|
profileCompleter.complete(_profileA);
|
||||||
|
await pendingRequest;
|
||||||
|
await pumpEventQueue();
|
||||||
|
|
||||||
|
expect(cubit!.state, isA<ProfileAvatarStateLoading>());
|
||||||
|
verifyNever(() => repository.getMyProfileDetails());
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'ensureFreshForCurrentSession does not refetch for the same token',
|
||||||
|
() async {
|
||||||
|
stubSuccessfulProfile(_profileA);
|
||||||
|
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||||
|
|
||||||
|
await cubit!.ensureFreshForCurrentSession();
|
||||||
|
await cubit!.ensureFreshForCurrentSession();
|
||||||
|
|
||||||
|
verify(() => repository.getMyProfile()).called(1);
|
||||||
|
expect(cubit!.state, isA<ProfileAvatarStateLoaded>());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'ensureFreshForCurrentSession refetches after the token changes',
|
||||||
|
() async {
|
||||||
|
final profiles = <ProfileModel>[_profileA, _profileB];
|
||||||
|
when(
|
||||||
|
() => repository.getMyProfile(),
|
||||||
|
).thenAnswer((_) async => profiles.removeAt(0));
|
||||||
|
when(
|
||||||
|
() => repository.getMyProfileDetails(),
|
||||||
|
).thenAnswer((_) async => _details);
|
||||||
|
|
||||||
|
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||||
|
|
||||||
|
await cubit!.ensureFreshForCurrentSession();
|
||||||
|
final firstState = cubit!.state as ProfileAvatarStateLoaded;
|
||||||
|
expect(firstState.profile.id, _profileA.id);
|
||||||
|
|
||||||
|
await TokenStorage.saveTokenInStorage('token-b');
|
||||||
|
await cubit!.ensureFreshForCurrentSession();
|
||||||
|
|
||||||
|
final secondState = cubit!.state as ProfileAvatarStateLoaded;
|
||||||
|
expect(secondState.profile.id, _profileB.id);
|
||||||
|
expect(secondState.profile.profilePhotoUrl, _profileB.profilePhotoUrl);
|
||||||
|
verify(() => repository.getMyProfile()).called(2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('errors emit ProfileAvatarStateError and do not navigate', () async {
|
||||||
|
when(
|
||||||
|
() => repository.getMyProfile(),
|
||||||
|
).thenAnswer((_) async => throw Exception('profile request failed'));
|
||||||
|
|
||||||
|
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||||
|
|
||||||
|
await cubit!.getMyProfile();
|
||||||
|
|
||||||
|
expect(cubit!.state, isA<ProfileAvatarStateError>());
|
||||||
|
expect(
|
||||||
|
(cubit!.state as ProfileAvatarStateError).errorMessage,
|
||||||
|
contains('profile request failed'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||||
@@ -5,6 +6,7 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.dart
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import 'profile_avatar_cubit_test.mocks.dart';
|
import 'profile_avatar_cubit_test.mocks.dart';
|
||||||
|
|
||||||
@@ -16,6 +18,8 @@ void main() {
|
|||||||
late MockProfileRepository mockRepository;
|
late MockProfileRepository mockRepository;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues({'token': 'test-token'});
|
||||||
|
TokenStorage.clearCache();
|
||||||
mockRepository = MockProfileRepository();
|
mockRepository = MockProfileRepository();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.dart
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import 'menu_cubit_test.mocks.dart';
|
import 'menu_cubit_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateNiceMocks([MockSpec<ProfileRepository>()])
|
@GenerateNiceMocks([MockSpec<ProfileRepository>()])
|
||||||
@@ -25,12 +27,12 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('начальное состояние - MenuState', () {
|
test('начальное состояние - MenuState', () {
|
||||||
menuCubit = MenuCubit(mockProfileRepository);
|
menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService());
|
||||||
expect(menuCubit!.state, isA<MenuState>());
|
expect(menuCubit!.state, isA<MenuState>());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('makePassKey - успешно создает passKey', () async {
|
test('makePassKey - успешно создает passKey', () async {
|
||||||
menuCubit = MenuCubit(mockProfileRepository);
|
menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService());
|
||||||
const expectedPassKey = 'test-pass-key';
|
const expectedPassKey = 'test-pass-key';
|
||||||
|
|
||||||
when(mockProfileRepository.getPassKey(
|
when(mockProfileRepository.getPassKey(
|
||||||
@@ -48,7 +50,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('makePassKey - возвращает null при ошибке', () async {
|
test('makePassKey - возвращает null при ошибке', () async {
|
||||||
menuCubit = MenuCubit(mockProfileRepository);
|
menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService());
|
||||||
|
|
||||||
when(mockProfileRepository.getPassKey(
|
when(mockProfileRepository.getPassKey(
|
||||||
routeName: 'product_nutritional_value.index',
|
routeName: 'product_nutritional_value.index',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import 'menu_screen_smoke_test.mocks.dart';
|
import 'menu_screen_smoke_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateNiceMocks([MockSpec<ProfileRepository>()])
|
@GenerateNiceMocks([MockSpec<ProfileRepository>()])
|
||||||
@@ -138,7 +139,7 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('renders menu screen for user', (tester) async {
|
testWidgets('renders menu screen for user', (tester) async {
|
||||||
final repository = MockProfileRepository();
|
final repository = MockProfileRepository();
|
||||||
final cubit = MenuCubit(repository);
|
final cubit = MenuCubit(repository, FakeSessionLifecycleService());
|
||||||
|
|
||||||
when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey');
|
when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey');
|
||||||
when(repository.getMyProfile()).thenAnswer((_) async => mockUserProfile);
|
when(repository.getMyProfile()).thenAnswer((_) async => mockUserProfile);
|
||||||
@@ -155,7 +156,7 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('renders menu screen for consultant', (tester) async {
|
testWidgets('renders menu screen for consultant', (tester) async {
|
||||||
final repository = MockProfileRepository();
|
final repository = MockProfileRepository();
|
||||||
final cubit = MenuCubit(repository);
|
final cubit = MenuCubit(repository, FakeSessionLifecycleService());
|
||||||
|
|
||||||
when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey');
|
when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey');
|
||||||
when(repository.getMyProfile()).thenAnswer((_) async => mockConsultantProfile);
|
when(repository.getMyProfile()).thenAnswer((_) async => mockConsultantProfile);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
|
|
||||||
|
import '../../fixtures/doubles/session_doubles.dart';
|
||||||
import 'basic_widget_test.mocks.dart';
|
import 'basic_widget_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateMocks([RegistrationRepository, Prefs])
|
@GenerateMocks([RegistrationRepository, Prefs])
|
||||||
@@ -19,7 +20,7 @@ void main() {
|
|||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
mockRegistrationRepository = MockRegistrationRepository();
|
mockRegistrationRepository = MockRegistrationRepository();
|
||||||
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
|
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService());
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
@@ -38,7 +39,7 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('should create RegistrationCubit successfully', (WidgetTester tester) async {
|
testWidgets('should create RegistrationCubit successfully', (WidgetTester tester) async {
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
final cubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
|
final cubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(cubit, isA<RegistrationCubit>());
|
expect(cubit, isA<RegistrationCubit>());
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
|
|
||||||
|
import '../../fixtures/doubles/session_doubles.dart';
|
||||||
import 'integration_widget_test.mocks.dart';
|
import 'integration_widget_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateMocks([RegistrationRepository, Prefs])
|
@GenerateMocks([RegistrationRepository, Prefs])
|
||||||
@@ -17,7 +18,7 @@ void main() {
|
|||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
mockRegistrationRepository = MockRegistrationRepository();
|
mockRegistrationRepository = MockRegistrationRepository();
|
||||||
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
|
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService());
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:eatvio_app/features/registration/repositories/registration_repos
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../../../../fixtures/doubles/session_doubles.dart';
|
||||||
|
|
||||||
class MockRegistrationRepository extends Mock implements RegistrationRepository {}
|
class MockRegistrationRepository extends Mock implements RegistrationRepository {}
|
||||||
|
|
||||||
class MockPrefs extends Mock implements Prefs {}
|
class MockPrefs extends Mock implements Prefs {}
|
||||||
@@ -26,7 +28,7 @@ void main() {
|
|||||||
setUp(() {
|
setUp(() {
|
||||||
mockRepository = MockRegistrationRepository();
|
mockRepository = MockRegistrationRepository();
|
||||||
mockPrefs = MockPrefs();
|
mockPrefs = MockPrefs();
|
||||||
cubit = RegistrationCubit(registrationRepository: mockRepository, prefs: mockPrefs);
|
cubit = RegistrationCubit(registrationRepository: mockRepository, prefs: mockPrefs, sessionLifecycleService: FakeSessionLifecycleService());
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
|
|
||||||
|
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import 'registration_screen_smoke_test.mocks.dart';
|
import 'registration_screen_smoke_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateNiceMocks([MockSpec<RegistrationRepository>(), MockSpec<Prefs>()])
|
@GenerateNiceMocks([MockSpec<RegistrationRepository>(), MockSpec<Prefs>()])
|
||||||
@@ -44,7 +45,7 @@ void main() {
|
|||||||
testWidgets('renders registration screen with initial step', (tester) async {
|
testWidgets('renders registration screen with initial step', (tester) async {
|
||||||
final repository = MockRegistrationRepository();
|
final repository = MockRegistrationRepository();
|
||||||
final prefs = MockPrefs();
|
final prefs = MockPrefs();
|
||||||
final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs);
|
final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs, sessionLifecycleService: FakeSessionLifecycleService());
|
||||||
|
|
||||||
sl.registerLazySingleton<RegistrationRepository>(() => repository);
|
sl.registerLazySingleton<RegistrationRepository>(() => repository);
|
||||||
sl.registerLazySingleton<Prefs>(() => prefs);
|
sl.registerLazySingleton<Prefs>(() => prefs);
|
||||||
@@ -59,7 +60,7 @@ void main() {
|
|||||||
testWidgets('renders phone screen as first step', (tester) async {
|
testWidgets('renders phone screen as first step', (tester) async {
|
||||||
final repository = MockRegistrationRepository();
|
final repository = MockRegistrationRepository();
|
||||||
final prefs = MockPrefs();
|
final prefs = MockPrefs();
|
||||||
final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs);
|
final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs, sessionLifecycleService: FakeSessionLifecycleService());
|
||||||
|
|
||||||
sl.registerLazySingleton<RegistrationRepository>(() => repository);
|
sl.registerLazySingleton<RegistrationRepository>(() => repository);
|
||||||
sl.registerLazySingleton<Prefs>(() => prefs);
|
sl.registerLazySingleton<Prefs>(() => prefs);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
|
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
|
||||||
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
|
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -7,45 +9,66 @@ import 'package:mockito/annotations.dart';
|
|||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../../fixtures/doubles/session_doubles.dart';
|
||||||
import 'registration_cubit_v2_test.mocks.dart';
|
import 'registration_cubit_v2_test.mocks.dart';
|
||||||
|
|
||||||
|
class _RecordingFirebaseApi extends Fake implements FirebaseApi {
|
||||||
|
int registerPushTokenCalls = 0;
|
||||||
|
bool hadAuthToken = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> registerPushToken() async {
|
||||||
|
registerPushTokenCalls++;
|
||||||
|
hadAuthToken = await TokenStorage.haveTokenInStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateMocks([RegistrationRepositoryV2])
|
@GenerateMocks([RegistrationRepositoryV2])
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
group('RegistrationCubitV2', () {
|
group('RegistrationCubitV2', () {
|
||||||
late RegistrationCubitV2 cubit;
|
late RegistrationCubitV2 cubit;
|
||||||
late MockRegistrationRepositoryV2 mockRepository;
|
late MockRegistrationRepositoryV2 mockRepository;
|
||||||
const MethodChannel passwordGeneratorChannel = MethodChannel('flutter_password_generator');
|
late _RecordingFirebaseApi firebaseApi;
|
||||||
|
late FakeSessionLifecycleService fakeSession;
|
||||||
|
const MethodChannel passwordGeneratorChannel = MethodChannel(
|
||||||
|
'flutter_password_generator',
|
||||||
|
);
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
mockRepository = MockRegistrationRepositoryV2();
|
mockRepository = MockRegistrationRepositoryV2();
|
||||||
cubit = RegistrationCubitV2(registrationRepository: mockRepository);
|
firebaseApi = _RecordingFirebaseApi();
|
||||||
|
fakeSession = FakeSessionLifecycleService();
|
||||||
|
cubit = RegistrationCubitV2(
|
||||||
|
registrationRepository: mockRepository,
|
||||||
|
firebaseApi: firebaseApi,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
|
);
|
||||||
|
|
||||||
// Инициализируем SharedPreferences для тестов
|
// Инициализируем SharedPreferences для тестов
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
|
||||||
// Мокируем MethodChannel для flutter_password_generator
|
// Мокируем MethodChannel для flutter_password_generator
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
passwordGeneratorChannel,
|
.setMockMethodCallHandler(passwordGeneratorChannel, (
|
||||||
(MethodCall methodCall) async {
|
MethodCall methodCall,
|
||||||
if (methodCall.method == 'generateSecureRandomData') {
|
) async {
|
||||||
// Возвращаем Uint8List для генерации пароля
|
if (methodCall.method == 'generateSecureRandomData') {
|
||||||
// MethodChannel автоматически преобразует Uint8List в List<int>
|
// Возвращаем Uint8List для генерации пароля
|
||||||
return Uint8List.fromList(List<int>.generate(32, (i) => i % 256));
|
// MethodChannel автоматически преобразует Uint8List в List<int>
|
||||||
}
|
return Uint8List.fromList(List<int>.generate(32, (i) => i % 256));
|
||||||
return null;
|
}
|
||||||
},
|
return null;
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
|
TokenStorage.clearCache();
|
||||||
cubit.close();
|
cubit.close();
|
||||||
// Очищаем моки
|
// Очищаем моки
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
passwordGeneratorChannel,
|
.setMockMethodCallHandler(passwordGeneratorChannel, null);
|
||||||
null,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('начальное состояние', () {
|
test('начальное состояние', () {
|
||||||
@@ -182,14 +205,23 @@ void main() {
|
|||||||
cubit.setBirthday(DateTime(1990, 1, 1));
|
cubit.setBirthday(DateTime(1990, 1, 1));
|
||||||
|
|
||||||
const token = 'test-token';
|
const token = 'test-token';
|
||||||
when(mockRepository.register(registrationModel: anyNamed('registrationModel')))
|
when(
|
||||||
.thenAnswer((_) async => token);
|
mockRepository.register(
|
||||||
|
registrationModel: anyNamed('registrationModel'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => token);
|
||||||
|
|
||||||
final result = await cubit.register();
|
final result = await cubit.register();
|
||||||
|
|
||||||
expect(result.$1, isTrue);
|
expect(result.$1, isTrue);
|
||||||
expect(result.$2, isFalse);
|
expect(result.$2, isFalse);
|
||||||
verify(mockRepository.register(registrationModel: anyNamed('registrationModel'))).called(1);
|
expect(await TokenStorage.getTokenFromStorage(), token);
|
||||||
|
expect(fakeSession.onAuthenticatedCalls, 1);
|
||||||
|
verify(
|
||||||
|
mockRepository.register(
|
||||||
|
registrationModel: anyNamed('registrationModel'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('register - возвращает false при невалидных полях', () async {
|
test('register - возвращает false при невалидных полях', () async {
|
||||||
@@ -199,7 +231,11 @@ void main() {
|
|||||||
|
|
||||||
expect(result.$1, isFalse);
|
expect(result.$1, isFalse);
|
||||||
expect(result.$2, isTrue);
|
expect(result.$2, isTrue);
|
||||||
verifyNever(mockRepository.register(registrationModel: anyNamed('registrationModel')));
|
verifyNever(
|
||||||
|
mockRepository.register(
|
||||||
|
registrationModel: anyNamed('registrationModel'),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('register - обработка ошибки валидации email', () async {
|
test('register - обработка ошибки валидации email', () async {
|
||||||
@@ -226,16 +262,21 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockRepository.register(registrationModel: anyNamed('registrationModel')))
|
when(
|
||||||
.thenThrow(dioException);
|
mockRepository.register(
|
||||||
|
registrationModel: anyNamed('registrationModel'),
|
||||||
|
),
|
||||||
|
).thenThrow(dioException);
|
||||||
|
|
||||||
final result = await cubit.register();
|
final result = await cubit.register();
|
||||||
|
|
||||||
expect(result.$1, isFalse);
|
expect(result.$1, isFalse);
|
||||||
expect(result.$2, isTrue);
|
expect(result.$2, isTrue);
|
||||||
expect(cubit.state.isEmailValid, isFalse);
|
expect(cubit.state.isEmailValid, isFalse);
|
||||||
expect(cubit.state.emailDisplayError, equals('Некорректный формат email'));
|
expect(
|
||||||
|
cubit.state.emailDisplayError,
|
||||||
|
equals('Некорректный формат email'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
||||||
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
||||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||||
@@ -7,6 +8,7 @@ import 'package:eatvio_app/features/settings/repositories/service_repository.dar
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
|
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import '../../../../fixtures/settings_fixtures.dart';
|
import '../../../../fixtures/settings_fixtures.dart';
|
||||||
import 'settings_cubit_test.mocks.dart';
|
import 'settings_cubit_test.mocks.dart';
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ void main() {
|
|||||||
late MockSettingsRepository mockSettingsRepository;
|
late MockSettingsRepository mockSettingsRepository;
|
||||||
late MockConsultantRepository mockConsultantRepository;
|
late MockConsultantRepository mockConsultantRepository;
|
||||||
late MockConsultationsRepository mockConsultationsRepository;
|
late MockConsultationsRepository mockConsultationsRepository;
|
||||||
|
late FakeSessionLifecycleService fakeSession;
|
||||||
|
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
@@ -30,6 +33,7 @@ void main() {
|
|||||||
mockSettingsRepository = MockSettingsRepository();
|
mockSettingsRepository = MockSettingsRepository();
|
||||||
mockConsultantRepository = MockConsultantRepository();
|
mockConsultantRepository = MockConsultantRepository();
|
||||||
mockConsultationsRepository = MockConsultationsRepository();
|
mockConsultationsRepository = MockConsultationsRepository();
|
||||||
|
fakeSession = FakeSessionLifecycleService();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
@@ -46,6 +50,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
expect(settingsCubit!.state, const SettingsState.loading());
|
expect(settingsCubit!.state, const SettingsState.loading());
|
||||||
});
|
});
|
||||||
@@ -56,6 +61,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockProfileRepository.getMyProfile())
|
when(mockProfileRepository.getMyProfile())
|
||||||
@@ -86,6 +92,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ошибка происходит при загрузке настроек профиля (внутри try-catch блока)
|
// Ошибка происходит при загрузке настроек профиля (внутри try-catch блока)
|
||||||
@@ -114,6 +121,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockProfileRepository.getMyProfile())
|
when(mockProfileRepository.getMyProfile())
|
||||||
@@ -145,6 +153,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockProfileRepository.getMyProfile())
|
when(mockProfileRepository.getMyProfile())
|
||||||
@@ -176,6 +185,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockProfileRepository.deleteAccount())
|
when(mockProfileRepository.deleteAccount())
|
||||||
@@ -184,6 +194,8 @@ void main() {
|
|||||||
final result = await settingsCubit!.deleteAccount();
|
final result = await settingsCubit!.deleteAccount();
|
||||||
|
|
||||||
expect(result, true);
|
expect(result, true);
|
||||||
|
expect(fakeSession.endSessionCalls, 1);
|
||||||
|
expect(fakeSession.lastEndReason, SessionEndReason.accountDeleted);
|
||||||
verify(mockProfileRepository.deleteAccount()).called(1);
|
verify(mockProfileRepository.deleteAccount()).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,6 +205,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockProfileRepository.deleteAccount())
|
when(mockProfileRepository.deleteAccount())
|
||||||
@@ -209,6 +222,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockProfileRepository.getMyProfile())
|
when(mockProfileRepository.getMyProfile())
|
||||||
@@ -244,6 +258,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: fakeSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockProfileRepository.getMyProfile())
|
when(mockProfileRepository.getMyProfile())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import '../../../../fixtures/settings_fixtures.dart';
|
import '../../../../fixtures/settings_fixtures.dart';
|
||||||
import '../bloc/settings_cubit_test.mocks.dart';
|
import '../bloc/settings_cubit_test.mocks.dart';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ void main() {
|
|||||||
settingsRepository: mockSettingsRepository,
|
settingsRepository: mockSettingsRepository,
|
||||||
consultantRepository: mockConsultantRepository,
|
consultantRepository: mockConsultantRepository,
|
||||||
consultationsRepository: mockConsultationsRepository,
|
consultationsRepository: mockConsultationsRepository,
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||||
import 'settings_screen_smoke_test.mocks.dart';
|
import 'settings_screen_smoke_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateNiceMocks([
|
@GenerateNiceMocks([
|
||||||
@@ -57,6 +58,7 @@ void main() {
|
|||||||
settingsRepository: sl(),
|
settingsRepository: sl(),
|
||||||
consultantRepository: sl(),
|
consultantRepository: sl(),
|
||||||
consultationsRepository: sl(),
|
consultationsRepository: sl(),
|
||||||
|
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
24
test/fixtures/doubles/session_doubles.dart
vendored
Normal file
24
test/fixtures/doubles/session_doubles.dart
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||||
|
|
||||||
|
class FakeSessionLifecycleService implements SessionLifecycleService {
|
||||||
|
int onAuthenticatedCalls = 0;
|
||||||
|
int endSessionCalls = 0;
|
||||||
|
int logoutCalls = 0;
|
||||||
|
SessionEndReason? lastEndReason;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onAuthenticated() async {
|
||||||
|
onAuthenticatedCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> endSession({required SessionEndReason reason}) async {
|
||||||
|
endSessionCalls++;
|
||||||
|
lastEndReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> logout() async {
|
||||||
|
logoutCalls++;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user