LINE 로그인 지원 추가

LINE 로그인 요청과 토큰 처리 흐름을 추가함
This commit is contained in:
Yu Sung
2026-01-28 19:05:42 +09:00
parent 5e85b1d679
commit 42e375ec4b
10 changed files with 131 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "23fbb8fc47a95510f46840139efb1770d536256f32ac2ba9f74cf03ee90a3979", "originHash" : "9f35428c4c178ca4a8bfa4b72544585a9e4d5b119825b423e1d2166cbe03fe37",
"pins" : [ "pins" : [
{ {
"identity" : "abseil-cpp-binary", "identity" : "abseil-cpp-binary",
@@ -154,6 +154,15 @@
"version" : "1.22.5" "version" : "1.22.5"
} }
}, },
{
"identity" : "line-sdk-ios-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/line/line-sdk-ios-swift.git",
"state" : {
"revision" : "51ef2ebefb05db8f748e80208b3281ca723abcdb",
"version" : "5.14.0"
}
},
{ {
"identity" : "moya", "identity" : "moya",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -14,6 +14,7 @@ import FBSDKCoreKit
import FirebaseCore import FirebaseCore
import FirebaseAnalytics import FirebaseAnalytics
import FirebaseMessaging import FirebaseMessaging
import LineSDK
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -24,6 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure() FirebaseApp.configure()
LoginManager.shared.setup(channelID: LINE_CHANNEL_ID, universalLinkURL: nil)
Notifly.initialize(projectId: NOTIFLY_PROJECT_ID, username: NOTIFLY_USERNAME, password: NOTIFLY_PASSWORD) Notifly.initialize(projectId: NOTIFLY_PROJECT_ID, username: NOTIFLY_USERNAME, password: NOTIFLY_PASSWORD)
Messaging.messaging().delegate = self Messaging.messaging().delegate = self
setupAppsFlyer() setupAppsFlyer()
@@ -75,6 +77,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool {
_ = LoginManager.shared.application(application, open: userActivity.webpageURL)
AppsFlyerLib.shared().continue(userActivity, restorationHandler: nil) AppsFlyerLib.shared().continue(userActivity, restorationHandler: nil)
return true return true
} }

View File

@@ -13,6 +13,7 @@ import AppsFlyerLib
import GoogleSignIn import GoogleSignIn
import KakaoSDKCommon import KakaoSDKCommon
import KakaoSDKAuth import KakaoSDKAuth
import LineSDK
@main @main
struct SodaLiveApp: App { struct SodaLiveApp: App {
@@ -83,6 +84,7 @@ struct SodaLiveApp: App {
comps.path.lowercased() == "/result" { comps.path.lowercased() == "/result" {
canPgPaymentViewModel.handleVerifyOpenURL(url) canPgPaymentViewModel.handleVerifyOpenURL(url)
} else { } else {
_ = LoginManager.shared.application(UIApplication.shared, open: url, options: [:])
ApplicationDelegate.shared.application(UIApplication.shared, open: url, options: [:]) ApplicationDelegate.shared.application(UIApplication.shared, open: url, options: [:])
AppsFlyerLib.shared().handleOpen(url) AppsFlyerLib.shared().handleOpen(url)
GIDSignIn.sharedInstance.handle(url) GIDSignIn.sharedInstance.handle(url)

View File

@@ -27,3 +27,5 @@ let GID_CLIENT_ID = "758414412471-3cf403jb4s405eu17qrfrcbs9ofhq369.apps.googleus
let GID_SERVER_CLIENT_ID = "758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com" let GID_SERVER_CLIENT_ID = "758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com"
let KAKAO_APP_KEY = "20cf19413d63bfdfd30e8e6dff933d33" let KAKAO_APP_KEY = "20cf19413d63bfdfd30e8e6dff933d33"
let LINE_CHANNEL_ID = "2008995582"

View File

@@ -1153,6 +1153,32 @@ enum I18n {
) )
} }
} }
enum Line {
static var openFailed: String {
pick(
ko: "라인 로그인 화면을 열 수 없습니다.\n다시 시도해 주세요.",
en: "Unable to open LINE sign-in.\nPlease try again.",
ja: "LINEログイン画面を開けません。\nもう一度お試しください。"
)
}
static var signInFailed: String {
pick(
ko: "라인 로그인에 실패했습니다.\n다시 시도해 주세요.",
en: "LINE sign-in failed.\nPlease try again.",
ja: "LINEログインに失敗しました。\nもう一度お試しください。"
)
}
static var tokenMissing: String {
pick(
ko: "라인 인증 토큰을 가져오지 못했습니다.",
en: "Failed to retrieve LINE token.",
ja: "LINE認証トークンを取得できませんでした。"
)
}
}
} }
// (Text Message) // (Text Message)

