@@ -2,22 +2,68 @@ import Foundation
22
33@MainActor
44final class AppViewModel : ObservableObject {
5+ enum UpdatePrompt : Identifiable , Equatable {
6+ case force
7+ case optional
8+
9+ var id : String {
10+ switch self {
11+ case . force:
12+ return " force "
13+ case . optional:
14+ return " optional "
15+ }
16+ }
17+
18+ var title : String {
19+ switch self {
20+ case . force:
21+ return " 업데이트 필요 "
22+ case . optional:
23+ return " 업데이트 권장 "
24+ }
25+ }
26+
27+ var message : String {
28+ switch self {
29+ case . force:
30+ return " 안정적인 서비스 이용을 위해 최신 버전으로 업데이트해 주세요. "
31+ case . optional:
32+ return " 새 버전이 출시되었어요. 지금 업데이트하면 더 나은 경험을 이용할 수 있어요. "
33+ }
34+ }
35+ }
36+
537 @Published var currentStep : AppFlowStep = . splash
38+ @Published var updatePrompt : UpdatePrompt ?
39+ @Published var setupFlowViewModel : InitialSetupFlowViewModel ?
40+ @Published var isResolvingPostLoginFlow = false
641
742 let loginViewModel : LoginViewModel
843
944 private let tokenStore : TokenStoring
45+ private let userService : UserServicing
46+ private let diaryService : DiaryServicing
47+ private let calendarService : CalendarServicing
1048 private let notificationCenter : NotificationCenter
49+ private let appStoreURL : URL ?
1150 private var sessionExpiredObserver : NSObjectProtocol ?
1251
1352 init (
1453 authenticationService: AuthenticationServicing = AuthenticationService ( ) ,
1554 authService: AuthServicing = AuthService ( ) ,
55+ userService: UserServicing = UserService ( ) ,
56+ diaryService: DiaryServicing = DiaryService ( ) ,
57+ calendarService: CalendarServicing = CalendarService ( ) ,
1658 tokenStore: TokenStoring = TokenStore . shared,
1759 notificationCenter: NotificationCenter = . default
1860 ) {
1961 self . tokenStore = tokenStore
62+ self . userService = userService
63+ self . diaryService = diaryService
64+ self . calendarService = calendarService
2065 self . notificationCenter = notificationCenter
66+ self . appStoreURL = Self . resolveAppStoreURL ( )
2167
2268 let loginViewModel = LoginViewModel (
2369 authenticationService: authenticationService,
@@ -26,7 +72,9 @@ final class AppViewModel: ObservableObject {
2672
2773 self . loginViewModel = loginViewModel
2874 self . loginViewModel. onLoginSuccess = { [ weak self] _ in
29- self ? . currentStep = . main
75+ Task { @MainActor [ weak self] in
76+ await self ? . resolvePostLoginFlow ( )
77+ }
3078 }
3179
3280 sessionExpiredObserver = notificationCenter. addObserver (
@@ -47,15 +95,115 @@ final class AppViewModel: ObservableObject {
4795 }
4896
4997 func completeSplash( ) {
50- currentStep = tokenStore. hasSessionTokens ? . main : . onboarding
98+ guard tokenStore. hasSessionTokens else {
99+ currentStep = . login
100+ return
101+ }
102+
103+ Task {
104+ await resolvePostLoginFlow ( )
105+ }
106+ }
107+
108+ func resolvePostLoginFlow( ) async {
109+ guard !isResolvingPostLoginFlow else { return }
110+
111+ isResolvingPostLoginFlow = true
112+ loginViewModel. loginErrorMessage = nil
113+ defer { isResolvingPostLoginFlow = false }
114+
115+ do {
116+ let settings = try await userService. fetchInitSettings ( )
117+ applyRoute ( for: settings)
118+
119+ if settings. app. needsForceUpdate {
120+ updatePrompt = . force
121+ } else if settings. app. needsOptionalUpdate {
122+ updatePrompt = . optional
123+ }
124+ } catch {
125+ if isRequestCancelled ( error) { return }
126+ currentStep = . login
127+ setupFlowViewModel = nil
128+ loginViewModel. loginErrorMessage = resolveErrorMessage ( from: error)
129+ }
130+ }
131+
132+ func openAppStoreURLForUpdate( ) -> URL ? {
133+ appStoreURL
51134 }
52135
53- func completeOnboarding( ) {
54- currentStep = tokenStore. hasSessionTokens ? . main : . login
136+ func dismissOptionalUpdatePrompt( ) {
137+ guard updatePrompt == . optional else { return }
138+ updatePrompt = nil
55139 }
56140
57141 func logout( ) {
58142 loginViewModel. resetState ( )
143+ setupFlowViewModel = nil
144+ updatePrompt = nil
59145 currentStep = . login
60146 }
147+
148+ private func applyRoute( for settings: UserInitSettingsResponse ) {
149+ if settings. needsPolicyAgreement || settings. needsTagSetup {
150+ let setupViewModel = InitialSetupFlowViewModel (
151+ settings: settings,
152+ userService: userService,
153+ diaryService: diaryService,
154+ calendarService: calendarService
155+ )
156+ setupViewModel. onComplete = { [ weak self] in
157+ self ? . enterMainFlow ( )
158+ }
159+ setupFlowViewModel = setupViewModel
160+ currentStep = . setup
161+ return
162+ }
163+
164+ enterMainFlow ( )
165+ }
166+
167+ private func enterMainFlow( ) {
168+ setupFlowViewModel = nil
169+ currentStep = . main
170+ }
171+
172+ private func resolveErrorMessage( from error: Error ) -> String {
173+ if let userServiceError = error as? UserServiceError {
174+ return userServiceError. errorDescription ?? " 초기 설정 정보를 불러오지 못했어요. "
175+ }
176+
177+ if let apiError = error as? APIClientError {
178+ return apiError. errorDescription ?? " 초기 설정 정보를 불러오지 못했어요. "
179+ }
180+
181+ if let localizedError = error as? LocalizedError {
182+ return localizedError. errorDescription ?? " 초기 설정 정보를 불러오지 못했어요. "
183+ }
184+
185+ return " 초기 설정 정보를 불러오지 못했어요. "
186+ }
187+
188+ private func isRequestCancelled( _ error: Error ) -> Bool {
189+ if error is CancellationError { return true }
190+
191+ let nsError = error as NSError
192+ return nsError. domain == NSURLErrorDomain && nsError. code == NSURLErrorCancelled
193+ }
194+
195+ private static func resolveAppStoreURL( ) -> URL ? {
196+ guard
197+ let rawValue = Bundle . main. object ( forInfoDictionaryKey: " APP_STORE_URL " ) as? String
198+ else {
199+ return nil
200+ }
201+
202+ let trimmed = rawValue. trimmingCharacters ( in: . whitespacesAndNewlines)
203+ guard !trimmed. isEmpty, let url = URL ( string: trimmed) else {
204+ return nil
205+ }
206+
207+ return url
208+ }
61209}
0 commit comments