Skip to content

Commit cf7b5d8

Browse files
authored
Merge pull request #50 from ApptiveDev/dev
[Release] 1.0.19 업데이트
2 parents 712afb7 + 9d12d57 commit cf7b5d8

23 files changed

+770
-187
lines changed

KillingPart.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
435435
CODE_SIGN_ENTITLEMENTS = KillingPart/KillingPart.entitlements;
436436
CODE_SIGN_STYLE = Automatic;
437-
CURRENT_PROJECT_VERSION = 18;
437+
CURRENT_PROJECT_VERSION = 19;
438438
DEAD_CODE_STRIPPING = YES;
439439
DEVELOPMENT_TEAM = GQ89YG5G9R;
440440
ENABLE_APP_SANDBOX = YES;
@@ -459,7 +459,7 @@
459459
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
460460
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
461461
MACOSX_DEPLOYMENT_TARGET = 14.0;
462-
MARKETING_VERSION = 1.0.18;
462+
MARKETING_VERSION = 1.0.19;
463463
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
464464
PRODUCT_NAME = "$(TARGET_NAME)";
465465
REGISTER_APP_GROUPS = YES;
@@ -479,7 +479,7 @@
479479
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
480480
CODE_SIGN_ENTITLEMENTS = KillingPart/KillingPart.entitlements;
481481
CODE_SIGN_STYLE = Automatic;
482-
CURRENT_PROJECT_VERSION = 18;
482+
CURRENT_PROJECT_VERSION = 19;
483483
DEAD_CODE_STRIPPING = YES;
484484
DEVELOPMENT_TEAM = GQ89YG5G9R;
485485
ENABLE_APP_SANDBOX = YES;
@@ -504,7 +504,7 @@
504504
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
505505
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
506506
MACOSX_DEPLOYMENT_TARGET = 14.0;
507-
MARKETING_VERSION = 1.0.18;
507+
MARKETING_VERSION = 1.0.19;
508508
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
509509
PRODUCT_NAME = "$(TARGET_NAME)";
510510
REGISTER_APP_GROUPS = YES;
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
4-
<dict/>
4+
<dict>
5+
<key>com.apple.developer.applesignin</key>
6+
<array>
7+
<string>Default</string>
8+
</array>
9+
</dict>
510
</plist>

