Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merge :: 회원가입 - 인증 이메일 입력 Feature #96

Merged
merged 14 commits into from
Oct 27, 2022
Merged
6 changes: 6 additions & 0 deletions Projects/App/Sources/Application/DI/AppComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public extension AppComponent {
var signinComponent: SigninComponent {
SigninComponent(parent: self)
}
var signupEmailVerifyComponent: SignupEmailVerifyComponent {
SignupEmailVerifyComponent(parent: self)
}
var signupEmailAuthCodeVerifyComponent: SignupEmailAuthCodeVerifyComponent {
SignupEmailAuthCodeVerifyComponent(parent: self)
}
}

// MARK: - Main
Expand Down
48 changes: 48 additions & 0 deletions Projects/App/Sources/Application/NeedleGenerated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,38 @@ private class SchoolCodeDependencyc0114744c1c8c7843672Provider: SchoolCodeDepend
private func factoryb65c1efbf06b87162473f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject {
return SchoolCodeDependencyc0114744c1c8c7843672Provider(appComponent: parent1(component) as! AppComponent)
}
private class SignupEmailAuthCodeVerifyDependencyaf9da1ebf0e9e5f1b708Provider: SignupEmailAuthCodeVerifyDependency {
var sendAuthCodeUseCase: any SendAuthCodeUseCase {
return appComponent.sendAuthCodeUseCase
}
var verifyAuthCodeUseCase: any VerifyAuthCodeUseCase {
return appComponent.verifyAuthCodeUseCase
}
private let appComponent: AppComponent
init(appComponent: AppComponent) {
self.appComponent = appComponent
}
}
/// ^->AppComponent->SignupEmailAuthCodeVerifyComponent
private func factoryb06be35aa893adde971bf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject {
return SignupEmailAuthCodeVerifyDependencyaf9da1ebf0e9e5f1b708Provider(appComponent: parent1(component) as! AppComponent)
}
private class SignupEmailVerifyDependencyf9d372ac752ee19b78caProvider: SignupEmailVerifyDependency {
var checkDuplicateEmailUseCase: any CheckDuplicateEmailUseCase {
return appComponent.checkDuplicateEmailUseCase
}
var signupEmailAuthCodeVerifyComponent: SignupEmailAuthCodeVerifyComponent {
return appComponent.signupEmailAuthCodeVerifyComponent
}
private let appComponent: AppComponent
init(appComponent: AppComponent) {
self.appComponent = appComponent
}
}
/// ^->AppComponent->SignupEmailVerifyComponent
private func factory3b1904c76335d70151ebf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject {
return SignupEmailVerifyDependencyf9d372ac752ee19b78caProvider(appComponent: parent1(component) as! AppComponent)
}
private class MainTabDependency2826cdb310ed0b17a725Provider: MainTabDependency {
var homeComponent: HomeComponent {
return appComponent.homeComponent
Expand Down Expand Up @@ -110,6 +142,8 @@ extension AppComponent: Registration {
localTable["schoolCodeComponent-SchoolCodeComponent"] = { self.schoolCodeComponent as Any }
localTable["findIDComponent-FindIDComponent"] = { self.findIDComponent as Any }
localTable["signinComponent-SigninComponent"] = { self.signinComponent as Any }
localTable["signupEmailVerifyComponent-SignupEmailVerifyComponent"] = { self.signupEmailVerifyComponent as Any }
localTable["signupEmailAuthCodeVerifyComponent-SignupEmailAuthCodeVerifyComponent"] = { self.signupEmailAuthCodeVerifyComponent as Any }
localTable["mainTabComponent-MainTabComponent"] = { self.mainTabComponent as Any }
localTable["homeComponent-HomeComponent"] = { self.homeComponent as Any }
localTable["remoteStudentsDataSource-any RemoteStudentsDataSource"] = { self.remoteStudentsDataSource as Any }
Expand All @@ -136,6 +170,18 @@ extension SchoolCodeComponent: Registration {
keyPathToName[\SchoolCodeDependency.checkSchoolCodeUseCase] = "checkSchoolCodeUseCase-any CheckSchoolCodeUseCase"
}
}
extension SignupEmailAuthCodeVerifyComponent: Registration {
public func registerItems() {
keyPathToName[\SignupEmailAuthCodeVerifyDependency.sendAuthCodeUseCase] = "sendAuthCodeUseCase-any SendAuthCodeUseCase"
keyPathToName[\SignupEmailAuthCodeVerifyDependency.verifyAuthCodeUseCase] = "verifyAuthCodeUseCase-any VerifyAuthCodeUseCase"
}
}
extension SignupEmailVerifyComponent: Registration {
public func registerItems() {
keyPathToName[\SignupEmailVerifyDependency.checkDuplicateEmailUseCase] = "checkDuplicateEmailUseCase-any CheckDuplicateEmailUseCase"
keyPathToName[\SignupEmailVerifyDependency.signupEmailAuthCodeVerifyComponent] = "signupEmailAuthCodeVerifyComponent-SignupEmailAuthCodeVerifyComponent"
}
}
extension MainTabComponent: Registration {
public func registerItems() {
keyPathToName[\MainTabDependency.homeComponent] = "homeComponent-HomeComponent"
Expand Down Expand Up @@ -175,6 +221,8 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi
private func register1() {
registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider)
registerProviderFactory("^->AppComponent->SchoolCodeComponent", factoryb65c1efbf06b87162473f47b58f8f304c97af4d5)
registerProviderFactory("^->AppComponent->SignupEmailAuthCodeVerifyComponent", factoryb06be35aa893adde971bf47b58f8f304c97af4d5)
registerProviderFactory("^->AppComponent->SignupEmailVerifyComponent", factory3b1904c76335d70151ebf47b58f8f304c97af4d5)
registerProviderFactory("^->AppComponent->MainTabComponent", factory1ab5a747ddf21e1393f9f47b58f8f304c97af4d5)
registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5)
registerProviderFactory("^->AppComponent->HomeComponent", factory67229cdf0f755562b2b1f47b58f8f304c97af4d5)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import SwiftUI

public struct RootPresentationModeKey: EnvironmentKey {
public static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}

public extension EnvironmentValues {
var rootPresentationMode: Binding<RootPresentationMode> {
get { return self[RootPresentationModeKey.self] }
set { self[RootPresentationModeKey.self] = newValue }
}
}

public typealias RootPresentationMode = Bool

public extension RootPresentationMode {
mutating func dismiss() {
self.toggle()
}
}
23 changes: 23 additions & 0 deletions Projects/Features/BaseFeature/Sources/HideKeyboard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import UIKit

public extension UIApplication {
func hideKeyboard() {
guard
let scene = connectedScenes.first as? UIWindowScene,
let window = scene.windows.first
else { return }
let tapRecognizer = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing))
tapRecognizer.cancelsTouchesInView = false
tapRecognizer.delegate = self
window.addGestureRecognizer(tapRecognizer)
}
}

extension UIApplication: UIGestureRecognizerDelegate {
public func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
return false
}
}
4 changes: 2 additions & 2 deletions Projects/Features/SigninFeature/Sources/SigninView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ struct SigninView: View {
VStack(alignment: .leading, spacing: 8) {
Text("DMS")
.dmsFont(.title(.extraLarge), color: .PrimaryVariant.primary)
.padding(.top, 28)

Text("더 편한 기숙사 생활을 위해")
.dmsFont(.text(.medium), color: .GrayScale.gray6)
}

Spacer()
}
.padding(.top, 24)

