Skip to content

Commit 13d84d3

Browse files
authored
[#378] Github email을 받아오는 방법과 비공개 해놓았을 경우 얼럿에 해당 내용이 뜨도록 개선한다 (#380)
* refactor: 깃헙 이메일 최초 요청 시 실패할 경우 /user/emails 로 한번 더 요청해서 받아와 확인하도록 개선 * refactor: 이메일을 받지 못했을 때 클라이언트로 에러 코드 알려주도록 개선 * refactor: 받은 에러 코드를 보고 확인 후 해당 내용을 얼럿으로 띄우도록 개선 * refactor: 토큰 반환에 대한 에러 처리 개선 * docs: 앱스토어 링크로 수정 * docs: 영어 l10n 문구 수정 * chore: 버전 1.0 -> 1.0.1 * refactor: 강제 옵셔널 언래핑 제거 * refactor: 메서드 및 타입 private 처리
1 parent 057aa2e commit 13d84d3

7 files changed

Lines changed: 235 additions & 52 deletions

File tree

DevLog.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@
355355
"@executable_path/Frameworks",
356356
);
357357
LOCALIZED_STRING_SWIFTUI_SUPPORT = YES;
358-
MARKETING_VERSION = 1.0;
358+
MARKETING_VERSION = 1.0.1;
359359
PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog;
360360
PRODUCT_NAME = "$(TARGET_NAME)";
361361
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -402,7 +402,7 @@
402402
"@executable_path/Frameworks",
403403
);
404404
LOCALIZED_STRING_SWIFTUI_SUPPORT = YES;
405-
MARKETING_VERSION = 1.0;
405+
MARKETING_VERSION = 1.0.1;
406406
PRODUCT_BUNDLE_IDENTIFIER = opfic.DevLog;
407407
PRODUCT_NAME = "$(TARGET_NAME)";
408408
PROVISIONING_PROFILE_SPECIFIER = "";