View File

@@ -150,6 +150,7 @@ struct LoginView: View {
Image("ic_login_line") Image("ic_login_line")
.onTapGesture { .onTapGesture {
hideKeyboard() hideKeyboard()
viewModel.loginWithLine()
} }
Image("ic_login_x") Image("ic_login_x")

View File

@@ -15,12 +15,14 @@ import UIKit
import GoogleSignIn import GoogleSignIn
import KakaoSDKUser import KakaoSDKUser
import KakaoSDKAuth import KakaoSDKAuth
import LineSDK
final class LoginViewModel: NSObject, ObservableObject { final class LoginViewModel: NSObject, ObservableObject {
private let appViewModel = AppViewModel() private let appViewModel = AppViewModel()
private let repository = UserRepository() private let repository = UserRepository()
private var subscription = Set<AnyCancellable>() private var subscription = Set<AnyCancellable>()
private var currentNonce: String? private var currentNonce: String?
private var currentLineNonce: String?
@Published var email = "" @Published var email = ""
@Published var password = "" @Published var password = ""
@@ -127,6 +129,49 @@ final class LoginViewModel: NSObject, ObservableObject {
KakaoSDKUser.UserApi.shared.loginWithKakaoAccount(completion: loginHandler) KakaoSDKUser.UserApi.shared.loginWithKakaoAccount(completion: loginHandler)
} }
} }
func loginWithLine() {
guard let presentingViewController = presentingViewController() else {
self.errorMessage = I18n.Login.Line.openFailed
self.isShowPopup = true
return
}
let nonce = randomNonceString()
currentLineNonce = nonce
var parameters = LoginManager.Parameters()
parameters.IDTokenNonce = nonce
DispatchQueue.main.async {
LoginManager.shared.login(permissions: [.profile, .openID], in: presentingViewController, parameters: parameters) { result in
switch result {
case .success(let loginResult):
guard let identityToken = loginResult.accessToken.IDTokenRaw, !identityToken.isEmpty else {
self.currentLineNonce = nil
self.errorMessage = I18n.Login.Line.tokenMissing
self.isShowPopup = true
return
}
guard let lineNonce = self.currentLineNonce else {
self.errorMessage = I18n.Common.commonError
self.isShowPopup = true
return
}
self.currentLineNonce = nil
self.loginWithLine(identityToken: identityToken, nonce: lineNonce)
case .failure(let error):
self.currentLineNonce = nil
ERROR_LOG(error.localizedDescription)
self.errorMessage = I18n.Login.Line.signInFailed
self.isShowPopup = true
}
}
}
}
private func loginWithApple(identityToken: String, nonce: String) { private func loginWithApple(identityToken: String, nonce: String) {
let pushToken = UserDefaults.string(forKey: .pushToken) let pushToken = UserDefaults.string(forKey: .pushToken)
@@ -209,6 +254,33 @@ final class LoginViewModel: NSObject, ObservableObject {
.store(in: &subscription) .store(in: &subscription)
} }
private func loginWithLine(identityToken: String, nonce: String) {
let pushToken = UserDefaults.string(forKey: .pushToken)
let marketingPid = UserDefaults.string(forKey: .marketingPid)
let request = SocialLoginRequest(
container: "ios",
pushToken: pushToken.isEmpty ? nil : pushToken,
marketingPid: marketingPid.isEmpty ? nil : marketingPid,
identityToken: identityToken,
nonce: nonce
)
isLoading = true
repository.loginLine(request: request)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
self.isLoading = false
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { response in
self.handleLoginResponse(response)
}
.store(in: &subscription)
}
private func presentingViewController() -> UIViewController? { private func presentingViewController() -> UIViewController? {
return UIApplication.shared.connectedScenes return UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene } .compactMap { $0 as? UIWindowScene }

View File

@@ -13,6 +13,7 @@ enum UserApi {
case loginApple(request: SocialLoginRequest) case loginApple(request: SocialLoginRequest)
case loginGoogle(request: SocialLoginRequest, idToken: String) case loginGoogle(request: SocialLoginRequest, idToken: String)
case loginKakao(request: SocialLoginRequest, accessToken: String) case loginKakao(request: SocialLoginRequest, accessToken: String)
case loginLine(request: SocialLoginRequest)
case signUp(request: SignUpRequest) case signUp(request: SignUpRequest)
case findPassword(request: ForgotPasswordRequest) case findPassword(request: ForgotPasswordRequest)
case searchUser(nickname: String) case searchUser(nickname: String)
@@ -58,6 +59,9 @@ extension UserApi: TargetType {
case .loginKakao: case .loginKakao:
return "/member/login/kakao" return "/member/login/kakao"
case .loginLine:
return "/member/login/line"
case .signUp: case .signUp:
return "/member/signup/v2" return "/member/signup/v2"
@@ -132,7 +136,7 @@ extension UserApi: TargetType {
var method: Moya.Method { var method: Moya.Method {
switch self { switch self {
case .login, .loginApple, .loginGoogle, .loginKakao, .signUp, .findPassword, .notification, .logout, .logoutAllDevice, .signOut, .creatorFollow, .creatorUnFollow, .memberBlock, .memberUnBlock, case .login, .loginApple, .loginGoogle, .loginKakao, .loginLine, .signUp, .findPassword, .notification, .logout, .logoutAllDevice, .signOut, .creatorFollow, .creatorUnFollow, .memberBlock, .memberUnBlock,
.profileImageUpdate: .profileImageUpdate:
return .post return .post
@@ -157,6 +161,9 @@ extension UserApi: TargetType {
case .loginKakao(let request, _): case .loginKakao(let request, _):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .loginLine(let request):
return .requestJSONEncodable(request)
case .signUp(let request): case .signUp(let request):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
@@ -220,7 +227,7 @@ extension UserApi: TargetType {
var headers: [String : String]? { var headers: [String : String]? {
switch self { switch self {
case .login, .loginApple, .signUp, .findPassword: case .login, .loginApple, .loginLine, .signUp, .findPassword:
return nil return nil
case .loginGoogle(_, let idToken): case .loginGoogle(_, let idToken):

View File

@@ -28,6 +28,10 @@ final class UserRepository {
func loginKakao(request: SocialLoginRequest, accessToken: String) -> AnyPublisher<Response, MoyaError> { func loginKakao(request: SocialLoginRequest, accessToken: String) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.loginKakao(request: request, accessToken: accessToken)) return api.requestPublisher(.loginKakao(request: request, accessToken: accessToken))
} }
func loginLine(request: SocialLoginRequest) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.loginLine(request: request))
}
func signUp(request: SignUpRequest) -> AnyPublisher<Response, MoyaError> { func signUp(request: SignUpRequest) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.signUp(request: request)) return api.requestPublisher(.signUp(request: request))

View File

@@ -27,3 +27,5 @@ let GID_CLIENT_ID = "983594297130-m6bv7lvc1lsetsvv3rk92etqc98uopqj.apps.googleus
let GID_SERVER_CLIENT_ID = "983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com" let GID_SERVER_CLIENT_ID = "983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com"
let KAKAO_APP_KEY = "231cf78acfa8252fca38b9eedf87c5cb" let KAKAO_APP_KEY = "231cf78acfa8252fca38b9eedf87c5cb"
let LINE_CHANNEL_ID = "2008995539"