2fa support
This commit is contained in:
@@ -7,14 +7,11 @@ class VKException(var url: String = "", var description: String = "", var error:
|
||||
IOException(description) {
|
||||
|
||||
var captcha: Pair<String, String>? = null
|
||||
var redirectUri: String? = null
|
||||
|
||||
var validationSid: String? = null
|
||||
|
||||
var json: JSONObject? = null
|
||||
|
||||
override fun toString(): String {
|
||||
return "url: $url;\n\nerror: $error; description: $description;"
|
||||
return "error: $error; description: $description;"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,16 +2,19 @@ package com.meloda.fast.api.network
|
||||
|
||||
object VKUrls {
|
||||
|
||||
const val OAUTH = "https://oauth.vk.com"
|
||||
const val API = "https://api.vk.com/method"
|
||||
|
||||
object Auth {
|
||||
const val directAuth = "https://oauth.vk.com/token"
|
||||
const val directAuth = "$OAUTH/token"
|
||||
const val sendSms = "$API/auth.validatePhone"
|
||||
}
|
||||
|
||||
object Conversations {
|
||||
const val get = "messages.getConversations"
|
||||
const val get = "$API/messages.getConversations"
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,4 +7,6 @@ class AuthDataSource @Inject constructor(
|
||||
private val repo: AuthRepo
|
||||
) : AuthRepo {
|
||||
override suspend fun auth(param: Map<String, String?>) = repo.auth(param)
|
||||
|
||||
override suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.meloda.fast.api.network.repo
|
||||
import com.meloda.fast.api.network.VKUrls
|
||||
import com.meloda.fast.api.network.response.ResponseAuthDirect
|
||||
import com.meloda.fast.api.network.Answer
|
||||
import com.meloda.fast.api.network.response.ResponseSendSms
|
||||
import retrofit2.http.*
|
||||
|
||||
interface AuthRepo {
|
||||
@@ -10,4 +11,7 @@ interface AuthRepo {
|
||||
@GET(VKUrls.Auth.directAuth)
|
||||
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
|
||||
|
||||
@GET(VKUrls.Auth.sendSms)
|
||||
suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
|
||||
|
||||
}
|
||||
@@ -11,3 +11,11 @@ data class ResponseAuthDirect(
|
||||
@SerializedName("trusted_hash") val twoFaHash: String? = null,
|
||||
@SerializedName("validation_sid") val validationSid: String? = null
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class ResponseSendSms(
|
||||
@SerializedName("sid") val validationSid: String?,
|
||||
@SerializedName("delay") val delay: Int?,
|
||||
@SerializedName("validation_type") val validationType: String?,
|
||||
@SerializedName("validation_resend") val validationResend: String?
|
||||
) : Parcelable
|
||||
@@ -7,7 +7,6 @@ import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
@@ -25,6 +24,7 @@ import com.meloda.fast.base.viewmodel.StartProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.VKEvent
|
||||
import com.meloda.fast.databinding.DialogCaptchaBinding
|
||||
import com.meloda.fast.databinding.DialogValidationBinding
|
||||
import com.meloda.fast.databinding.FragmentLoginBinding
|
||||
import com.meloda.fast.screens.main.MainFragment
|
||||
import com.meloda.fast.util.KeyboardUtils
|
||||
@@ -45,7 +45,9 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
||||
private var lastPassword: String = ""
|
||||
|
||||
private var errorTimer: Timer? = null
|
||||
|
||||
private var captchaInputLayout: TextInputLayout? = null
|
||||
private var validationInputLayout: TextInputLayout? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -59,11 +61,6 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
||||
setFragmentResultListener("validation") { _, bundle ->
|
||||
lifecycleScope.launch { viewModel.getValidatedData(bundle) }
|
||||
}
|
||||
|
||||
// showCaptchaDialog(
|
||||
// "https://www.vets4pets.com/syssiteassets/species/cat/kitten/tiny-kitten-in-field.jpg?width=1040",
|
||||
// ""
|
||||
// )
|
||||
}
|
||||
|
||||
override fun onEvent(event: VKEvent) {
|
||||
@@ -72,8 +69,12 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
||||
when (event) {
|
||||
is ShowError -> showErrorSnackbar(event.errorDescription)
|
||||
is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second)
|
||||
is ValidationRequired -> goToValidation()
|
||||
|
||||
CodeSent -> showValidationDialog()
|
||||
|
||||
is ValidationRequired -> showValidationRequired()
|
||||
is SuccessAuth -> goToMain(event.haveAuthorized)
|
||||
|
||||
StartProgressEvent -> onProgressStarted()
|
||||
StopProgressEvent -> onProgressStopped()
|
||||
}
|
||||
@@ -151,19 +152,19 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
||||
|
||||
KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
|
||||
|
||||
lifecycleScope.launch {
|
||||
|
||||
viewModel.login(
|
||||
login = loginString,
|
||||
password = passwordString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 7/27/2021 extract strings to resources
|
||||
private fun validateInputData(
|
||||
loginString: String?,
|
||||
passwordString: String?,
|
||||
captchaCode: String? = null
|
||||
captchaCode: String? = null,
|
||||
validationCode: String? = null
|
||||
): Boolean {
|
||||
var isValidated = true
|
||||
|
||||
@@ -182,6 +183,11 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
||||
setError("Input code", captchaInputLayout!!)
|
||||
}
|
||||
|
||||
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
|
||||
isValidated = false
|
||||
setError("Input code", validationInputLayout!!)
|
||||
}
|
||||
|
||||
return isValidated
|
||||
}
|
||||
|
||||
@@ -237,17 +243,52 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
||||
|
||||
dialog.dismiss()
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.login(
|
||||
login = lastLogin,
|
||||
password = lastPassword,
|
||||
captcha = captchaSid to captchaCode
|
||||
)
|
||||
}
|
||||
}
|
||||
captchaBinding.cancel.setOnClickListener { dialog.dismiss() }
|
||||
}
|
||||
|
||||
private fun showValidationDialog() {
|
||||
val validationBinding = DialogValidationBinding.inflate(layoutInflater, null, false)
|
||||
validationInputLayout = validationBinding.codeLayout
|
||||
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
.setView(validationBinding.root)
|
||||
.setCancelable(false)
|
||||
.setTitle(R.string.input_validation_code)
|
||||
|
||||
val dialog = builder.show()
|
||||
|
||||
validationBinding.ok.setOnClickListener {
|
||||
val validationCode = validationBinding.codeInput.text.toString().trim()
|
||||
|
||||
if (!validateInputData(
|
||||
loginString = null,
|
||||
passwordString = null,
|
||||
validationCode = validationCode
|
||||
)
|
||||
) return@setOnClickListener
|
||||
|
||||
dialog.dismiss()
|
||||
|
||||
viewModel.login(
|
||||
login = lastLogin,
|
||||
password = lastPassword,
|
||||
twoFaCode = validationCode
|
||||
)
|
||||
}
|
||||
validationBinding.cancel.setOnClickListener { dialog.dismiss() }
|
||||
}
|
||||
|
||||
// TODO: 8/31/2021 show snackbar
|
||||
private fun showValidationRequired() {
|
||||
|
||||
}
|
||||
|
||||
private fun showErrorSnackbar(errorDescription: String) {
|
||||
val snackbar = Snackbar.make(
|
||||
requireView(),
|
||||
@@ -259,19 +300,10 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
private fun goToValidation() {
|
||||
// findNavController().navigate(
|
||||
// R.id.toValidation,
|
||||
// bundleOf("redirectUrl" to redirectUrl)
|
||||
// )
|
||||
}
|
||||
|
||||
private fun goToMain(haveAuthorized: Boolean) {
|
||||
lifecycleScope.launch {
|
||||
private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch {
|
||||
if (haveAuthorized) delay(500)
|
||||
|
||||
findNavController().navigate(R.id.toMain)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,13 +26,12 @@ class LoginViewModel @Inject constructor(
|
||||
private val repo: AuthRepo
|
||||
) : BaseViewModel() {
|
||||
|
||||
suspend fun login(
|
||||
fun login(
|
||||
login: String,
|
||||
password: String,
|
||||
twoFa: Boolean = false,
|
||||
twoFaCode: String? = null,
|
||||
captcha: Pair<String, String>? = null
|
||||
) {
|
||||
) = viewModelScope.launch {
|
||||
makeJob(
|
||||
{
|
||||
repo.auth(
|
||||
@@ -43,7 +42,7 @@ class LoginViewModel @Inject constructor(
|
||||
username = login,
|
||||
password = password,
|
||||
scope = VKAuth.scope,
|
||||
twoFaForceSms = twoFa,
|
||||
twoFaForceSms = true,
|
||||
twoFaCode = twoFaCode,
|
||||
captchaSid = captcha?.first,
|
||||
captchaKey = captcha?.second
|
||||
@@ -63,13 +62,18 @@ class LoginViewModel @Inject constructor(
|
||||
},
|
||||
onError = {
|
||||
checkErrors(it)
|
||||
|
||||
if (it !is VKException) return@makeJob
|
||||
|
||||
if (VKUtil.isValidationRequired(it)) {
|
||||
sendEvent(ValidationRequired(validationSid = it.validationSid))
|
||||
} else if (VKUtil.isCaptchaRequired(it) && it.captcha != null) {
|
||||
sendEvent(CaptchaRequired(it.captcha!!.first to it.captcha!!.second))
|
||||
it.validationSid?.let { sid ->
|
||||
sendEvent(ValidationRequired(validationSid = sid))
|
||||
|
||||
sendSms(sid)
|
||||
}
|
||||
} else if (VKUtil.isCaptchaRequired(it)) {
|
||||
it.captcha?.let { captcha ->
|
||||
sendEvent(CaptchaRequired(captcha.first to captcha.second))
|
||||
}
|
||||
}
|
||||
},
|
||||
onStart = { sendEvent(StartProgressEvent) },
|
||||
@@ -77,49 +81,12 @@ class LoginViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MoveVariableDeclarationIntoWhen")
|
||||
private fun checkResponse(response: JSONObject) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
if (response.has("error")) {
|
||||
sendEvent(StopProgressEvent)
|
||||
|
||||
val errorString = response.optString("error")
|
||||
val errorDescription = response.optString("error_description")
|
||||
|
||||
// TODO: 7/27/2021 use this with localized resources
|
||||
// val errorType = response.optString("error_type")
|
||||
|
||||
when (errorString) {
|
||||
"need_validation" -> {
|
||||
val redirectUrl = response.optString("redirect_uri")
|
||||
|
||||
tasksEventChannel.send(ValidationRequired(redirectUrl))
|
||||
}
|
||||
"need_captcha" -> {
|
||||
val captchaImage = response.optString("captcha_img")
|
||||
val captchaSid = response.optString("captcha_sid")
|
||||
|
||||
Log.d("CAPTCHA", "captchaImage: $captchaImage")
|
||||
|
||||
tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid))
|
||||
}
|
||||
else -> {
|
||||
tasksEventChannel.send(ShowError(errorDescription))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delay(1500)
|
||||
sendEvent(StopProgressEvent)
|
||||
|
||||
val userId = response.optInt("user_id", -1)
|
||||
val accessToken = response.optString("access_token")
|
||||
|
||||
UserConfig.accessToken = accessToken
|
||||
UserConfig.userId = userId
|
||||
|
||||
tasksEventChannel.send(SuccessAuth())
|
||||
}
|
||||
}
|
||||
fun sendSms(validationSid: String) = viewModelScope.launch {
|
||||
makeJob({ repo.sendSms(validationSid) },
|
||||
onAnswer = { sendEvent(CodeSent) },
|
||||
onError = {},
|
||||
onStart = {},
|
||||
onEnd = {})
|
||||
}
|
||||
|
||||
suspend fun getValidatedData(bundle: Bundle) {
|
||||
@@ -135,13 +102,10 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
data class ShowError(val errorDescription: String) : VKEvent()
|
||||
data class ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent()
|
||||
|
||||
data class ValidationRequired(
|
||||
val redirectUrl: String? = null,
|
||||
val validationSid: String? = null
|
||||
) : VKEvent()
|
||||
|
||||
data class ValidationRequired(val validationSid: String) : VKEvent()
|
||||
data class CaptchaRequired(val captcha: Pair<String, String>) : VKEvent()
|
||||
|
||||
object CodeSent : VKEvent()
|
||||
|
||||
data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent()
|
||||
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/activity_horizontal_margin">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/codeContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/codeImage"
|
||||
style="@style/AppTheme.Login.EditText.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_security"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/codeLayout"
|
||||
style="@style/Widget.TextInputLayout.NoError.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/codeInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/code_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="number"
|
||||
android:lines="1"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||
android:layout_weight="1"
|
||||
android:text="@android:string/cancel"
|
||||
android:textColor="?colorAction"
|
||||
app:elevation="0dp"
|
||||
app:rippleColor="?colorActionRipple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok"
|
||||
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="?colorAction"
|
||||
android:text="@android:string/ok"
|
||||
android:textColor="?colorActionContentPrimary"
|
||||
app:elevation="0dp"
|
||||
app:rippleColor="?colorActionRipple" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -156,5 +156,7 @@
|
||||
<string name="login_hint">Login</string>
|
||||
|
||||
<string name="conversations">Conversations</string>
|
||||
<string name="code_hint">Code</string>
|
||||
<string name="input_validation_code">Input code from sms</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user