VStack(spacing: 72) {
DMSFloatingTextField(
Expand Down Expand Up @@ -96,7 +96,7 @@ struct SigninView: View {
DMSWideButton(text: "로그인", color: .PrimaryVariant.primary) {
viewModel.signinButtonDidTap()
}
.disabled(!viewModel.isSigninButtonEnabled)
.disabled(!viewModel.isSigninEnabled)
.padding(.top, 24)
.frame(maxWidth: .infinity)
.padding(.bottom, 40)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ final class SigninViewModel: BaseViewModel {
@Published var isOnAutoSignin = true
@Published var isNavigateSignup = false
@Published var isSuccessSignin = false
var isSigninButtonEnabled: Bool {
var isSigninEnabled: Bool {
!id.isEmpty && !password.isEmpty
}

Expand All @@ -19,7 +19,7 @@ final class SigninViewModel: BaseViewModel {
}

func signinButtonDidTap() {
guard isSigninButtonEnabled else { return }
guard isSigninEnabled else { return }
addCancellable(signinUseCase.execute(req: .init(accountID: id, password: password))) { [weak self] _ in
self?.isSuccessSignin = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class SigninViewModelSpec: QuickSpec {
it("isSigninButtonEnabled은 false이다.") {
expect { sut.id }.to(beEmpty())
expect { sut.password }.to(beEmpty())
expect { sut.isSigninButtonEnabled }.to(beFalse())
expect { sut.isSigninEnabled }.to(beFalse())
}
}
context("유저가 ID만 입력했다면") {
Expand All @@ -30,7 +30,7 @@ final class SigninViewModelSpec: QuickSpec {
it("isSigninButtonEnabled은 false이다.") {
expect { sut.id }.toNot(beEmpty())
expect { sut.password }.to(beEmpty())
expect { sut.isSigninButtonEnabled }.to(beFalse())
expect { sut.isSigninEnabled }.to(beFalse())
}
}
context("유저가 Password만 입력했다면") {
Expand All @@ -40,7 +40,7 @@ final class SigninViewModelSpec: QuickSpec {
it("isSigninButtonEnabled은 false이다.") {
expect { sut.id }.to(beEmpty())
expect { sut.password }.toNot(beEmpty())
expect { sut.isSigninButtonEnabled }.to(beFalse())
expect { sut.isSigninEnabled }.to(beFalse())
}
}
context("유저가 ID와 Password를 모두 입력했다면") {
Expand All @@ -51,7 +51,7 @@ final class SigninViewModelSpec: QuickSpec {
it("isSigninButtonEnabled은 true이다.") {
expect { sut.id }.toNot(beEmpty())
expect { sut.password }.toNot(beEmpty())
expect { sut.isSigninButtonEnabled }.to(beTrue())
expect { sut.isSigninEnabled }.to(beTrue())
}
}
context("유저가 ID와 Password를 알맞게 입력하고 로그인을 시도한다면") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ struct SchoolCodeView: View {
Text("학교 인증코드 입력")
.dmsFont(.text(.medium), color: .GrayScale.gray6)
}
.padding(.top, 28)

Spacer()
}
.padding(.top, 24)
.padding(.horizontal, 24)

Spacer()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import DomainModule
import NeedleFoundation
import SwiftUI

public protocol SignupEmailAuthCodeVerifyDependency: Dependency {
var sendAuthCodeUseCase: any SendAuthCodeUseCase { get }
var verifyAuthCodeUseCase: any VerifyAuthCodeUseCase { get }
}

public final class SignupEmailAuthCodeVerifyComponent: Component<SignupEmailAuthCodeVerifyDependency> {
public func makeView(signupEmailAuthCodeVerifyParam: SignupEmailAuthCodeVerifyParam) -> some View {
SignupEmailAuthCodeVerifyView(
viewModel: .init(
sendAuthCodeUseCase: dependency.sendAuthCodeUseCase,
verifyAuthCodeUseCase: dependency.verifyAuthCodeUseCase,
signupEmailAuthCodeVerifyParam: signupEmailAuthCodeVerifyParam
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import BaseFeature
import DesignSystem
import SwiftUI

struct SignupEmailAuthCodeVerifyView: View {
@StateObject var viewModel: SignupEmailAuthCodeVerifyViewModel
@Environment(\.dismiss) var dismiss

init(
viewModel: SignupEmailAuthCodeVerifyViewModel
) {
_viewModel = StateObject(wrappedValue: viewModel)
}

var body: some View {
VStack {
HStack {
VStack(alignment: .leading, spacing: 8) {
Text("DMS")
.dmsFont(.title(.extraLarge), color: .PrimaryVariant.primary)

Text("이메일 주소 입력")
.dmsFont(.text(.medium), color: .GrayScale.gray6)
}

Spacer()
}
.padding(.top, 24)

VStack(spacing: 40) {
DMSPassCodeView(codeCount: 6, text: $viewModel.authCode)

Text(viewModel.isErrorOcuured ? viewModel.errorMessage : "이메일로 전송된 인증코드 6자리를 입력해주세요.")
.dmsFont(.text(.small), color: viewModel.isErrorOcuured ? .System.error : .GrayScale.gray5)
}
.padding(.top, 60)

Text(viewModel.timeText)
.dmsFont(.text(.small), color: .PrimaryVariant.primary)
.padding(.top, 10)

Spacer()

DMSButton(text: "인증코드 재발송", style: .underline, color: .GrayScale.gray6) {
viewModel.sendEmailAuthCode()
}

DMSWideButton(text: "인증", color: .PrimaryVariant.primary) {
viewModel.verifyEmailAuthCode()
}
.padding(.top, 32)
.padding(.bottom, 40)
}
.dmsBackButton(dismiss: dismiss)
.padding(.horizontal, 24)
.onAppear {
UIApplication.shared.hideKeyboard()
viewModel.sendEmailAuthCode()
}
.dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import BaseFeature
import Combine
import DomainModule
import ErrorModule
import Foundation

final class SignupEmailAuthCodeVerifyViewModel: BaseViewModel {
@Published var authCode = "" {
didSet { isErrorOcuured = false }
}
@Published var timeRemaining = 180
@Published var isShowingToast = false
@Published var toastMessage = ""
@Published var isNavigateSignupID = false
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var timeText: String {
timeRemaining % 60 < 10 ?
"\(timeRemaining/60):0\(timeRemaining%60)" :
"\(timeRemaining/60):\(timeRemaining%60)"
}

private let sendAuthCodeUseCase: any SendAuthCodeUseCase
private let verifyAuthCodeUseCase: any VerifyAuthCodeUseCase
let signupEmailAuthCodeVerifyParam: SignupEmailAuthCodeVerifyParam

init(
sendAuthCodeUseCase: any SendAuthCodeUseCase,
verifyAuthCodeUseCase: any VerifyAuthCodeUseCase,
signupEmailAuthCodeVerifyParam: SignupEmailAuthCodeVerifyParam
) {
self.sendAuthCodeUseCase = sendAuthCodeUseCase
self.verifyAuthCodeUseCase = verifyAuthCodeUseCase
self.signupEmailAuthCodeVerifyParam = signupEmailAuthCodeVerifyParam
super.init()

addCancellable(
timer.setFailureType(to: DmsError.self).eraseToAnyPublisher()
) { [weak self] _ in
guard let self, self.timeRemaining > 0 else { return }
self.timeRemaining -= 1
}
}

func sendEmailAuthCode() {
addCancellable(
sendAuthCodeUseCase.execute(
req: .init(email: signupEmailAuthCodeVerifyParam.email, type: .signup)
)
) { [weak self] _ in
self?.authCode = ""
self?.timeRemaining = 180
self?.toastMessage = "입력하신 이메일로 인증번호가 전송되었습니다."
self?.isShowingToast = true
}
}

func verifyEmailAuthCode() {
addCancellable(
verifyAuthCodeUseCase.execute(
req: .init(email: signupEmailAuthCodeVerifyParam.email, authCode: authCode, type: .signup)
)
) { [weak self] _ in
self?.isNavigateSignupID = true
} onReceiveError: { [weak self] _ in
self?.authCode = ""
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

public struct SignupEmailAuthCodeVerifyParam: Equatable {
public init(schoolCode: String, schoolAnswer: String, email: String) {
self.schoolCode = schoolCode
self.schoolAnswer = schoolAnswer
self.email = email
}

public let schoolCode: String
public let schoolAnswer: String
public let email: String
}
Loading