From 4ddee2b1c14d05270b1e4b122e0c6504cde97f8c Mon Sep 17 00:00:00 2001 From: klaus Date: Wed, 28 Jan 2026 17:52:02 +0900 Subject: [PATCH] =?UTF-8?q?LINE=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LINE 로그인 요청에 id token과 nonce를 전달함 --- app/build.gradle | 7 ++ app/src/main/AndroidManifest.xml | 1 + .../kr/co/vividnext/sodalive/user/UserApi.kt | 5 ++ .../vividnext/sodalive/user/UserRepository.kt | 6 ++ .../sodalive/user/login/LoginActivity.kt | 68 +++++++++++++++++++ .../sodalive/user/login/LoginViewModel.kt | 41 +++++++++++ .../sodalive/user/login/SocialLoginRequest.kt | 4 +- 7 files changed, 131 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index db68472f..4db7bf1b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,6 +83,7 @@ android { buildConfigField 'String', 'KAKAO_APP_KEY', '"231cf78acfa8252fca38b9eedf87c5cb"' buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com"' buildConfigField 'String', 'APPSCHEME', '"voiceon"' + buildConfigField 'String', 'LINE_CHANNEL_ID', '"2008995539"' manifestPlaceholders = [ URISCHEME : "voiceon", APPLINK_HOST : "voiceon.onelink.me", @@ -109,6 +110,7 @@ android { buildConfigField 'String', 'KAKAO_APP_KEY', '"20cf19413d63bfdfd30e8e6dff933d33"' buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com"' buildConfigField 'String', 'APPSCHEME', '"voiceon-test"' + buildConfigField 'String', 'LINE_CHANNEL_ID', '"2008995582"' manifestPlaceholders = [ URISCHEME : "voiceon-test", APPLINK_HOST : "voiceon-test.onelink.me", @@ -234,6 +236,11 @@ dependencies { implementation 'com.github.orbitalsonic:Sonic-Water-Wave-Animation:2.0.1' + // Line + implementation("com.linecorp.linesdk:linesdk:5.6.1") { + exclude group: "org.jetbrains.kotlin", module: "kotlin-android-extensions-runtime" + } + // ----- Test dependencies ----- testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:5.20.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5a8cb71..f8f08c43 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,7 @@ android:supportsRtl="true" android:theme="@style/Theme.SodaLive" android:usesCleartextTraffic="true" + tools:replace="android:allowBackup" tools:targetApi="31"> > + @POST("/member/login/line") + fun loginLine( + @Body request: SocialLoginRequest + ): Single> + } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt index 2ae20ee9..36764799 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt @@ -141,4 +141,10 @@ class UserRepository(private val userApi: UserApi) { request = request, authHeader = token ) + + fun lineLogin( + request: SocialLoginRequest + ) = userApi.loginLine( + request = request + ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt index ad248c29..fbbc4fbb 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt @@ -8,6 +8,7 @@ import android.os.Looper import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.OptIn import androidx.credentials.Credential import androidx.credentials.CredentialManager @@ -24,6 +25,10 @@ import com.kakao.sdk.auth.model.OAuthToken import com.kakao.sdk.common.model.ClientError import com.kakao.sdk.common.model.ClientErrorCause import com.kakao.sdk.user.UserApiClient +import com.linecorp.linesdk.LineApiResponseCode +import com.linecorp.linesdk.Scope +import com.linecorp.linesdk.auth.LineAuthenticationParams +import com.linecorp.linesdk.auth.LineLoginApi import com.orhanobut.logger.Logger import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers @@ -38,6 +43,7 @@ import kr.co.vividnext.sodalive.main.MainActivity import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity import kr.co.vividnext.sodalive.user.signup.SignUpActivity import org.koin.android.ext.android.inject +import java.util.UUID @OptIn(UnstableApi::class) class LoginActivity : BaseActivity(ActivityLoginBinding::inflate) { @@ -49,6 +55,51 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i private val handler = Handler(Looper.getMainLooper()) + private var lineLoginNonce: String? = null + + private val lineLoginLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + loadingDialog.dismiss() + val data = result.data + if (data == null) { + showToast(getString(R.string.login_failed)) + return@registerForActivityResult + } + + val lineLoginResult = LineLoginApi.getLoginResultFromIntent(data) + when (lineLoginResult.responseCode) { + LineApiResponseCode.SUCCESS -> { + val identityToken = lineLoginResult.lineIdToken?.rawString + if (identityToken.isNullOrBlank()) { + showToast(getString(R.string.login_failed)) + return@registerForActivityResult + } + viewModel.lineLogin( + identityToken = identityToken, + nonce = lineLoginNonce + ) { + finishAffinity() + val nextIntent = Intent(applicationContext, MainActivity::class.java) + val extras = intent.getBundleExtra(Constants.EXTRA_DATA) + ?: if (intent.extras != null) { + intent.extras + } else { + null + } + if (extras != null) { + nextIntent.putExtra(Constants.EXTRA_DATA, extras) + } + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + startActivity(nextIntent) + } + } + + LineApiResponseCode.CANCEL -> Unit + else -> showToast(getString(R.string.login_failed)) + } + } + override fun onCreate(savedInstanceState: Bundle?) { imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager @@ -139,6 +190,7 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i } binding.ivLoginKakao.setOnClickListener { loginKakao() } + binding.ivLoginLine.setOnClickListener { loginLine() } } private fun handleSignIn(credential: Credential) { @@ -289,6 +341,22 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i } } + private fun loginLine() { + val lineChannelId = BuildConfig.LINE_CHANNEL_ID + if (lineChannelId.isBlank()) { + showToast(getString(R.string.login_failed)) + return + } + loadingDialog.show(screenWidth) + lineLoginNonce = UUID.randomUUID().toString() + val authParams = LineAuthenticationParams.Builder() + .scopes(listOf(Scope.OPENID_CONNECT)) + .nonce(lineLoginNonce) + .build() + val loginIntent = LineLoginApi.getLoginIntent(this, lineChannelId, authParams) + lineLoginLauncher.launch(loginIntent) + } + private fun startSignUp() { val nextIntent = Intent(applicationContext, SignUpActivity::class.java) val extras = intent.getBundleExtra(Constants.EXTRA_DATA) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginViewModel.kt index 62067695..16810d46 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginViewModel.kt @@ -104,6 +104,47 @@ class LoginViewModel(private val repository: UserRepository) : BaseViewModel() { ) } + fun lineLogin(identityToken: String, nonce: String?, onSuccess: () -> Unit) { + _isLoading.value = true + + compositeDisposable.add( + repository.lineLogin( + request = SocialLoginRequest( + pushToken = SharedPreferenceManager.pushToken, + marketingPid = SharedPreferenceManager.marketingPid, + identityToken = identityToken, + nonce = nonce + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + SharedPreferenceManager.token = it.data.token + SharedPreferenceManager.email = it.data.email + SharedPreferenceManager.userId = it.data.userId + SharedPreferenceManager.nickname = it.data.nickname + SharedPreferenceManager.profileImage = it.data.profileImage + onSuccess() + } else { + _toastLiveData.postValue( + it.message?.let { message -> + LoginUiMessage.Text(message) + } ?: LoginUiMessage.Resource(R.string.common_error_unknown) + ) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue(LoginUiMessage.Resource(R.string.common_error_unknown)) + } + ) + ) + } + fun login(onSuccess: (String?) -> Unit) { if (email.isBlank()) { _toastLiveData.postValue( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/login/SocialLoginRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/login/SocialLoginRequest.kt index 930e9991..f6d97a08 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/login/SocialLoginRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/login/SocialLoginRequest.kt @@ -7,5 +7,7 @@ import com.google.gson.annotations.SerializedName data class SocialLoginRequest( @SerializedName("container") val container: String = "aos", @SerializedName("pushToken") val pushToken: String?, - @SerializedName("marketingPid") val marketingPid: String + @SerializedName("marketingPid") val marketingPid: String, + @SerializedName("identityToken") val identityToken: String? = null, + @SerializedName("nonce") val nonce: String? = null )