Skip to content
Open
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
25 changes: 15 additions & 10 deletions Projects/TDCore/Sources/DeepLink/DeepLinkType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,46 @@ public enum DeepLinkType {
case diary
case home
case notification

case qrWebLogin(sessionToken: String)

public init?(url: URL) {
guard url.scheme == "toduck" else { return nil }

let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let queryItems = components?.queryItems
func queryValue(for name: String) -> String? {
return queryItems?.first(where: { $0.name == name })?.value
}

switch url.host {
case "profile":
guard let userId = queryValue(for: "userId") else { return nil }
self = .profile(userId: userId)

case "post":
guard let postId = queryValue(for: "postId") else { return nil }
let commentId = queryValue(for: "commentId")
self = .post(postId: postId, commentId: commentId)

case "createPost":
self = .createPost

case "todo":
self = .todo

case "diary":
self = .diary

case "home":
self = .home

case "notification":
self = .notification


case "qrWebLogin":
guard let sessionToken = queryValue(for: "sessionToken") else { return nil }
self = .qrWebLogin(sessionToken: sessionToken)

default:
return nil
}
Expand Down
4 changes: 4 additions & 0 deletions Projects/TDData/Sources/Repository/AuthRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ public struct AuthRepositoryImpl: AuthRepository {
let loginUserResponseDTO = try await service.refreshToken()
try await saveToken(response: loginUserResponseDTO)
}

public func authorizeWebSession(sessionToken: String) async throws {
try await service.authorizeWebSession(sessionToken: sessionToken)
}

private func saveToken(response: LoginUserResponseDTO) async throws {
let accessToken = response.accessToken
Expand Down
1 change: 1 addition & 0 deletions Projects/TDData/Sources/ServiceProtocol/AuthService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public protocol AuthService {
func requestKakaoLogin() async throws -> String
func requestLogin(loginId: String, password: String) async throws -> LoginUserResponseDTO
func refreshToken() async throws -> LoginUserResponseDTO
func authorizeWebSession(sessionToken: String) async throws
}
9 changes: 8 additions & 1 deletion Projects/TDDomain/Sources/DomainAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ public struct DomainAssembly: Assembly {
}
return RegisterUserUseCaseImpl(repository: repository)
}


container.register(AuthorizeWebSessionUseCase.self) { resolver in
guard let repository = resolver.resolve(AuthRepository.self) else {
fatalError("컨테이너에 AuthRepository가 등록되어 있지 않습니다.")
}
return AuthorizeWebSessionUseCaseImpl(repository: repository)
}

// MARK: - User Auth UseCases

container.register(FindUserIdUseCase.self) { resolve in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public protocol AuthRepository {
func requestAppleLogin(oauthId: String, idToken: String) async throws
func requestLogin(loginId: String, password: String) async throws
func refreshToken() async throws
func authorizeWebSession(sessionToken: String) async throws
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import TDCore

public protocol AuthorizeWebSessionUseCase {
func execute(sessionToken: String) async throws
}

public struct AuthorizeWebSessionUseCaseImpl: AuthorizeWebSessionUseCase {
private let repository: AuthRepository

public init(repository: AuthRepository) {
self.repository = repository
}

public func execute(sessionToken: String) async throws {
try await repository.authorizeWebSession(sessionToken: sessionToken)
}
}
19 changes: 17 additions & 2 deletions Projects/TDNetwork/Sources/Network/API/AuthAPI.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import TDCore

public enum AuthAPI {
case requestPhoneVerification(phoneNumber: String) // 휴대폰 본인 인증 요청
Expand All @@ -10,6 +11,7 @@ public enum AuthAPI {
case refreshToken(refreshToken: String) // 리프래시 토큰 발급
case saveFCMToken(userId: Int, fcmToken: String) // FCM 토큰 저장
case deleteUser(userId: Int) // 유저 삭제
case authorizeWebSession(sessionToken: String) // QR 코드 웹 로그인 인증
}

extension AuthAPI: MFTarget {
Expand Down Expand Up @@ -37,6 +39,8 @@ extension AuthAPI: MFTarget {
return "/users/\(userId)/fcm-token"
case .deleteUser(let userId):
return "/users/\(userId)"
case .authorizeWebSession:
return "/v1/auth/web/authorize"
}
}

Expand All @@ -50,7 +54,8 @@ extension AuthAPI: MFTarget {
case .registerUser,
.login,
.loginOauth,
.saveFCMToken:
.saveFCMToken,
.authorizeWebSession:
return .post
case .deleteUser:
return .delete
Expand All @@ -73,7 +78,8 @@ extension AuthAPI: MFTarget {
case .registerUser,
.login,
.saveFCMToken,
.refreshToken:
.refreshToken,
.authorizeWebSession:
return nil
case .deleteUser(let userId):
// TODO: - 나중 결정?
Expand Down Expand Up @@ -110,6 +116,10 @@ extension AuthAPI: MFTarget {
parameters: ["fcmToken": fcmToken]
)

case .authorizeWebSession(let sessionToken):
return .requestParameters(
parameters: ["sessionToken": sessionToken]
)
case .checkDuplicateUserID,
.requestPhoneVerification,
.checkPhoneVerification,
Expand All @@ -132,6 +142,11 @@ extension AuthAPI: MFTarget {
let cookieHeaderValue = "refreshToken=\(refreshToken)"
let headers: MFHeaders = [.cookie(cookieHeaderValue), .accept("application/json")]
return headers
case .authorizeWebSession:
return [
.authorization(bearerToken: TDTokenManager.shared.accessToken ?? ""),
.contentType("application/json")
]
case .requestPhoneVerification,
.saveFCMToken,
.deleteUser:
Expand Down
5 changes: 5 additions & 0 deletions Projects/TDNetwork/Sources/Service/AuthServiceImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ public struct AuthServiceImpl: AuthService {
}
}

public func authorizeWebSession(sessionToken: String) async throws {
let target = AuthAPI.authorizeWebSession(sessionToken: sessionToken)
try await provider.requestDecodable(of: EmptyResponse.self, target)
}

private func mapToLoginUserResponseDTO(from response: MFResponse<LoginUserResponseBody>) throws -> LoginUserResponseDTO {
if let refreshToken = response.extractRefreshToken(),
let refreshTokenExpiredAt = response.extractRefreshTokenExpiry() {
Expand Down
4 changes: 2 additions & 2 deletions Projects/TDPresentation/Sources/AppFlow/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,15 @@ public final class AppCoordinator: Coordinator {
pendingDeepLink = link
return
}

guard let tabBarCoordinator = childCoordinators.first(where: { $0 is MainTabBarCoordinator }) as? MainTabBarCoordinator else {
pendingDeepLink = link
startTabBarFlow { [weak self] in
self?.handleDeepLink(link)
}
return
}

tabBarCoordinator.handleDeepLink(link)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import SnapKit
import TDDesign
import UIKit

final class QRWebLoginPopupView: BaseView {
private let stackView = UIStackView().then {
$0.axis = .vertical
$0.spacing = 0
$0.alignment = .center
}

private let buttonStackView = UIStackView().then {
$0.axis = .horizontal
$0.spacing = 4
$0.alignment = .fill
$0.distribution = .fillEqually
}

let popupImageView = UIImageView().then {
$0.image = TDImage.loginToduck
$0.contentMode = .scaleAspectFit
}

let titleLabel = TDLabel(
toduckFont: .boldHeader4,
toduckColor: TDColor.Neutral.neutral800
)

let descriptionLabel = TDLabel(
toduckFont: .mediumHeader5,
toduckColor: TDColor.Neutral.neutral600
)

private(set) var loginButton = TDBaseButton(
title: "로그인",
backgroundColor: TDColor.Primary.primary500,
foregroundColor: TDColor.baseWhite,
radius: 12,
font: TDFont.boldBody1.font
)

private(set) var cancelButton = TDBaseButton(
title: "취소",
backgroundColor: TDColor.baseWhite,
foregroundColor: TDColor.Neutral.neutral700,
radius: 12,
font: TDFont.boldBody1.font
).then {
$0.layer.borderWidth = 1
$0.layer.borderColor = TDColor.Neutral.neutral300.cgColor
}

override func addview() {
addSubview(stackView)
stackView.addArrangedSubview(popupImageView)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(descriptionLabel)
stackView.addArrangedSubview(buttonStackView)
buttonStackView.addArrangedSubview(cancelButton)
buttonStackView.addArrangedSubview(loginButton)
}

override func layout() {
stackView.snp.makeConstraints {
$0.edges.equalToSuperview().inset(UIEdgeInsets(top: 28, left: 20, bottom: 28, right: 20))
}

popupImageView.snp.makeConstraints {
$0.width.equalTo(200)
$0.height.equalTo(200)
}

stackView.setCustomSpacing(16, after: popupImageView)
stackView.setCustomSpacing(8, after: titleLabel)
stackView.setCustomSpacing(24, after: descriptionLabel)

buttonStackView.snp.makeConstraints {
$0.height.equalTo(52)
$0.width.equalToSuperview()
}

cancelButton.snp.makeConstraints {
$0.height.equalTo(52)
}

loginButton.snp.makeConstraints {
$0.height.equalTo(52)
}
}

override func configure() {
layer.cornerRadius = 28
backgroundColor = TDColor.baseWhite

titleLabel.setText("웹 로그인 요청이 들어왔어요")
descriptionLabel.setText("toduck 웹 페이지에 로그인할까요?")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import SnapKit
import TDDesign
import TDDomain
import Then
import UIKit

final class QRWebLoginPopupViewController: TDPopupViewController<QRWebLoginPopupView> {
private let sessionToken: String
private let authorizeWebSessionUseCase: AuthorizeWebSessionUseCase
var onLoginSuccess: (() -> Void)?

init(sessionToken: String, authorizeWebSessionUseCase: AuthorizeWebSessionUseCase) {
self.sessionToken = sessionToken
self.authorizeWebSessionUseCase = authorizeWebSessionUseCase
super.init()
}

@available(*, unavailable)
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func configure() {
super.configure()
isBackgroundGestureEnabled = false

popupContentView.loginButton.addAction(UIAction { [weak self] _ in
self?.handleLogin()
}, for: .touchUpInside)

popupContentView.cancelButton.addAction(UIAction { [weak self] _ in
self?.dismissPopup()
}, for: .touchUpInside)
}

private func handleLogin() {
popupContentView.loginButton.isEnabled = false

Task { [weak self] in
guard let self else { return }
do {
try await authorizeWebSessionUseCase.execute(sessionToken: sessionToken)
await MainActor.run {
self.dismiss(animated: true) {
self.onLoginSuccess?()
self.showLoginSuccessToast()
}
}
} catch {
await MainActor.run {
self.popupContentView.loginButton.isEnabled = true
self.showErrorAlert(errorMessage: "웹 로그인에 실패했습니다.")
}
}
}
}

private func showLoginSuccessToast() {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { return }

let toastLabel = UILabel().then {
$0.text = "웹 로그인이 완료되었습니다."
$0.font = TDFont.boldBody2.font
$0.textColor = TDColor.baseWhite
$0.backgroundColor = TDColor.Neutral.neutral700
$0.textAlignment = .center
$0.layer.cornerRadius = 12
$0.clipsToBounds = true
}

window.addSubview(toastLabel)
toastLabel.snp.makeConstraints {
$0.centerX.equalToSuperview()
$0.bottom.equalTo(window.safeAreaLayoutGuide).inset(32)
$0.height.equalTo(44)
$0.leading.trailing.equalToSuperview().inset(40)
}

UIView.animate(withDuration: 0.3, delay: 1.5, options: .curveEaseOut) {
toastLabel.alpha = 0
} completion: { _ in
toastLabel.removeFromSuperview()
}
}
}
Loading