LINE 로그인 연동 추가

LINE 로그인 요청에 id token과 nonce를 전달함
This commit is contained in:
2026-01-28 17:52:02 +09:00
parent 6031638260
commit 4ddee2b1c1
7 changed files with 131 additions and 1 deletions

View File

@@ -83,6 +83,7 @@ android {
buildConfigField 'String', 'KAKAO_APP_KEY', '"231cf78acfa8252fca38b9eedf87c5cb"' buildConfigField 'String', 'KAKAO_APP_KEY', '"231cf78acfa8252fca38b9eedf87c5cb"'
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com"' buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com"'
buildConfigField 'String', 'APPSCHEME', '"voiceon"' buildConfigField 'String', 'APPSCHEME', '"voiceon"'
buildConfigField 'String', 'LINE_CHANNEL_ID', '"2008995539"'
manifestPlaceholders = [ manifestPlaceholders = [
URISCHEME : "voiceon", URISCHEME : "voiceon",
APPLINK_HOST : "voiceon.onelink.me", APPLINK_HOST : "voiceon.onelink.me",
@@ -109,6 +110,7 @@ android {
buildConfigField 'String', 'KAKAO_APP_KEY', '"20cf19413d63bfdfd30e8e6dff933d33"' buildConfigField 'String', 'KAKAO_APP_KEY', '"20cf19413d63bfdfd30e8e6dff933d33"'
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com"' buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com"'
buildConfigField 'String', 'APPSCHEME', '"voiceon-test"' buildConfigField 'String', 'APPSCHEME', '"voiceon-test"'
buildConfigField 'String', 'LINE_CHANNEL_ID', '"2008995582"'
manifestPlaceholders = [ manifestPlaceholders = [
URISCHEME : "voiceon-test", URISCHEME : "voiceon-test",
APPLINK_HOST : "voiceon-test.onelink.me", APPLINK_HOST : "voiceon-test.onelink.me",
@@ -234,6 +236,11 @@ dependencies {
implementation 'com.github.orbitalsonic:Sonic-Water-Wave-Animation:2.0.1' 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 ----- // ----- Test dependencies -----
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.20.0' testImplementation 'org.mockito:mockito-core:5.20.0'

View File

@@ -63,6 +63,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SodaLive" android:theme="@style/Theme.SodaLive"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:replace="android:allowBackup"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".main.DeepLinkActivity" android:name=".main.DeepLinkActivity"

View File

@@ -176,4 +176,9 @@ interface UserApi {
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<LoginResponse>> ): Single<ApiResponse<LoginResponse>>
@POST("/member/login/line")
fun loginLine(
@Body request: SocialLoginRequest
): Single<ApiResponse<LoginResponse>>
} }

View File

@@ -141,4 +141,10 @@ class UserRepository(private val userApi: UserApi) {
request = request, request = request,
authHeader = token authHeader = token
) )
fun lineLogin(
request: SocialLoginRequest
) = userApi.loginLine(
request = request
)
} }

View File

@@ -8,6 +8,7 @@ import android.os.Looper
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.credentials.Credential import androidx.credentials.Credential
import androidx.credentials.CredentialManager 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.ClientError
import com.kakao.sdk.common.model.ClientErrorCause import com.kakao.sdk.common.model.ClientErrorCause
import com.kakao.sdk.user.UserApiClient 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 com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers 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.find_password.FindPasswordActivity
import kr.co.vividnext.sodalive.user.signup.SignUpActivity import kr.co.vividnext.sodalive.user.signup.SignUpActivity
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.util.UUID
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::inflate) { class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::inflate) {
@@ -49,6 +55,51 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
private val handler = Handler(Looper.getMainLooper()) 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?) { override fun onCreate(savedInstanceState: Bundle?) {
imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
@@ -139,6 +190,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(ActivityLoginBinding::i
} }
binding.ivLoginKakao.setOnClickListener { loginKakao() } binding.ivLoginKakao.setOnClickListener { loginKakao() }
binding.ivLoginLine.setOnClickListener { loginLine() }
} }
private fun handleSignIn(credential: Credential) { private fun handleSignIn(credential: Credential) {
@@ -289,6 +341,22 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(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() { private fun startSignUp() {
val nextIntent = Intent(applicationContext, SignUpActivity::class.java) val nextIntent = Intent(applicationContext, SignUpActivity::class.java)
val extras = intent.getBundleExtra(Constants.EXTRA_DATA) val extras = intent.getBundleExtra(Constants.EXTRA_DATA)

View File

@@ -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) { fun login(onSuccess: (String?) -> Unit) {
if (email.isBlank()) { if (email.isBlank()) {
_toastLiveData.postValue( _toastLiveData.postValue(

View File

@@ -7,5 +7,7 @@ import com.google.gson.annotations.SerializedName
data class SocialLoginRequest( data class SocialLoginRequest(
@SerializedName("container") val container: String = "aos", @SerializedName("container") val container: String = "aos",
@SerializedName("pushToken") val pushToken: String?, @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
) )