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
4 changes: 2 additions & 2 deletions DevLog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@
"@executable_path/Frameworks",
);
LOCALIZED_STRING_SWIFTUI_SUPPORT = YES;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -402,7 +402,7 @@
"@executable_path/Frameworks",
);
LOCALIZED_STRING_SWIFTUI_SUPPORT = YES;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
13 changes: 13 additions & 0 deletions DevLog/Infra/Common/InfraLayerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ enum UIError: Error {
enum EmailFetchError: Error, Equatable {
case emailNotFound
case emailMismatch

var code: String {
switch self {
case .emailMismatch:
"email_mismatch"
case .emailNotFound:
"email_not_found"
}
}
}

enum TokenError: Error {
case invalidResponse
}

enum SocialLoginError: Error {
Expand Down
101 changes: 84 additions & 17 deletions DevLog/Infra/Service/SocialLogin/GithubAuthenticationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ final class GithubAuthenticationService: NSObject, AuthenticationService {
}

@MainActor
func requestAuthorizationCode() async throws -> String {
private func requestAuthorizationCode() async throws -> String {
guard let clientID = Bundle.main.object(forInfoDictionaryKey: "GITHUB_CLIENT_ID") as? String,
let redirectURL = Bundle.main.object(forInfoDictionaryKey: "APP_REDIRECT_URL") as? String,
let urlComponents = URLComponents(string: redirectURL),
Expand Down Expand Up @@ -212,19 +212,24 @@ final class GithubAuthenticationService: NSObject, AuthenticationService {
}

// Firebase Function 호출: Custom Token 발급
func requestTokens(authorizationCode: String) async throws -> (String, String) {
private func requestTokens(authorizationCode: String) async throws -> (String, String) {
let requestTokenFunction = functions.httpsCallable(FunctionName.requestGithubTokens)
let result = try await requestTokenFunction.call(["code": authorizationCode])

if let data = result.data as? [String: Any],
let accessToken = data["accessToken"] as? String,
let customToken = data["customToken"] as? String {
return (accessToken, customToken)

do {
let result = try await requestTokenFunction.call(["code": authorizationCode])

if let data = result.data as? [String: Any],
let accessToken = data["accessToken"] as? String,
let customToken = data["customToken"] as? String {
return (accessToken, customToken)
}
throw TokenError.invalidResponse
} catch {
throw mapRequestTokensError(error)
}
throw URLError(.badServerResponse)
}

func revokeAccessToken(accessToken: String? = nil) async throws {
private func revokeAccessToken(accessToken: String? = nil) async throws {
var param: [String: Any] = [:]

if let accessToken = accessToken {
Expand All @@ -237,8 +242,12 @@ final class GithubAuthenticationService: NSObject, AuthenticationService {
}

// GitHub API로 사용자 프로필 정보 가져오기
func requestUserProfile(accessToken: String) async throws -> GitHubUser {
var request = URLRequest(url: URL(string: "https://api.github.com/user")!)
private func requestUserProfile(accessToken: String) async throws -> GitHubUser {
guard let url = URL(string: "https://api.github.com/user") else {
throw URLError(.badURL)
}

var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.addValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
Expand All @@ -251,15 +260,62 @@ final class GithubAuthenticationService: NSObject, AuthenticationService {
}

let decoder = JSONDecoder()
return try decoder.decode(GitHubUser.self, from: data)
let gitHubUser = try decoder.decode(GitHubUser.self, from: data)

if gitHubUser.email != nil {
return gitHubUser
}

let email = try await requestPrimaryVerifiedEmail(accessToken: accessToken)
return GitHubUser(
login: gitHubUser.login,
name: gitHubUser.name,
avatarURL: gitHubUser.avatarURL,
email: email
)
}
}

extension GithubAuthenticationService: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return provider.keyWindow() ?? ASPresentationAnchor()
private func requestPrimaryVerifiedEmail(accessToken: String) async throws -> String? {
guard let url = URL(string: "https://api.github.com/user/emails") else {
throw URLError(.badURL)
}

var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.addValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")

let (data, response) = try await URLSession.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}

let decoder = JSONDecoder()
let gitHubEmails = try decoder.decode([GitHubEmail].self, from: data)

if let primaryVerifiedEmail = gitHubEmails.first(where: { $0.primary && $0.verified }) {
return primaryVerifiedEmail.email
}

return gitHubEmails.first(where: { $0.verified })?.email
}

private func mapRequestTokensError(_ error: Error) -> Error {
let nsError = error as NSError
guard nsError.domain == FunctionsErrorDomain,
let details = nsError.userInfo[FunctionsErrorDetailsKey] as? [String: Any],
let reason = details["reason"] as? String,
reason == EmailFetchError.emailNotFound.code else {
return error
}

return EmailFetchError.emailNotFound
}
}

private extension GithubAuthenticationService {
struct GitHubUser: Codable {
let login: String
let name: String?
Expand All @@ -274,4 +330,15 @@ extension GithubAuthenticationService: ASWebAuthenticationPresentationContextPro
}
}

struct GitHubEmail: Codable {
let email: String
let primary: Bool
let verified: Bool
}
}

extension GithubAuthenticationService: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return provider.keyWindow() ?? ASPresentationAnchor()
}
}
38 changes: 32 additions & 6 deletions DevLog/Presentation/ViewModel/LoginViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ final class LoginViewModel: Store {
struct State: Equatable {
var isLoading = false
var showAlert: Bool = false
var alertType: AlertType?
var alertTitle: String = ""
var alertMessage: String = ""
}

enum Action {
case setAlert(Bool)
case setAlert(Bool, AlertType? = nil)
case tapSignInButton(AuthProvider)
case setLoading(Bool)
}
Expand All @@ -26,6 +27,11 @@ final class LoginViewModel: Store {
case signIn(AuthProvider)
}

enum AlertType {
case emailUnavailable
case error
}

private let signInUseCase: SignInUseCase
private let loadingState = LoadingState()

Expand All @@ -42,8 +48,8 @@ final class LoginViewModel: Store {
var effects: [SideEffect] = []

switch action {
case .setAlert(let isPresented):
setAlert(&state, isPresented: isPresented)
case .setAlert(let isPresented, let alertType):
setAlert(&state, isPresented: isPresented, alertType: alertType)
case .tapSignInButton(let authProvider):
effects = [.signIn(authProvider)]
case .setLoading(let value):
Expand All @@ -64,7 +70,7 @@ final class LoginViewModel: Store {
try await self.signInUseCase.execute(authProvider)
} catch {
if error.isSocialLoginCancelled { return }
send(.setAlert(true))
send(.setAlert(true, alertType(for: error)))
}
}
}
Expand All @@ -75,10 +81,30 @@ private extension LoginViewModel {
func setAlert(
_ state: inout State,
isPresented: Bool,
alertType: AlertType?,
) {
state.alertTitle = String(localized: "common_error_title")
state.alertMessage = String(localized: "common_error_message")
switch alertType {
case .emailUnavailable:
state.alertTitle = String(localized: "login_alert_email_unavailable_title")
state.alertMessage = String(localized: "login_alert_email_unavailable_message")
case .error:
state.alertTitle = String(localized: "common_error_title")
state.alertMessage = String(localized: "common_error_message")
case .none:
state.alertTitle = ""
state.alertMessage = ""
}
state.showAlert = isPresented
state.alertType = alertType
}

func alertType(for error: Error) -> AlertType {
if let emailFetchError = error as? EmailFetchError,
emailFetchError == .emailNotFound {
return .emailUnavailable
}

return .error
}

func beginLoading(_ mode: LoadingState.Mode) {
Expand Down
36 changes: 35 additions & 1 deletion DevLog/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Email Unavailable"
"value" : "Unable to Verify Email"
}
},
"ko" : {
Expand Down Expand Up @@ -537,6 +537,40 @@
}
}
},
"login_alert_email_unavailable_message" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The selected GitHub account's email could not be verified, so sign in could not be completed. Check the GitHub account settings and try again."
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "선택한 GitHub 계정의 이메일 정보를 확인할 수 없어 로그인할 수 없어요. GitHub 계정 설정을 확인한 뒤 다시 시도해주세요."
}
}
}
},
"login_alert_email_unavailable_title" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unable to Verify Email"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "이메일 확인 불가"
}
}
}
},
"login_google_sign_in" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
Loading