KillingPart/Models/AuthModel.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
3+
struct KakaoLoginRequest: Encodable {
4+
let accessToken: String
5+
}
6+
7+
struct AppleLoginRequest: Encodable {
8+
let identityToken: String
9+
let authorizationCode: String
10+
let email: String?
11+
let name: String?
12+
13+
enum CodingKeys: String, CodingKey {
14+
case identityToken
15+
case authorizationCode
16+
case email
17+
case name
18+
}
19+
20+
func encode(to encoder: Encoder) throws {
21+
var container = encoder.container(keyedBy: CodingKeys.self)
22+
try container.encode(identityToken, forKey: .identityToken)
23+
try container.encode(authorizationCode, forKey: .authorizationCode)
24+
25+
if let email {
26+
try container.encode(email, forKey: .email)
27+
} else {
28+
try container.encodeNil(forKey: .email)
29+
}
30+
31+
if let name {
32+
try container.encode(name, forKey: .name)
33+
} else {
34+
try container.encodeNil(forKey: .name)
35+
}
36+
}
37+
}
38+
39+
struct AuthLoginResponse: Decodable {
40+
let accessToken: String
41+
let refreshToken: String
42+
let isNew: Bool
43+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
3+
struct ITunesSearchResponse: Decodable {
4+
let resultCount: Int
5+
let results: [ITunesTrackItem]
6+
}
7+
8+
struct ITunesTrackItem: Decodable {
9+
let trackId: Int?
10+
let trackName: String?
11+
let artistName: String?
12+
let artworkUrl100: String?
13+
let collectionId: Int?
14+
}

KillingPart/Models/KakaoSocialLoginModels.swift

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import Foundation
2+
import AuthenticationServices
3+
import KakaoSDKUser
4+
import UIKit
5+
6+
protocol AuthServicing {
7+
@MainActor
8+
func loginWithKakao() async throws -> String
9+
10+
@MainActor
11+
func loginWithApple() async throws -> AppleAuthPayload
12+
}
13+
14+
struct AppleAuthPayload {
15+
let identityToken: String
16+
let authorizationCode: String
17+
let email: String?
18+
let name: String?
19+
}
20+
21+
enum AuthServiceError: LocalizedError {
22+
case missingNativeAppKey
23+
case kakaoLoginFailed(underlying: Error)
24+
case missingKakaoAccessToken
25+
case applePresentationAnchorUnavailable
26+
case appleAuthorizationFailed(underlying: Error)
27+
case invalidAppleCredential
28+
case missingAppleIdentityToken
29+
case missingAppleAuthorizationCode
30+
case invalidAppleIdentityTokenEncoding
31+
case invalidAppleAuthorizationCodeEncoding
32+
33+
var errorDescription: String? {
34+
switch self {
35+
case .missingNativeAppKey:
36+
return "KAKAO_NATIVE_APP_KEY 설정이 필요해요."
37+
case .kakaoLoginFailed:
38+
return "카카오 로그인에 실패했어요. 다시 시도해 주세요."
39+
case .missingKakaoAccessToken:
40+
return "카카오 액세스 토큰을 가져오지 못했어요."
41+
case .applePresentationAnchorUnavailable:
42+
return "애플 로그인 화면을 표시할 수 없어요. 다시 시도해 주세요."
43+
case .appleAuthorizationFailed:
44+
return "애플 로그인에 실패했어요. 다시 시도해 주세요."
45+
case .invalidAppleCredential:
46+
return "애플 로그인 정보를 확인할 수 없어요."
47+
case .missingAppleIdentityToken:
48+
return "애플 identity token을 가져오지 못했어요."
49+
case .missingAppleAuthorizationCode:
50+
return "애플 authorization code를 가져오지 못했어요."
51+
case .invalidAppleIdentityTokenEncoding, .invalidAppleAuthorizationCodeEncoding:
52+
return "애플 로그인 인증값 인코딩에 실패했어요."
53+
}
54+
}
55+
}
56+
57+
final class AuthService: NSObject, AuthServicing {
58+
private var appleLoginContinuation: CheckedContinuation<AppleAuthPayload, Error>?
59+
private var applePresentationAnchor: ASPresentationAnchor?
60+
private var appleAuthorizationController: ASAuthorizationController?
61+
62+
func loginWithKakao() async throws -> String {
63+
let appKey = (Bundle.main.object(forInfoDictionaryKey: "KAKAO_NATIVE_APP_KEY") as? String ?? "")
64+
.trimmingCharacters(in: .whitespacesAndNewlines)
65+
guard !appKey.isEmpty, appKey != "YOUR_KAKAO_NATIVE_APP_KEY" else {
66+
throw AuthServiceError.missingNativeAppKey
67+
}
68+
69+
if UserApi.isKakaoTalkLoginAvailable() {
70+
return try await loginWithKakaoTalk()
71+
}
72+
73+
return try await loginWithKakaoAccount()
74+
}
75+
76+
func loginWithApple() async throws -> AppleAuthPayload {
77+
guard let anchor = resolvePresentationAnchor() else {
78+
throw AuthServiceError.applePresentationAnchorUnavailable
79+
}
80+
81+
let request = ASAuthorizationAppleIDProvider().createRequest()
82+
request.requestedScopes = [.email, .fullName]
83+
84+
return try await withCheckedThrowingContinuation { continuation in
85+
appleLoginContinuation = continuation
86+
applePresentationAnchor = anchor
87+
88+
let controller = ASAuthorizationController(authorizationRequests: [request])
89+
controller.delegate = self
90+
controller.presentationContextProvider = self
91+
appleAuthorizationController = controller
92+
controller.performRequests()
93+
}
94+
}
95+
96+
private func loginWithKakaoTalk() async throws -> String {
97+
try await withCheckedThrowingContinuation { continuation in
98+
UserApi.shared.loginWithKakaoTalk { oauthToken, error in
99+
if let error {
100+
continuation.resume(throwing: AuthServiceError.kakaoLoginFailed(underlying: error))
101+
return
102+
}
103+
104+
guard let accessToken = oauthToken?.accessToken, !accessToken.isEmpty else {
105+
continuation.resume(throwing: AuthServiceError.missingKakaoAccessToken)
106+
return
107+
}
108+
109+
continuation.resume(returning: accessToken)
110+
}
111+
}
112+
}
113+
114+
private func loginWithKakaoAccount() async throws -> String {
115+
try await withCheckedThrowingContinuation { continuation in
116+
UserApi.shared.loginWithKakaoAccount { oauthToken, error in
117+
if let error {
118+
continuation.resume(throwing: AuthServiceError.kakaoLoginFailed(underlying: error))
119+
return
120+
}
121+
122+
guard let accessToken = oauthToken?.accessToken, !accessToken.isEmpty else {
123+
continuation.resume(throwing: AuthServiceError.missingKakaoAccessToken)
124+
return
125+
}
126+
127+
continuation.resume(returning: accessToken)
128+
}
129+
}
130+
}
131+
132+
private func resolvePresentationAnchor() -> ASPresentationAnchor? {
133+
let scenes = UIApplication.shared.connectedScenes
134+
.compactMap { $0 as? UIWindowScene }
135+
136+
if let keyWindow = scenes
137+
.flatMap(\.windows)
138+
.first(where: \.isKeyWindow) {
139+
return keyWindow
140+
}
141+
142+
return scenes
143+
.flatMap(\.windows)
144+
.first
145+
}
146+
147+
private func completeAppleLogin(_ result: Result<AppleAuthPayload, Error>) {
148+
appleAuthorizationController = nil
149+
applePresentationAnchor = nil
150+
151+
guard let continuation = appleLoginContinuation else { return }
152+
appleLoginContinuation = nil
153+
154+
switch result {
155+
case .success(let payload):
156+
continuation.resume(returning: payload)
157+
case .failure(let error):
158+
continuation.resume(throwing: error)
159+
}
160+
}
161+
}
162+
163+
extension AuthService: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
164+
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
165+
applePresentationAnchor ?? ASPresentationAnchor(frame: .zero)
166+
}
167+
168+
func authorizationController(
169+
controller: ASAuthorizationController,
170+
didCompleteWithAuthorization authorization: ASAuthorization
171+
) {
172+
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
173+
completeAppleLogin(.failure(AuthServiceError.invalidAppleCredential))
174+
return
175+
}
176+
177+
guard let identityTokenData = credential.identityToken else {
178+
completeAppleLogin(.failure(AuthServiceError.missingAppleIdentityToken))
179+
return
180+
}
181+
182+
guard let authorizationCodeData = credential.authorizationCode else {
183+
completeAppleLogin(.failure(AuthServiceError.missingAppleAuthorizationCode))
184+
return
185+
}
186+
187+
guard let identityToken = String(data: identityTokenData, encoding: .utf8), !identityToken.isEmpty else {
188+
completeAppleLogin(.failure(AuthServiceError.invalidAppleIdentityTokenEncoding))
189+
return
190+
}
191+
192+
guard let authorizationCode = String(data: authorizationCodeData, encoding: .utf8), !authorizationCode.isEmpty else {
193+
completeAppleLogin(.failure(AuthServiceError.invalidAppleAuthorizationCodeEncoding))
194+
return
195+
}
196+
197+
let email = credential.email?
198+
.trimmingCharacters(in: .whitespacesAndNewlines)
199+
let normalizedEmail = email?.isEmpty == true ? nil : email
200+
let name = normalizedAppleUserName(from: credential.fullName)
201+
202+
if normalizedEmail == nil {
203+
print("[Auth][Apple] email is missing from credential. This can happen after first authorization.")
204+
}
205+
if name == nil {
206+
print("[Auth][Apple] name is missing from credential. This can happen after first authorization.")
207+
}
208+
209+
completeAppleLogin(
210+
.success(
211+
AppleAuthPayload(
212+
identityToken: identityToken,
213+
authorizationCode: authorizationCode,
214+
email: normalizedEmail,
215+
name: name
216+
)
217+
)
218+
)
219+
}
220+
221+
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
222+
completeAppleLogin(.failure(AuthServiceError.appleAuthorizationFailed(underlying: error)))
223+
}
224+
225+
private func normalizedAppleUserName(from fullName: PersonNameComponents?) -> String? {
226+
guard let fullName else { return nil }
227+
228+
let formatter = PersonNameComponentsFormatter()
229+
let formattedName = formatter.string(from: fullName)
230+
.trimmingCharacters(in: .whitespacesAndNewlines)
231+
if !formattedName.isEmpty {
232+
return formattedName
233+
}
234+
235+
let manualName = [fullName.familyName, fullName.givenName]
236+
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
237+
.filter { !$0.isEmpty }
238+
.joined(separator: " ")
239+
240+
return manualName.isEmpty ? nil : manualName
241+
}
242+
}

0 commit comments

Comments
 (0)