Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions DevLog/App/Assembler/DataAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,12 @@ final class DataAssembler: Assembler {
metadataService: container.resolve(WebPageMetadataService.self)
)
}

container.register(UserPreferencesRepository.self) {
UserPreferencesRepositoryImpl(
store: container.resolve(UserDefaultsStore.self),
themeStore: container.resolve(ThemeStore.self)
)
}
}
}
126 changes: 91 additions & 35 deletions DevLog/App/Assembler/DomainAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@

final class DomainAssembler: Assembler {
func assemble(_ container: DIContainer) {
container.register(FetchPinnedTodosUseCase.self) {
FetchPinnedTodosUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(FetchTodoByIDUseCase.self) {
FetchTodoByIDUseCaseImpl(container.resolve(TodoRepository.self))
}
registerAuthUseCases(container)
registerAuthProviderUseCases(container)
registerTodoUseCases(container)
registerUserDataUseCases(container)
registerPushNotificationUseCases(container)
registerWebPageUseCases(container)
registerUserPreferencesUseCases(container)
}
}

private extension DomainAssembler {
func registerAuthUseCases(_ container: DIContainer) {
container.register(SignInUseCase.self) {
SignInUseCaseImpl(container.resolve(AuthenticationRepository.self))
}
Expand All @@ -27,42 +31,84 @@ final class DomainAssembler: Assembler {
DeleteAuthUseCaseImpl(container.resolve(AuthenticationRepository.self))
}

container.register(AuthSessionUseCase.self) {
AuthSessionUseCaseImpl(container.resolve(AuthSessionRepository.self))
}
}

func registerAuthProviderUseCases(_ container: DIContainer) {
container.register(FetchAuthProvidersUseCase.self) {
FetchAuthProvidersUseCaseImpl(container.resolve(AuthDataRepository.self))
}

container.register(LinkAuthProviderUseCase.self) {
LinkAuthProviderUseCaseImpl(container.resolve(AuthDataRepository.self))
}

container.register(UnlinkAuthProviderUseCase.self) {
UnlinkAuthProviderUseCaseImpl(container.resolve(AuthDataRepository.self))
}
}

func registerTodoUseCases(_ container: DIContainer) {
container.register(FetchPinnedTodosUseCase.self) {
FetchPinnedTodosUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(FetchTodoByIDUseCase.self) {
FetchTodoByIDUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(FetchTodosByKindUseCase.self) {
FetchTodosByKindUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(FetchTodosByKeywordUseCase.self) {
FetchTodosByKeywordUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(UpsertTodoUseCase.self) {
UpsertTodoUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(DeleteTodoUseCase.self) {
DeleteTodoUseCaseImpl(container.resolve(TodoRepository.self))
}
}

container.register(AuthSessionUseCase.self) {
AuthSessionUseCaseImpl(container.resolve(AuthSessionRepository.self))
}

func registerUserDataUseCases(_ container: DIContainer) {
container.register(FetchUserDataUseCase.self) {
FetchUserDataUseCaseImpl(container.resolve(UserDataRepository.self))
}

container.register(FetchPushSettingsUseCase.self) {
FetchPushNotificationSettingsUseCaseImpl(container.resolve(PushNotificationRepository.self))
}

container.register(UpsertStatusMessageUseCase.self) {
UpsertStatusMessageUseCaseImpl(container.resolve(UserDataRepository.self))
}
}

func registerPushNotificationUseCases(_ container: DIContainer) {
container.register(FetchPushSettingsUseCase.self) {
FetchPushNotificationSettingsUseCaseImpl(container.resolve(PushNotificationRepository.self))
}

container.register(UpdatePushSettingsUseCase.self) {
UpdatePushSettingsUseCaseImpl(container.resolve(PushNotificationRepository.self))
}

container.register(FetchTodosByKindUseCase.self) {
FetchTodosByKindUseCaseImpl(container.resolve(TodoRepository.self))
container.register(DeletePushNotificationUseCase.self) {
DeletePushNotificationUseCaseImpl(container.resolve(PushNotificationRepository.self))
}

container.register(FetchTodosByKeywordUseCase.self) {
FetchTodosByKeywordUseCaseImpl(container.resolve(TodoRepository.self))
container.register(FetchPushNotificationsUseCase.self) {
FetchPushNotificationsUseCaseImpl(container.resolve(PushNotificationRepository.self))
}

container.register(TogglePushNotificationReadUseCase.self) {
TogglePushNotificationReadUseCaseImpl(container.resolve(PushNotificationRepository.self))
}
}

func registerWebPageUseCases(_ container: DIContainer) {
container.register(FetchWebPagesUseCase.self) {
FetchWebPagesUseCaseImpl(container.resolve(WebPageRepository.self))
}
Expand All @@ -74,29 +120,39 @@ final class DomainAssembler: Assembler {
container.register(DeleteWebPageUseCase.self) {
DeleteWebPageUseCaseImpl(container.resolve(WebPageRepository.self))
}
}

container.register(DeletePushNotificationUseCase.self) {
DeletePushNotificationUseCaseImpl(container.resolve(PushNotificationRepository.self))
func registerUserPreferencesUseCases(_ container: DIContainer) {
container.register(ObserveSystemThemeUseCase.self) {
ObserveSystemThemeUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}

container.register(FetchPushNotificationsUseCase.self) {
FetchPushNotificationsUseCaseImpl(container.resolve(PushNotificationRepository.self))
container.register(UpdateSystemThemeUseCase.self) {
UpdateSystemThemeUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}

container.register(TogglePushNotificationReadUseCase.self) {
TogglePushNotificationReadUseCaseImpl(container.resolve(PushNotificationRepository.self))
container.register(FetchFirstLaunchUseCase.self) {
FetchFirstLaunchUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}
container.register(FetchAuthProvidersUseCase.self) {
FetchAuthProvidersUseCaseImpl(container.resolve(AuthDataRepository.self))

container.register(UpdateFirstLaunchUseCase.self) {
UpdateFirstLaunchUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}
container.register(LinkAuthProviderUseCase.self) {
LinkAuthProviderUseCaseImpl(container.resolve(AuthDataRepository.self))

container.register(FetchRecentSearchQueriesUseCase.self) {
FetchRecentSearchQueriesUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}

container.register(UnlinkAuthProviderUseCase.self) {
UnlinkAuthProviderUseCaseImpl(container.resolve(AuthDataRepository.self))

container.register(UpdateRecentSearchQueriesUseCase.self) {
UpdateRecentSearchQueriesUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}

container.register(FetchPushNotificationQueryUseCase.self) {
FetchPushNotificationQueryUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}

container.register(UpdatePushNotificationQueryUseCase.self) {
UpdatePushNotificationQueryUseCaseImpl(container.resolve(UserPreferencesRepository.self))
}
}
}
8 changes: 8 additions & 0 deletions DevLog/App/Assembler/InfraAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,13 @@ final class InfraAssembler: Assembler {
container.register(WebPageMetadataService.self) {
WebPageMetadataService()
}

container.register(UserDefaultsStore.self) {
UserDefaultsStore()
}

container.register(ThemeStore.self) {
ThemeStore()
}
}
}
15 changes: 8 additions & 7 deletions DevLog/App/DevLogApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import SwiftUI
@main
struct DevLogApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@AppStorage("theme") var theme: SystemTheme = .automatic
@Environment(\.diContainer) var container: DIContainer

init() {
Expand All @@ -19,12 +18,14 @@ struct DevLogApp: App {

var body: some Scene {
WindowGroup {
RootView(
viewModel: RootViewModel(
sessionUseCase: container.resolve(AuthSessionUseCase.self),
signOutUseCase: container.resolve(SignOutUseCase.self)
))
.preferredColorScheme(theme.colorScheme)
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)
))
}
}
}
11 changes: 6 additions & 5 deletions DevLog/App/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ struct RootView: View {
signOutUseCase: container.resolve(SignOutUseCase.self),
sessionUseCase: container.resolve(AuthSessionUseCase.self))
)
.onAppear {
if viewModel.state.isFirstLaunch {
viewModel.send(.setFirstLaunch(false))
viewModel.send(.signOutAuto)
}
.onAppear {
if viewModel.state.isFirstLaunch {
viewModel.send(.setFirstLaunch(false))
viewModel.send(.signOutAuto)
}
}
}
} else {
Color.clear.onAppear {
Expand All @@ -41,6 +41,7 @@ struct RootView: View {
}
}
}
.preferredColorScheme(viewModel.state.theme.colorScheme)
.alert(viewModel.state.alertTitle, isPresented: Binding(
get: { viewModel.state.showAlert },
set: { viewModel.send(.setAlert($0)) }
Expand Down
30 changes: 30 additions & 0 deletions DevLog/Data/Protocol/UserPreferencesRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// UserPreferencesRepository.swift
// DevLog
//
// Created by 최윤진 on 2/25/26.
//

import Foundation
import Combine

protocol UserPreferencesRepository {
var systemThemePublisher: AnyPublisher<SystemTheme, Never> { get }
func systemTheme() -> SystemTheme
func setSystemTheme(_ theme: SystemTheme)

func isFirstLaunch() -> Bool
func setFirstLaunch(_ value: Bool)

func recentSearchQueries() -> [String]
func setRecentSearchQueries(_ queries: [String])

func pushNotificationSortOrder() -> PushNotificationQuery.SortOrder
func setPushNotificationSortOrder(_ order: PushNotificationQuery.SortOrder)

func pushNotificationTimeFilter() -> PushNotificationQuery.TimeFilter
func setPushNotificationTimeFilter(_ filter: PushNotificationQuery.TimeFilter)

func pushNotificationUnreadOnly() -> Bool
func setPushNotificationUnreadOnly(_ value: Bool)
}
95 changes: 95 additions & 0 deletions DevLog/Data/Repository/UserPreferencesRepositoryImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// UserPreferencesRepositoryImpl.swift
// DevLog
//
// Created by 최윤진 on 2/25/26.
//

import Foundation
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"
static let pushUnreadOnly = "PushNotification.showUnreadOnly"
}

private let store: UserDefaultsStore
private let themeStore: ThemeStore

init(
store: UserDefaultsStore,
themeStore: ThemeStore
) {
self.store = store
self.themeStore = themeStore
themeStore.send(systemTheme())
}

var systemThemePublisher: AnyPublisher<SystemTheme, Never> {
themeStore.themePublisher
}

func systemTheme() -> SystemTheme {
guard let rawValue = store.string(forKey: Key.theme),
let theme = SystemTheme(rawValue: rawValue) else {
return .automatic
}
return theme
}

func setSystemTheme(_ theme: SystemTheme) {
store.setString(theme.rawValue, forKey: Key.theme)
themeStore.send(theme)
}

func isFirstLaunch() -> Bool {
if store.string(forKey: Key.firstLaunch) == nil {
return true
}
return store.bool(forKey: Key.firstLaunch)
Comment on lines +51 to +54
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The isFirstLaunch() method incorrectly uses store.string(forKey:) to check for the existence of a Boolean value in UserDefaults. This will always evaluate to true, causing a perpetual "first launch" state and a denial of service.

}

func setFirstLaunch(_ value: Bool) {
store.setBool(value, forKey: Key.firstLaunch)
}

func recentSearchQueries() -> [String] {
store.stringArray(forKey: Key.recentQueries)
}

func setRecentSearchQueries(_ queries: [String]) {
store.setStringArray(queries, forKey: Key.recentQueries)
}

func pushNotificationSortOrder() -> PushNotificationQuery.SortOrder {
guard let rawValue = store.string(forKey: Key.pushSortOrder) else { return .latest }
return rawValue == "oldest" ? .oldest : .latest
}

func setPushNotificationSortOrder(_ order: PushNotificationQuery.SortOrder) {
let value = order == .oldest ? "oldest" : "latest"
store.setString(value, forKey: Key.pushSortOrder)
}

func pushNotificationTimeFilter() -> PushNotificationQuery.TimeFilter {
let id = store.string(forKey: Key.pushTimeFilter) ?? "none"
return PushNotificationQuery.TimeFilter(id: id)
}

func setPushNotificationTimeFilter(_ filter: PushNotificationQuery.TimeFilter) {
store.setString(filter.id, forKey: Key.pushTimeFilter)
}

func pushNotificationUnreadOnly() -> Bool {
store.bool(forKey: Key.pushUnreadOnly)
}

func setPushNotificationUnreadOnly(_ value: Bool) {
store.setBool(value, forKey: Key.pushUnreadOnly)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// FetchFirstLaunchUseCase.swift
// DevLog
//
// Created by 최윤진 on 2/25/26.
//

protocol FetchFirstLaunchUseCase {
func execute() -> Bool
}
Loading