From ba0ec5e8365952cbb1ce2d46df18bb690d0c1d18 Mon Sep 17 00:00:00 2001 From: Yunosuke Sakai Date: Tue, 17 Dec 2024 23:14:22 +0900 Subject: [PATCH] save API key in Keychain --- .../xcshareddata/xcschemes/Settings.xcscheme | 79 +++++++++++++++++++ Features/Package.swift | 1 + .../Settings/APIKey/APIKeySetting.swift | 11 ++- .../Settings/APIKey/APIKeySettingView.swift | 3 + .../Sources/Settings/Settings/Settings.swift | 16 +++- .../Settings/Settings/SettingsView.swift | 5 +- .../SettingsTests/APIKeySettingTests.swift | 38 +++++++++ .../Tests/SettingsTests/SettingsTests.swift | 20 ++++- 8 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 Features/.swiftpm/xcode/xcshareddata/xcschemes/Settings.xcscheme create mode 100644 Features/Tests/SettingsTests/APIKeySettingTests.swift diff --git a/Features/.swiftpm/xcode/xcshareddata/xcschemes/Settings.xcscheme b/Features/.swiftpm/xcode/xcshareddata/xcschemes/Settings.xcscheme new file mode 100644 index 0000000..8733460 --- /dev/null +++ b/Features/.swiftpm/xcode/xcshareddata/xcschemes/Settings.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Features/Package.swift b/Features/Package.swift index 72afa86..618c5d6 100644 --- a/Features/Package.swift +++ b/Features/Package.swift @@ -102,6 +102,7 @@ let package = Package( name: "Settings", dependencies: [ "APIClientLive", + "APIKeyClient", .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "LicenseList", package: "LicenseList"), ] diff --git a/Features/Sources/Settings/APIKey/APIKeySetting.swift b/Features/Sources/Settings/APIKey/APIKeySetting.swift index 4a0af7c..9d6c876 100644 --- a/Features/Sources/Settings/APIKey/APIKeySetting.swift +++ b/Features/Sources/Settings/APIKey/APIKeySetting.swift @@ -1,5 +1,6 @@ import APIClient import APIClientLive +import APIKeyClient import ComposableArchitecture import Models @@ -22,20 +23,28 @@ public struct APIKeySetting { } public enum Action: Equatable { + case onAppear case setAPIKeyInput(String) case updateButtonTapped } + + @Dependency(\.apiKeyClient) private var apiKeyClient public init() {} public func reduce(into state: inout State, action: Action) -> Effect { switch action { + case .onAppear: + state.apiKeyInput = apiKeyClient.getKey() ?? .init(rawValue: "") + return .none + case let .setAPIKeyInput(input): state.apiKeyInput = .init(rawValue: input) return .none case .updateButtonTapped: - // TODO: Persist API key + apiKeyClient.setKey(state.apiKeyInput) + // TODO: remove this logic APIClient.setAPIKey(state.apiKeyInput) return .none } diff --git a/Features/Sources/Settings/APIKey/APIKeySettingView.swift b/Features/Sources/Settings/APIKey/APIKeySettingView.swift index c2692c1..91eb935 100644 --- a/Features/Sources/Settings/APIKey/APIKeySettingView.swift +++ b/Features/Sources/Settings/APIKey/APIKeySettingView.swift @@ -33,6 +33,9 @@ struct APIKeySettingView: View { .disabled(!viewStore.isEdited) } } + .onAppear { + viewStore.send(.onAppear) + } } private var link: some View { diff --git a/Features/Sources/Settings/Settings/Settings.swift b/Features/Sources/Settings/Settings/Settings.swift index f85c525..3da1ee8 100644 --- a/Features/Sources/Settings/Settings/Settings.swift +++ b/Features/Sources/Settings/Settings/Settings.swift @@ -1,4 +1,6 @@ +import APIKeyClient import ComposableArchitecture +import Models @Reducer public struct Settings { @@ -9,19 +11,31 @@ public struct Settings { @ObservableState public struct State: Equatable { + var apiKey: APIKey var path = StackState() - public init() {} + public init( + apiKey: APIKey = .init(rawValue: "") + ) { + self.apiKey = apiKey + } } public enum Action { + case onAppear case path(StackActionOf) case popToRoot } + + @Dependency(\.apiKeyClient) private var apiKeyClient public var body: some ReducerOf { Reduce { state, action in switch action { + case .onAppear: + state.apiKey = apiKeyClient.getKey() ?? .init(rawValue: "") + return .none + case let .path(action): switch action { case let .element(id: id, action: .apiKeySetting(.updateButtonTapped)): diff --git a/Features/Sources/Settings/Settings/SettingsView.swift b/Features/Sources/Settings/Settings/SettingsView.swift index 3eded19..5161769 100644 --- a/Features/Sources/Settings/Settings/SettingsView.swift +++ b/Features/Sources/Settings/Settings/SettingsView.swift @@ -28,7 +28,7 @@ public struct SettingsView: View { Spacer() - Text(APIClient.apiKey?.rawValue.masked ?? "None") + Text(store.apiKey.rawValue.masked ?? "None") .foregroundStyle(Color.secondary) } } @@ -77,6 +77,9 @@ public struct SettingsView: View { APIKeySettingView(store: store) } } + .onAppear { + store.send(.onAppear) + } } } diff --git a/Features/Tests/SettingsTests/APIKeySettingTests.swift b/Features/Tests/SettingsTests/APIKeySettingTests.swift new file mode 100644 index 0000000..841e102 --- /dev/null +++ b/Features/Tests/SettingsTests/APIKeySettingTests.swift @@ -0,0 +1,38 @@ +import ComposableArchitecture +@testable import Settings +import Testing + +@MainActor +struct APIKeySettingTests { + @Test + func setAPIKeyInput() async throws { + let store = TestStore( + initialState: APIKeySetting.State() + ) { + APIKeySetting() + } + + await store.send(.setAPIKeyInput("test")) { + $0.apiKeyInput = .init(rawValue: "test") + } + } + + @Test + func onAppear() async throws { + let store = TestStore( + initialState: APIKeySetting.State() + ) { + APIKeySetting() + } + + store.dependencies.apiKeyClient.getKey = { + .init(rawValue: "test") + } + + await store.send(.onAppear) { + $0.apiKeyInput = .init(rawValue: "test") + } + } + + // TODO: add test case for `updateButtonTapped` +} diff --git a/Features/Tests/SettingsTests/SettingsTests.swift b/Features/Tests/SettingsTests/SettingsTests.swift index ff51695..40feb5f 100644 --- a/Features/Tests/SettingsTests/SettingsTests.swift +++ b/Features/Tests/SettingsTests/SettingsTests.swift @@ -1,6 +1,24 @@ +import ComposableArchitecture import Testing @testable import Settings @MainActor -struct SettingsTests {} +struct SettingsTests { + @Test + func onAppear() async throws { + let store = TestStore( + initialState: Settings.State() + ) { + Settings() + } + + store.dependencies.apiKeyClient.getKey = { + .init(rawValue: "test") + } + + await store.send(.onAppear) { + $0.apiKey = .init(rawValue: "test") + } + } +}