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
Binary file added Projects/DVDesign/Resources/progress.lottie
Binary file not shown.
26 changes: 14 additions & 12 deletions Projects/DVDesign/Sources/Components/DVButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,42 @@ public struct DVButton: View {

public enum Style {
case primary
case primarySmall
case secondary

var cornerRadius: CGFloat {
switch self {
case .primary: return 20
case .secondary: return 6
case .primary, .primarySmall: return 20
case .secondary: return 6
}
}

var height: CGFloat {
switch self {
case .primary: return 40
case .secondary: return 24
case .primary, .primarySmall: return 40
case .secondary: return 24
}
}

var horizontalPadding: CGFloat {
switch self {
case .primary: return 16
case .secondary: return 16
case .primary, .primarySmall: return 16
case .secondary: return 16
}
}

var width: CGFloat {
switch self {
case .primary: return 242
case .secondary: return 74
case .primary: return 242
case .primarySmall: return 134
case .secondary: return 74
}
}

var font: DVFont {
switch self {
case .primary: return .bodyLG
case .secondary: return .bodyMD
case .primary, .primarySmall: return .bodyLG
case .secondary: return .bodyMD
}
}
}
Expand Down Expand Up @@ -108,7 +110,7 @@ private struct DVButtonStyle: ButtonStyle {

private var foregroundColor: Color {
switch style {
case .primary:
case .primary, .primarySmall:
return Color.dv(.white)
case .secondary:
return isEnabled ? Color.dv(.gray800) : Color.dv(.gray400)
Expand All @@ -117,7 +119,7 @@ private struct DVButtonStyle: ButtonStyle {

private func backgroundColor(isPressed: Bool) -> Color {
switch style {
case .primary:
case .primary, .primarySmall:
if !isEnabled { return Color.dv(.vaultGreenTint) }
if isPressed || isHovered { return Color.dv(.vaultGreenDark) }
return Color.dv(.vaultGreen)
Expand Down
5 changes: 4 additions & 1 deletion Projects/DVDesign/Sources/Components/DVCategory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public struct DVCategory: View {
public let title: String
public let count: Int
public let systemImage: String
public let iconColor: Color
public let isSelected: Bool
public let action: () -> Void

Expand All @@ -20,12 +21,14 @@ public struct DVCategory: View {
title: String,
count: Int,
systemImage: String,
iconColor: Color = Color.dv(.gray800),
isSelected: Bool,
action: @escaping () -> Void
) {
self.title = title
self.count = count
self.systemImage = systemImage
self.iconColor = iconColor
self.isSelected = isSelected
self.action = action
}
Expand Down Expand Up @@ -65,7 +68,7 @@ extension DVCategory {
private var iconView: some View {
Image(systemName: systemImage)
.dvFont(.headingLG)
.foregroundStyle(isSelected ? Color.dv(.white) : Color.dv(.gray800))
.foregroundStyle(isSelected ? Color.dv(.white) : iconColor)
.frame(width: 24, height: 24)
}

Expand Down
2 changes: 1 addition & 1 deletion Projects/DVDesign/Sources/Components/DVStepIndicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ extension DVStepIndicator {

private func dot(isActive: Bool) -> some View {
RoundedRectangle(cornerRadius: 4)
.fill(isActive ? Color.dv(.vaultGreen) : Color.dv(.gray300))
.fill(isActive ? Color.dv(.vaultGreen) : Color.dv(.gray500))
.frame(width: isActive ? 24 : 8, height: 8)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isActive)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ public extension View {
func dvBackgroundColor(_ token: DVColor) -> some View {
self.background(token.color)
}

func dvScreenBackground(_ token: DVColor = .gray100) -> some View {
self.background(token.color.ignoresSafeArea())
}
}
1 change: 1 addition & 0 deletions Projects/DVPresentation/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let project = Project.project(

// 3rd-party dependency
.tca(),
.lottie(),
]
),
]
Expand Down
57 changes: 57 additions & 0 deletions Projects/DVPresentation/Sources/Features/Lock/LockFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright © 2026 Devault. All rights reserved

import Foundation

import ComposableArchitecture

// MARK: - LockFeature

@Reducer
public struct LockFeature {

// MARK: - State

@ObservableState
public struct State: Equatable {
public var isPostOnboarding: Bool

public init(isPostOnboarding: Bool = false) {
self.isPostOnboarding = isPostOnboarding
}
}

// MARK: - Action

public enum Action: Equatable {

// MARK: - View

case didTapUnlock

// MARK: - Delegate

case delegate(Delegate)

public enum Delegate: Equatable {
case unlockCompleted
}
}

// MARK: - Init

public init() {}

// MARK: - Body

public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .didTapUnlock:
return .send(.delegate(.unlockCompleted))
Comment on lines +49 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching LockFeature.swift:"
fd -a 'LockFeature\.swift$' . | sed 's#^\./##'

file="$(fd 'LockFeature\.swift$' . | head -n1)"
if [ -n "${file:-}" ]; then
  echo
  echo "File: $file"
  wc -l "$file"
  echo
  echo "Outline:"
  ast-grep outline "$file" || true
  echo
  echo "Relevant source:"
  sed -n '1,180p' "$file" | cat -n
fi

echo
echo "Search for unlockCompleted and delegate usages:"
rg -n "unlockCompleted|didTapUnlock|unlockRequested|delegate\(" . -g '*.swift' | head -n 200

Repository: DevaultProject/Devault-macOS

Length of output: 3801


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "LockView.swift:"
sed -n '1,160p' ./Projects/DVPresentation/Sources/Features/Lock/LockView.swift | cat -n

echo
echo "Files containing LockFeature / lock references:"
rg -n "LockFeature|\\bunlockCompleted\\b|didTapUnlock|LocalAuthentication|Touch ID|touch id|biometric|Face ID|face id|authenticate" . -g '*.swift'

Repository: DevaultProject/Devault-macOS

Length of output: 5929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Authentication service interface:"
sed -n '1,120p' ./Projects/DVDomain/Sources/Service/Interface/UserAuthenticationService.swift | cat -n

echo
echo "LockFeature references and delegates:"
rg -n "delegate\\(.*unlockCompleted|unlockCompleted\\)|LockFeature\\(|\\.lock|LockFeature\\.init|store: StoreOf<LockFeature>|StoreOf<LockFeature>" . -g '*.swift' | head -n 200

echo
echo "Lock files:"
fd -a 'Lock.*\.swift$' . | sed 's#^\./##'

echo
echo "User auth impl:"
sed -n '1,180p' ./Projects/DVData/Sources/ServiceImpl/Authentication/LocalUserAuthenticationServiceImpl.swift | cat -n

Repository: DevaultProject/Devault-macOS

Length of output: 3157


unlockCompleted 전송을 인증 성공 결과로 바꾸세요.

didTapUnlock가 버튼을 누르는 즉시 delegate를 보내고 있어, 현재 UserAuthenticationService.authenticate가 호출되지 않습니다. LockFeature에서 LocalUserAuthenticationServiceImpl 의존성을 받아 인증을 트리거하고, 성공 시에만 unlockCompleted를 보냅니다. 실패/취소/미지원은 잠금 상태를 유지하거나 에러(delegate/state)로 전달하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVPresentation/Sources/Features/Lock/LockFeature.swift` around lines
49 - 50, LockFeature의 didTapUnlock 처리에서 즉시 unlockCompleted를 보내지 말고
LocalUserAuthenticationServiceImpl 의존성을 주입해 authenticate를 호출하세요. 인증 성공 결과에서만
delegate unlockCompleted를 전송하고, 실패·취소·미지원 결과는 잠금 상태를 유지하거나 기존 에러 전달 경로로 처리하세요.


case .delegate:
return .none
}
}
}
}
89 changes: 89 additions & 0 deletions Projects/DVPresentation/Sources/Features/Lock/LockView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright © 2026 Devault. All rights reserved

import SwiftUI

import ComposableArchitecture
import DVDesign

// MARK: - LockView

public struct LockView: View {

// MARK: - Properties

@Bindable public var store: StoreOf<LockFeature>

// MARK: - Init

public init(store: StoreOf<LockFeature>) {
self.store = store
}

// MARK: - Body

public var body: some View {
content
.dvScreenBackground()
}
}

// MARK: - Subviews

extension LockView {

private var content: some View {
ZStack {
unlockView
if store.isPostOnboarding {
VStack {
Spacer()
DVStepIndicator(totalSteps: 4, currentStep: 3)
.padding(.bottom, 40)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}

private var unlockView: some View {
VStack(spacing: 40) {
appIconWithLogoView
DVButton(titleText: "Unlock with Touch ID", style: .primary) {
store.send(.didTapUnlock)
}
}
}

private var appIconWithLogoView: some View {
VStack(spacing: 20) {
RoundedRectangle(cornerRadius: 20)
.fill(Color.dv(.gray800))
.frame(width: 80, height: 80)
(
Text("De").foregroundStyle(Color.dv(.vaultDark))
+ Text("Vault").foregroundStyle(Color.dv(.vaultGreen))
)
.font(.dv(.displayBrand))
}
}
}

// MARK: - Preview

#Preview("Post Onboarding") {
LockView(
store: Store(initialState: LockFeature.State(isPostOnboarding: true)) {
LockFeature()
}
)
.frame(width: 540, height: 400)
}

#Preview("Re-entry / Locked") {
LockView(
store: Store(initialState: LockFeature.State(isPostOnboarding: false)) {
LockFeature()
}
)
.frame(width: 540, height: 400)
}
2 changes: 2 additions & 0 deletions Projects/DVPresentation/Sources/Features/Main/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import SwiftUI

import ComposableArchitecture
import DVDesign

// MARK: - MainView

Expand All @@ -16,6 +17,7 @@ struct MainView: View {

var body: some View {
content
.dvScreenBackground()
.task { store.send(.task) }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright © 2026 Devault. All rights reserved

import Foundation

import ComposableArchitecture

// MARK: - OnboardingFeature

@Reducer
public struct OnboardingFeature {

// MARK: - Step

public enum Step: Equatable {
case welcome
case security
case icloudSync
case syncing
}

// MARK: - State

@ObservableState
public struct State: Equatable {
public var step: Step = .welcome

public init(step: Step = .welcome) {
self.step = step
}

var currentStepIndex: Int {
switch step {
case .welcome: return 0
case .security: return 1
case .icloudSync: return 2
case .syncing: return 2
}
}
Comment on lines +31 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Syncing 단계를 네 번째 indicator로 표시하세요.

DVStepIndicator(totalSteps: 4, ...)를 사용하지만 .syncing이 2를 반환해 iCloud Sync 단계와 같은 세 번째 상태로 표시됩니다.

수정 예시
-      case .syncing:    return 2
+      case .syncing:    return 3
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var currentStepIndex: Int {
switch step {
case .welcome: return 0
case .security: return 1
case .icloudSync: return 2
case .syncing: return 2
}
}
var currentStepIndex: Int {
switch step {
case .welcome: return 0
case .security: return 1
case .icloudSync: return 2
case .syncing: return 3
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVPresentation/Sources/Features/Onboarding/OnboardingFeature.swift`
around lines 31 - 38, Update the currentStepIndex computed property so the
.syncing case returns index 3, making it the fourth indicator step while
preserving the existing indices for .welcome, .security, and .icloudSync.

}

// MARK: - Action

public enum Action: Equatable {

// MARK: - View

case didTapStart
case didTapEnableTouchID
case didTapNotNow
case didTapEnableSync
case syncingCompleted

// MARK: - Delegate

case delegate(Delegate)

public enum Delegate: Equatable {
case completed
}
}

// MARK: - Init

public init() {}

// MARK: - Body

public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .didTapStart:
state.step = .security
return .none

case .didTapEnableTouchID:
state.step = .icloudSync
return .none
Comment on lines +75 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Touch ID 성공 확인 후에만 다음 단계로 진행하세요.

현재 didTapEnableTouchID는 인증·등록 결과와 무관하게 즉시 iCloud Sync 화면으로 전환됩니다. 취소·실패 시에도 “Enable Touch ID”가 완료된 것처럼 진행되므로, 인증 클라이언트의 성공 콜백 또는 부모 delegate 이벤트를 받은 뒤 상태를 변경하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVPresentation/Sources/Features/Onboarding/OnboardingFeature.swift`
around lines 75 - 77, Update the didTapEnableTouchID handling in
OnboardingFeature so it advances to .icloudSync only after the authentication
client’s success callback or the parent delegate’s success event is received.
Keep the current step unchanged and avoid emitting the transition for canceled
or failed Touch ID attempts.


case .didTapNotNow:
return .send(.delegate(.completed))

case .didTapEnableSync:
state.step = .syncing
return .none

case .syncingCompleted:
return .send(.delegate(.completed))
Comment on lines +82 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

실제 동기화 완료 이벤트를 연결하세요.

didTapEnableSync.syncing으로만 전환하고, 이 리듀서 내에서는 syncingCompleted를 보내는 효과가 없습니다. 따라서 사용자는 Syncing 화면에 계속 머뭅니다. 동기화 클라이언트를 의존성으로 주입해 성공 시 완료 delegate를 보내고, 실패·재시도 상태도 처리하세요. 원하시면 TCA effect 구조로 정리해드릴 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVPresentation/Sources/Features/Onboarding/OnboardingFeature.swift`
around lines 82 - 87, Connect the actual synchronization client to the
didTapEnableSync case, dispatching syncingCompleted only after a successful sync
so the existing completed delegate is reached. Handle synchronization failures
by updating the appropriate failure state and provide the retry path from the
syncing flow, using the feature’s existing TCA dependency and action/state
symbols.


case .delegate:
return .none
}
}
}
}
Loading