DevLog/Infra/Common/InfraLayerError.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ enum UIError: Error {
2727
enum EmailFetchError: Error, Equatable {
2828
case emailNotFound
2929
case emailMismatch
30+
31+
var code: String {
32+
switch self {
33+
case .emailMismatch:
34+
"email_mismatch"
35+
case .emailNotFound:
36+
"email_not_found"
37+
}
38+
}
39+
}
40+
41+
enum TokenError: Error {
42+
case invalidResponse
3043
}
3144

3245
enum SocialLoginError: Error {

DevLog/Infra/Service/SocialLogin/GithubAuthenticationService.swift

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ final class GithubAuthenticationService: NSObject, AuthenticationService {
152152
}
153153

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

214214
// Firebase Function 호출: Custom Token 발급
215-
func requestTokens(authorizationCode: String) async throws -> (String, String) {
215+
private func requestTokens(authorizationCode: String) async throws -> (String, String) {
216216
let requestTokenFunction = functions.httpsCallable(FunctionName.requestGithubTokens)
217-
let result = try await requestTokenFunction.call(["code": authorizationCode])
218-
219-
if let data = result.data as? [String: Any],
220-
let accessToken = data["accessToken"] as? String,
221-
let customToken = data["customToken"] as? String {
222-
return (accessToken, customToken)
217+
218+
do {
219+
let result = try await requestTokenFunction.call(["code": authorizationCode])
220+
221+
if let data = result.data as? [String: Any],
222+
let accessToken = data["accessToken"] as? String,
223+
let customToken = data["customToken"] as? String {
224+
return (accessToken, customToken)
225+
}
226+
throw TokenError.invalidResponse
227+
} catch {
228+
throw mapRequestTokensError(error)
223229
}
224-
throw URLError(.badServerResponse)
225230
}
226231

227-
func revokeAccessToken(accessToken: String? = nil) async throws {
232+
private func revokeAccessToken(accessToken: String? = nil) async throws {
228233
var param: [String: Any] = [:]
229234

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

239244
// GitHub API로 사용자 프로필 정보 가져오기
240-
func requestUserProfile(accessToken: String) async throws -> GitHubUser {
241-
var request = URLRequest(url: URL(string: "https://api.github.com/user")!)
245+
private func requestUserProfile(accessToken: String) async throws -> GitHubUser {
246+
guard let url = URL(string: "https://api.github.com/user") else {
247+
throw URLError(.badURL)
248+
}
249+
250+
var request = URLRequest(url: url)
242251
request.httpMethod = "GET"
243252
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
244253
request.addValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
@@ -251,15 +260,62 @@ final class GithubAuthenticationService: NSObject, AuthenticationService {
251260
}
252261

253262
let decoder = JSONDecoder()
254-
return try decoder.decode(GitHubUser.self, from: data)
263+
let gitHubUser = try decoder.decode(GitHubUser.self, from: data)
264+
265+
if gitHubUser.email != nil {
266+
return gitHubUser
267+
}
268+
269+
let email = try await requestPrimaryVerifiedEmail(accessToken: accessToken)
270+
return GitHubUser(
271+
login: gitHubUser.login,
272+
name: gitHubUser.name,
273+
avatarURL: gitHubUser.avatarURL,
274+
email: email
275+
)
255276
}
256-
}
257277

258-
extension GithubAuthenticationService: ASWebAuthenticationPresentationContextProviding {
259-
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
260-
return provider.keyWindow() ?? ASPresentationAnchor()
278+
private func requestPrimaryVerifiedEmail(accessToken: String) async throws -> String? {
279+
guard let url = URL(string: "https://api.github.com/user/emails") else {
280+
throw URLError(.badURL)
281+
}
282+
283+
var request = URLRequest(url: url)
284+
request.httpMethod = "GET"
285+
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
286+
request.addValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
287+
288+
let (data, response) = try await URLSession.shared.data(for: request)
289+
290+
guard let httpResponse = response as? HTTPURLResponse,
291+
httpResponse.statusCode == 200 else {
292+
throw URLError(.badServerResponse)
293+
}
294+
295+
let decoder = JSONDecoder()
296+
let gitHubEmails = try decoder.decode([GitHubEmail].self, from: data)
297+
298+
if let primaryVerifiedEmail = gitHubEmails.first(where: { $0.primary && $0.verified }) {
299+
return primaryVerifiedEmail.email
300+
}
301+
302+
return gitHubEmails.first(where: { $0.verified })?.email
261303
}
262304

305+
private func mapRequestTokensError(_ error: Error) -> Error {
306+
let nsError = error as NSError
307+
guard nsError.domain == FunctionsErrorDomain,
308+
let details = nsError.userInfo[FunctionsErrorDetailsKey] as? [String: Any],
309+
let reason = details["reason"] as? String,
310+
reason == EmailFetchError.emailNotFound.code else {
311+
return error
312+
}
313+
314+
return EmailFetchError.emailNotFound
315+
}
316+
}
317+
318+
private extension GithubAuthenticationService {
263319
struct GitHubUser: Codable {
264320
let login: String
265321
let name: String?
@@ -274,4 +330,15 @@ extension GithubAuthenticationService: ASWebAuthenticationPresentationContextPro
274330
}
275331
}
276332

333+
struct GitHubEmail: Codable {
334+
let email: String
335+
let primary: Bool
336+
let verified: Bool
337+
}
338+
}
339+
340+
extension GithubAuthenticationService: ASWebAuthenticationPresentationContextProviding {
341+
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
342+
return provider.keyWindow() ?? ASPresentationAnchor()
343+
}
277344
}

DevLog/Presentation/ViewModel/LoginViewModel.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ final class LoginViewModel: Store {
1212
struct State: Equatable {
1313
var isLoading = false
1414
var showAlert: Bool = false
15+
var alertType: AlertType?
1516
var alertTitle: String = ""
1617
var alertMessage: String = ""
1718
}
1819

1920
enum Action {
20-
case setAlert(Bool)
21+
case setAlert(Bool, AlertType? = nil)
2122
case tapSignInButton(AuthProvider)
2223
case setLoading(Bool)
2324
}
@@ -26,6 +27,11 @@ final class LoginViewModel: Store {
2627
case signIn(AuthProvider)
2728
}
2829

30+
enum AlertType {
31+
case emailUnavailable
32+
case error
33+
}
34+
2935
private let signInUseCase: SignInUseCase
3036
private let loadingState = LoadingState()
3137

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

4450
switch action {
45-
case .setAlert(let isPresented):
46-
setAlert(&state, isPresented: isPresented)
51+
case .setAlert(let isPresented, let alertType):
52+
setAlert(&state, isPresented: isPresented, alertType: alertType)
4753
case .tapSignInButton(let authProvider):
4854
effects = [.signIn(authProvider)]
4955
case .setLoading(let value):
@@ -64,7 +70,7 @@ final class LoginViewModel: Store {
6470
try await self.signInUseCase.execute(authProvider)
6571
} catch {
6672
if error.isSocialLoginCancelled { return }
67-
send(.setAlert(true))
73+
send(.setAlert(true, alertType(for: error)))
6874
}
6975
}
7076
}
@@ -75,10 +81,30 @@ private extension LoginViewModel {
7581
func setAlert(
7682
_ state: inout State,
7783
isPresented: Bool,
84+
alertType: AlertType?,
7885
) {
79-
state.alertTitle = String(localized: "common_error_title")
80-
state.alertMessage = String(localized: "common_error_message")
86+
switch alertType {
87+
case .emailUnavailable:
88+
state.alertTitle = String(localized: "login_alert_email_unavailable_title")
89+
state.alertMessage = String(localized: "login_alert_email_unavailable_message")
90+
case .error:
91+
state.alertTitle = String(localized: "common_error_title")
92+
state.alertMessage = String(localized: "common_error_message")
93+
case .none:
94+
state.alertTitle = ""
95+
state.alertMessage = ""
96+
}
8197
state.showAlert = isPresented
98+
state.alertType = alertType
99+
}
100+
101+
func alertType(for error: Error) -> AlertType {
102+
if let emailFetchError = error as? EmailFetchError,
103+
emailFetchError == .emailNotFound {
104+
return .emailUnavailable
105+
}
106+
107+
return .error
82108
}
83109

84110
func beginLoading(_ mode: LoadingState.Mode) {

DevLog/Resource/Localizable.xcstrings

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
"en" : {
126126
"stringUnit" : {
127127
"state" : "translated",
128-
"value" : "Email Unavailable"
128+
"value" : "Unable to Verify Email"
129129
}
130130
},
131131
"ko" : {
@@ -537,6 +537,40 @@
537537
}
538538
}
539539
},
540+
"login_alert_email_unavailable_message" : {
541+
"extractionState" : "manual",
542+
"localizations" : {
543+
"en" : {
544+
"stringUnit" : {
545+
"state" : "translated",
546+
"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."
547+
}
548+
},
549+
"ko" : {
550+
"stringUnit" : {
551+
"state" : "translated",
552+
"value" : "선택한 GitHub 계정의 이메일 정보를 확인할 수 없어 로그인할 수 없어요. GitHub 계정 설정을 확인한 뒤 다시 시도해주세요."
553+
}
554+
}
555+
}
556+
},
557+
"login_alert_email_unavailable_title" : {
558+
"extractionState" : "manual",
559+
"localizations" : {
560+
"en" : {
561+
"stringUnit" : {
562+
"state" : "translated",
563+
"value" : "Unable to Verify Email"
564+
}
565+
},
566+
"ko" : {
567+
"stringUnit" : {
568+
"state" : "translated",
569+
"value" : "이메일 확인 불가"
570+
}
571+
}
572+
}
573+
},
540574
"login_google_sign_in" : {
541575
"extractionState" : "manual",
542576
"localizations" : {

0 commit comments

Comments
 (0)