diff --git a/DevLog/App/Assembler/DataAssembler.swift b/DevLog/App/Assembler/DataAssembler.swift index ebe70a5b..5abd368d 100644 --- a/DevLog/App/Assembler/DataAssembler.swift +++ b/DevLog/App/Assembler/DataAssembler.swift @@ -34,8 +34,7 @@ final class DataAssembler: Assembler { container.register(AuthSessionRepository.self) { AuthSessionRepositoryImpl( - authService: container.resolve(AuthService.self), - userDefaultsStore: container.resolve(UserDefaultsStore.self) + authService: container.resolve(AuthService.self) ) } diff --git a/DevLog/App/Assembler/DomainAssembler.swift b/DevLog/App/Assembler/DomainAssembler.swift index 30d60f15..c13e1693 100644 --- a/DevLog/App/Assembler/DomainAssembler.swift +++ b/DevLog/App/Assembler/DomainAssembler.swift @@ -139,14 +139,6 @@ private extension DomainAssembler { UpdateSystemThemeUseCaseImpl(container.resolve(UserPreferencesRepository.self)) } - container.register(FetchFirstLaunchUseCase.self) { - FetchFirstLaunchUseCaseImpl(container.resolve(UserPreferencesRepository.self)) - } - - container.register(UpdateFirstLaunchUseCase.self) { - UpdateFirstLaunchUseCaseImpl(container.resolve(UserPreferencesRepository.self)) - } - container.register(FetchRecentSearchQueriesUseCase.self) { FetchRecentSearchQueriesUseCaseImpl(container.resolve(UserPreferencesRepository.self)) } diff --git a/DevLog/App/DevLogApp.swift b/DevLog/App/DevLogApp.swift index 11bf5137..52d8e8d6 100644 --- a/DevLog/App/DevLogApp.swift +++ b/DevLog/App/DevLogApp.swift @@ -20,11 +20,7 @@ struct DevLogApp: App { WindowGroup { RootView(viewModel: RootViewModel( sessionUseCase: container.resolve(AuthSessionUseCase.self), - signOutUseCase: container.resolve(SignOutUseCase.self), - fetchFirstLaunchUseCase: container.resolve(FetchFirstLaunchUseCase.self), - updateFirstLaunchUseCase: container.resolve(UpdateFirstLaunchUseCase.self), - observeSystemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self), - updateSystemThemeUseCase: container.resolve(UpdateSystemThemeUseCase.self) + observeSystemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self) )) .autocorrectionDisabled() } diff --git a/DevLog/App/RootView.swift b/DevLog/App/RootView.swift index 53e2c68b..4730ec88 100644 --- a/DevLog/App/RootView.swift +++ b/DevLog/App/RootView.swift @@ -16,32 +16,19 @@ struct RootView: View { ZStack { Color(UIColor.systemGroupedBackground).ignoresSafeArea() if let signIn = viewModel.state.signIn { - if signIn && !viewModel.state.isFirstLaunch { + if signIn { MainView(viewModel: MainViewModel( observeUnreadPushCountUseCase: container.resolve(ObserveUnreadPushCountUseCase.self) )) } else { LoginView(viewModel: LoginViewModel( - signInUseCase: container.resolve(SignInUseCase.self), - signOutUseCase: container.resolve(SignOutUseCase.self), - sessionUseCase: container.resolve(AuthSessionUseCase.self)) + signInUseCase: container.resolve(SignInUseCase.self)) ) - .onAppear { - viewModel.send(.onAppear) - } - } - } else { - Color.clear.onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { - if viewModel.state.signIn == nil { - viewModel.send(.setFirstLaunch(true)) - viewModel.send(.signOutAuto) - } - } } } } .preferredColorScheme(viewModel.state.theme.colorScheme) + .onAppear { viewModel.send(.onAppear) } .alert(viewModel.state.alertTitle, isPresented: Binding( get: { viewModel.state.showAlert }, set: { viewModel.send(.setAlert($0)) } @@ -50,12 +37,6 @@ struct RootView: View { } message: { Text(viewModel.state.alertMessage) } - .onChange(of: viewModel.state.isFirstLaunch) { _, newValue in - if newValue { - viewModel.send(.setFirstLaunch(false)) - viewModel.send(.signOutAuto) - } - } .sheet(item: $selectedRoute) { route in switch route { case .todoDetail(let todoId): diff --git a/DevLog/Data/Repository/AuthSessionRepositoryImpl.swift b/DevLog/Data/Repository/AuthSessionRepositoryImpl.swift index ca15eec6..3318af6a 100644 --- a/DevLog/Data/Repository/AuthSessionRepositoryImpl.swift +++ b/DevLog/Data/Repository/AuthSessionRepositoryImpl.swift @@ -9,24 +9,12 @@ import Combine final class AuthSessionRepositoryImpl: AuthSessionRepository { private let authService: AuthService - private let userDefaultsStore: UserDefaultsStore - init(authService: AuthService, userDefaultsStore: UserDefaultsStore) { + init(authService: AuthService) { self.authService = authService - self.userDefaultsStore = userDefaultsStore - self.signIn = authService.uid != nil } - @Published private var signIn: Bool = false - var signedInPublisher: AnyPublisher { - $signIn.eraseToAnyPublisher() - } - - func setSession(_ signedIn: Bool) { - if !signedIn { - userDefaultsStore.removeAll() - } - self.signIn = signedIn + authService.signedInPublisher } } diff --git a/DevLog/Data/Repository/AuthenticationRepositoryImpl.swift b/DevLog/Data/Repository/AuthenticationRepositoryImpl.swift index f2ab79e9..ce02d5d6 100644 --- a/DevLog/Data/Repository/AuthenticationRepositoryImpl.swift +++ b/DevLog/Data/Repository/AuthenticationRepositoryImpl.swift @@ -44,16 +44,21 @@ final class AuthenticationRepositoryImpl: AuthenticationRepository { let providerID = try await authService.getProviderID(), let provider = AuthProvider(rawValue: providerID) else { - throw AuthError.notAuthenticated + try await authService.clearCurrentSession() + return } - switch provider { - case .apple: - try await appleAuthService.signOut(uid) - case .github: - try await githubAuthService.signOut(uid) - case .google: - try await googleAuthService.signOut(uid) + do { + switch provider { + case .apple: + try await appleAuthService.signOut(uid) + case .github: + try await githubAuthService.signOut(uid) + case .google: + try await googleAuthService.signOut(uid) + } + } catch AuthError.notAuthenticated { + try await authService.clearCurrentSession() } } diff --git a/DevLog/Data/Repository/UserPreferencesRepositoryImpl.swift b/DevLog/Data/Repository/UserPreferencesRepositoryImpl.swift index 7c6669ec..aa9e4bc7 100644 --- a/DevLog/Data/Repository/UserPreferencesRepositoryImpl.swift +++ b/DevLog/Data/Repository/UserPreferencesRepositoryImpl.swift @@ -11,7 +11,6 @@ import Combine final class UserPreferencesRepositoryImpl: UserPreferencesRepository { private enum Key { static let theme = "theme" - static let firstLaunch = "isFirstLaunch" static let recentQueries = "Search.recentQueries" static let pushSortOrder = "PushNotification.sortOption" static let pushTimeFilter = "PushNotification.timeFilter" @@ -50,17 +49,6 @@ final class UserPreferencesRepositoryImpl: UserPreferencesRepository { themeStore.send(theme) } - func isFirstLaunch() -> Bool { - if store.string(forKey: Key.firstLaunch) == nil { - return true - } - return store.bool(forKey: Key.firstLaunch) - } - - func setFirstLaunch(_ value: Bool) { - store.setBool(value, forKey: Key.firstLaunch) - } - func recentSearchQueries() -> [String] { store.stringArray(forKey: Key.recentQueries) } diff --git a/DevLog/Domain/Protocol/AuthSessionRepository.swift b/DevLog/Domain/Protocol/AuthSessionRepository.swift index 363c10ba..54e1f75a 100644 --- a/DevLog/Domain/Protocol/AuthSessionRepository.swift +++ b/DevLog/Domain/Protocol/AuthSessionRepository.swift @@ -9,5 +9,4 @@ import Combine protocol AuthSessionRepository { var signedInPublisher: AnyPublisher { get } - func setSession(_ signedIn: Bool) } diff --git a/DevLog/Domain/Protocol/UserPreferencesRepository.swift b/DevLog/Domain/Protocol/UserPreferencesRepository.swift index 2f0a700e..c35a2f43 100644 --- a/DevLog/Domain/Protocol/UserPreferencesRepository.swift +++ b/DevLog/Domain/Protocol/UserPreferencesRepository.swift @@ -13,9 +13,6 @@ protocol UserPreferencesRepository { func systemTheme() -> SystemTheme func setSystemTheme(_ theme: SystemTheme) - func isFirstLaunch() -> Bool - func setFirstLaunch(_ value: Bool) - func recentSearchQueries() -> [String] func setRecentSearchQueries(_ queries: [String]) diff --git a/DevLog/Domain/UseCase/Auth/Session/AuthSessionUseCase.swift b/DevLog/Domain/UseCase/Auth/Session/AuthSessionUseCase.swift index 9880527b..a97e3905 100644 --- a/DevLog/Domain/UseCase/Auth/Session/AuthSessionUseCase.swift +++ b/DevLog/Domain/UseCase/Auth/Session/AuthSessionUseCase.swift @@ -9,5 +9,4 @@ import Combine protocol AuthSessionUseCase { var signedInPublisher: AnyPublisher { get } - func execute(_ signIn: Bool) } diff --git a/DevLog/Domain/UseCase/Auth/Session/AuthSessionUseCaseImpl.swift b/DevLog/Domain/UseCase/Auth/Session/AuthSessionUseCaseImpl.swift index f6c2b062..c3754e1c 100644 --- a/DevLog/Domain/UseCase/Auth/Session/AuthSessionUseCaseImpl.swift +++ b/DevLog/Domain/UseCase/Auth/Session/AuthSessionUseCaseImpl.swift @@ -17,8 +17,4 @@ final class AuthSessionUseCaseImpl: AuthSessionUseCase { init(_ repository: AuthSessionRepository) { self.repository = repository } - - func execute(_ signIn: Bool) { - repository.setSession(signIn) - } } diff --git a/DevLog/Domain/UseCase/UserPreferences/Launch/FetchFirstLaunchUseCase.swift b/DevLog/Domain/UseCase/UserPreferences/Launch/FetchFirstLaunchUseCase.swift deleted file mode 100644 index b74ec202..00000000 --- a/DevLog/Domain/UseCase/UserPreferences/Launch/FetchFirstLaunchUseCase.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// FetchFirstLaunchUseCase.swift -// DevLog -// -// Created by 최윤진 on 2/25/26. -// - -protocol FetchFirstLaunchUseCase { - func execute() -> Bool -} diff --git a/DevLog/Domain/UseCase/UserPreferences/Launch/FetchFirstLaunchUseCaseImpl.swift b/DevLog/Domain/UseCase/UserPreferences/Launch/FetchFirstLaunchUseCaseImpl.swift deleted file mode 100644 index dd9c043a..00000000 --- a/DevLog/Domain/UseCase/UserPreferences/Launch/FetchFirstLaunchUseCaseImpl.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FetchFirstLaunchUseCaseImpl.swift -// DevLog -// -// Created by 최윤진 on 2/25/26. -// - -final class FetchFirstLaunchUseCaseImpl: FetchFirstLaunchUseCase { - private let repository: UserPreferencesRepository - - init(_ repository: UserPreferencesRepository) { - self.repository = repository - } - - func execute() -> Bool { - repository.isFirstLaunch() - } -} diff --git a/DevLog/Domain/UseCase/UserPreferences/Launch/UpdateFirstLaunchUseCase.swift b/DevLog/Domain/UseCase/UserPreferences/Launch/UpdateFirstLaunchUseCase.swift deleted file mode 100644 index 37b1ffe8..00000000 --- a/DevLog/Domain/UseCase/UserPreferences/Launch/UpdateFirstLaunchUseCase.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// UpdateFirstLaunchUseCase.swift -// DevLog -// -// Created by 최윤진 on 2/25/26. -// - -protocol UpdateFirstLaunchUseCase { - func execute(_ value: Bool) -} diff --git a/DevLog/Domain/UseCase/UserPreferences/Launch/UpdateFirstLaunchUseCaseImpl.swift b/DevLog/Domain/UseCase/UserPreferences/Launch/UpdateFirstLaunchUseCaseImpl.swift deleted file mode 100644 index beb122a5..00000000 --- a/DevLog/Domain/UseCase/UserPreferences/Launch/UpdateFirstLaunchUseCaseImpl.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// UpdateFirstLaunchUseCaseImpl.swift -// DevLog -// -// Created by 최윤진 on 2/25/26. -// - -final class UpdateFirstLaunchUseCaseImpl: UpdateFirstLaunchUseCase { - private let repository: UserPreferencesRepository - - init(_ repository: UserPreferencesRepository) { - self.repository = repository - } - - func execute(_ value: Bool) { - repository.setFirstLaunch(value) - } -} diff --git a/DevLog/Infra/Service/AuthService.swift b/DevLog/Infra/Service/AuthService.swift index 488e448f..84405908 100644 --- a/DevLog/Infra/Service/AuthService.swift +++ b/DevLog/Infra/Service/AuthService.swift @@ -5,6 +5,7 @@ // Created by 최윤진 on 11/29/25. // +import Combine import FirebaseAuth import FirebaseFirestore import FirebaseMessaging @@ -13,15 +14,34 @@ final class AuthService { private let store = Firestore.firestore() private let messaging = Messaging.messaging() private let logger = Logger(category: "AuthService") + private let subject = CurrentValueSubject(Auth.auth().currentUser != nil) + private var handler: AuthStateDidChangeListenerHandle? var uid: String? { Auth.auth().currentUser?.uid } + var signedInPublisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + var providerIDs: [String] { Auth.auth().currentUser?.providerData.map { $0.providerID } ?? [] } + init() { + handler = Auth.auth().addStateDidChangeListener { [weak self] _, user in + let signedIn = user != nil + self?.logger.info("Firebase auth state changed. signedIn: \(signedIn)") + self?.subject.send(signedIn) + } + } + + deinit { + guard let handler else { return } + Auth.auth().removeStateDidChangeListener(handler) + } + func getProviderID() async throws -> String? { logger.info("Fetching current provider ID") diff --git a/DevLog/Presentation/ViewModel/LoginViewModel.swift b/DevLog/Presentation/ViewModel/LoginViewModel.swift index a1fd81c8..bf92e3bc 100644 --- a/DevLog/Presentation/ViewModel/LoginViewModel.swift +++ b/DevLog/Presentation/ViewModel/LoginViewModel.swift @@ -5,14 +5,11 @@ // Created by 최윤진 on 11/14/25. // -import Combine import Foundation -import FirebaseAuth @Observable final class LoginViewModel: Store { struct State: Equatable { - var signIn: Bool? var isLoading = false var showAlert: Bool = false var alertTitle: String = "" @@ -20,42 +17,23 @@ final class LoginViewModel: Store { } enum Action { - case signOutAuto case setAlert(Bool) case tapSignInButton(AuthProvider) - case tapSignOutButton case setLoading(Bool) - case setLogined(Bool) } enum SideEffect { case signIn(AuthProvider) - case signOut } private let signInUseCase: SignInUseCase - private let signOutUseCase: SignOutUseCase - private let sessionUseCase: AuthSessionUseCase private(set) var state = State() - private var cancellables = Set() init( - signInUseCase: SignInUseCase, - signOutUseCase: SignOutUseCase, - sessionUseCase: AuthSessionUseCase + signInUseCase: SignInUseCase ) { self.signInUseCase = signInUseCase - self.signOutUseCase = signOutUseCase - self.sessionUseCase = sessionUseCase - - self.sessionUseCase.signedInPublisher - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] signIn in - self?.send(.setLogined(signIn)) - } - .store(in: &cancellables) } func reduce(with action: Action) -> [SideEffect] { @@ -67,12 +45,8 @@ final class LoginViewModel: Store { setAlert(&state, isPresented: isPresented) case .tapSignInButton(let authProvider): effects = [.signIn(authProvider)] - case .tapSignOutButton, .signOutAuto: - effects = [.signOut] case .setLoading(let value): state.isLoading = value - case .setLogined(let result): - state.signIn = result } if self.state != state { self.state = state } @@ -87,26 +61,11 @@ final class LoginViewModel: Store { do { defer { send(.setLoading(false)) } try await self.signInUseCase.execute(authProvider) - send(.setLogined(true)) - sessionUseCase.execute(true) } catch { - send(.setLogined(false)) - sessionUseCase.execute(false) if error.isSocialLoginCancelled { return } send(.setAlert(true)) } } - case .signOut: - Task { - do { - defer { send(.setLoading(false)) } - try await self.signOutUseCase.execute() - send(.setLogined(false)) - sessionUseCase.execute(false) - } catch { - send(.setAlert(true)) - } - } } } } diff --git a/DevLog/Presentation/ViewModel/RootViewModel.swift b/DevLog/Presentation/ViewModel/RootViewModel.swift index 645ed295..99b943db 100644 --- a/DevLog/Presentation/ViewModel/RootViewModel.swift +++ b/DevLog/Presentation/ViewModel/RootViewModel.swift @@ -16,7 +16,6 @@ final class RootViewModel: Store { var alertTitle: String = "" var alertMessage: String = "" var isNetworkConnected: Bool = true - var isFirstLaunch: Bool var signIn: Bool? var theme: SystemTheme = .automatic } @@ -25,43 +24,27 @@ final class RootViewModel: Store { case onAppear case setAlert(Bool) case networkStatusChanged(Bool) - case setFirstLaunch(Bool) case setTheme(SystemTheme) - case signOutAuto case didLogined(Bool) } enum SideEffect { case clearApplicationBadgeCount - case signOut } private(set) var state: State private let connectivityProvider = NWPathConnectivityProvider() private var cancellables = Set() private let sessionUseCase: AuthSessionUseCase - private let signOutUseCase: SignOutUseCase - private let fetchFirstLaunchUseCase: FetchFirstLaunchUseCase - private let updateFirstLaunchUseCase: UpdateFirstLaunchUseCase private let observeSystemThemeUseCase: ObserveSystemThemeUseCase - private let updateSystemThemeUseCase: UpdateSystemThemeUseCase init( sessionUseCase: AuthSessionUseCase, - signOutUseCase: SignOutUseCase, - fetchFirstLaunchUseCase: FetchFirstLaunchUseCase, - updateFirstLaunchUseCase: UpdateFirstLaunchUseCase, - observeSystemThemeUseCase: ObserveSystemThemeUseCase, - updateSystemThemeUseCase: UpdateSystemThemeUseCase + observeSystemThemeUseCase: ObserveSystemThemeUseCase ) { - let isFirstLaunch = fetchFirstLaunchUseCase.execute() self.sessionUseCase = sessionUseCase - self.signOutUseCase = signOutUseCase - self.fetchFirstLaunchUseCase = fetchFirstLaunchUseCase - self.updateFirstLaunchUseCase = updateFirstLaunchUseCase self.observeSystemThemeUseCase = observeSystemThemeUseCase - self.updateSystemThemeUseCase = updateSystemThemeUseCase - self.state = State(isFirstLaunch: isFirstLaunch) + self.state = State() setupNetworkMonitoring() setupSessionMonitoring() @@ -75,11 +58,6 @@ final class RootViewModel: Store { switch action { case .onAppear: effects = [.clearApplicationBadgeCount] - if state.isFirstLaunch { - state.isFirstLaunch = false - updateFirstLaunchUseCase.execute(false) - effects.append(.signOut) - } case .setAlert(let isPresented): setAlert(&state, isPresented: isPresented) case .networkStatusChanged(let isConnected): @@ -88,13 +66,8 @@ final class RootViewModel: Store { if wasConnected && !isConnected { setAlert(&state, isPresented: true) } - case .setFirstLaunch(let value): - state.isFirstLaunch = value - updateFirstLaunchUseCase.execute(value) case .setTheme(let theme): state.theme = theme - case .signOutAuto: - effects = [.signOut] case .didLogined(let result): state.signIn = result } @@ -106,23 +79,13 @@ final class RootViewModel: Store { func run(_ effect: SideEffect) { switch effect { case .clearApplicationBadgeCount: - clearApplicationBadgeCount() - case .signOut: - Task { - try? await signOutUseCase.execute() - send(.didLogined(false)) - sessionUseCase.execute(false) - } + UNUserNotificationCenter.current().setBadgeCount(0) { _ in } } } } // MARK: - Helper Methods private extension RootViewModel { - func clearApplicationBadgeCount() { - UNUserNotificationCenter.current().setBadgeCount(0) { _ in } - } - func setAlert( _ state: inout State, isPresented: Bool diff --git a/DevLog/Presentation/ViewModel/SettingViewModel.swift b/DevLog/Presentation/ViewModel/SettingViewModel.swift index d83997d0..8500493b 100644 --- a/DevLog/Presentation/ViewModel/SettingViewModel.swift +++ b/DevLog/Presentation/ViewModel/SettingViewModel.swift @@ -43,7 +43,6 @@ final class SettingViewModel: Store { private(set) var state = State() private let deleteAuthuseCase: DeleteAuthUseCase private let signOutUseCase: SignOutUseCase - private let sessionUseCase: AuthSessionUseCase private let observeSystemThemeUseCase: ObserveSystemThemeUseCase private let updateSystemThemeUseCase: UpdateSystemThemeUseCase private let loadingState = LoadingState() @@ -56,13 +55,11 @@ final class SettingViewModel: Store { init( deleteAuthUseCase: DeleteAuthUseCase, signOutUseCase: SignOutUseCase, - sessionUseCase: AuthSessionUseCase, observeSystemThemeUseCase: ObserveSystemThemeUseCase, updateSystemThemeUseCase: UpdateSystemThemeUseCase ) { self.deleteAuthuseCase = deleteAuthUseCase self.signOutUseCase = signOutUseCase - self.sessionUseCase = sessionUseCase self.observeSystemThemeUseCase = observeSystemThemeUseCase self.updateSystemThemeUseCase = updateSystemThemeUseCase setupThemeMonitoring() @@ -111,7 +108,6 @@ final class SettingViewModel: Store { send(.setAlert(isPresented: false)) defer { endLoading(.delayed) } try await deleteAuthuseCase.execute() - sessionUseCase.execute(false) } catch { send(.setAlert(isPresented: true, type: .error)) } @@ -123,7 +119,6 @@ final class SettingViewModel: Store { send(.setAlert(isPresented: false)) defer { endLoading(.delayed) } try await signOutUseCase.execute() - sessionUseCase.execute(false) } catch { send(.setAlert(isPresented: true, type: .error)) } diff --git a/DevLog/Storage/Persistence/UserDefaultsStore.swift b/DevLog/Storage/Persistence/UserDefaultsStore.swift index 6ec74c49..4c964719 100644 --- a/DevLog/Storage/Persistence/UserDefaultsStore.swift +++ b/DevLog/Storage/Persistence/UserDefaultsStore.swift @@ -37,13 +37,4 @@ final class UserDefaultsStore { func setBool(_ value: Bool, forKey key: String) { userDefaults.set(value, forKey: key) } - - func removeAll() { - guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return } - let firstLaunch = userDefaults.object(forKey: "isFirstLaunch") - userDefaults.removePersistentDomain(forName: bundleIdentifier) - if let value = firstLaunch as? Bool { - userDefaults.set(value, forKey: "isFirstLaunch") - } - } } diff --git a/DevLog/UI/Profile/ProfileView.swift b/DevLog/UI/Profile/ProfileView.swift index a3508cf4..43e48aa1 100644 --- a/DevLog/UI/Profile/ProfileView.swift +++ b/DevLog/UI/Profile/ProfileView.swift @@ -98,7 +98,6 @@ struct ProfileView: View { SettingView(viewModel: SettingViewModel( deleteAuthUseCase: container.resolve(DeleteAuthUseCase.self), signOutUseCase: container.resolve(SignOutUseCase.self), - sessionUseCase: container.resolve(AuthSessionUseCase.self), observeSystemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self), updateSystemThemeUseCase: container.resolve(UpdateSystemThemeUseCase.self) ))