forked from melod1n/fast-messenger
2fa support
This commit is contained in:
@@ -7,14 +7,11 @@ class VKException(var url: String = "", var description: String = "", var error:
|
|||||||
IOException(description) {
|
IOException(description) {
|
||||||
|
|
||||||
var captcha: Pair<String, String>? = null
|
var captcha: Pair<String, String>? = null
|
||||||
var redirectUri: String? = null
|
|
||||||
|
|
||||||
var validationSid: String? = null
|
var validationSid: String? = null
|
||||||
|
|
||||||
var json: JSONObject? = null
|
var json: JSONObject? = null
|
||||||
|
|
||||||
override fun toString(): String {
|
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 {
|
object VKUrls {
|
||||||
|
|
||||||
|
const val OAUTH = "https://oauth.vk.com"
|
||||||
|
const val API = "https://api.vk.com/method"
|
||||||
|
|
||||||
object Auth {
|
object Auth {
|
||||||
const val directAuth = "https://oauth.vk.com/token"
|
const val directAuth = "$OAUTH/token"
|
||||||
|
const val sendSms = "$API/auth.validatePhone"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Conversations {
|
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
|
private val repo: AuthRepo
|
||||||
) : AuthRepo {
|
) : AuthRepo {
|
||||||
override suspend fun auth(param: Map<String, String?>) = repo.auth(param)
|
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.VKUrls
|
||||||
import com.meloda.fast.api.network.response.ResponseAuthDirect
|
import com.meloda.fast.api.network.response.ResponseAuthDirect
|
||||||
import com.meloda.fast.api.network.Answer
|
import com.meloda.fast.api.network.Answer
|
||||||
|
import com.meloda.fast.api.network.response.ResponseSendSms
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
|
|
||||||
interface AuthRepo {
|
interface AuthRepo {
|
||||||
@@ -10,4 +11,7 @@ interface AuthRepo {
|
|||||||
@GET(VKUrls.Auth.directAuth)
|
@GET(VKUrls.Auth.directAuth)
|
||||||
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
|
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
|
||||||
|
|
||||||
|
@GET(VKUrls.Auth.sendSms)
|
||||||
|
suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,4 +10,12 @@ data class ResponseAuthDirect(
|
|||||||
@SerializedName("user_id") val userId: Int? = null,
|
@SerializedName("user_id") val userId: Int? = null,
|
||||||
@SerializedName("trusted_hash") val twoFaHash: String? = null,
|
@SerializedName("trusted_hash") val twoFaHash: String? = null,
|
||||||
@SerializedName("validation_sid") val validationSid: 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
|
) : Parcelable
|
||||||
@@ -7,7 +7,6 @@ import android.view.View
|
|||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.viewbinding.library.fragment.viewBinding
|
import android.viewbinding.library.fragment.viewBinding
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.setFragmentResultListener
|
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.StopProgressEvent
|
||||||
import com.meloda.fast.base.viewmodel.VKEvent
|
import com.meloda.fast.base.viewmodel.VKEvent
|
||||||
import com.meloda.fast.databinding.DialogCaptchaBinding
|
import com.meloda.fast.databinding.DialogCaptchaBinding
|
||||||
|
import com.meloda.fast.databinding.DialogValidationBinding
|
||||||
import com.meloda.fast.databinding.FragmentLoginBinding
|
import com.meloda.fast.databinding.FragmentLoginBinding
|
||||||
import com.meloda.fast.screens.main.MainFragment
|
import com.meloda.fast.screens.main.MainFragment
|
||||||
import com.meloda.fast.util.KeyboardUtils
|
import com.meloda.fast.util.KeyboardUtils
|
||||||
@@ -45,7 +45,9 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
|||||||
private var lastPassword: String = ""
|
private var lastPassword: String = ""
|
||||||
|
|
||||||
private var errorTimer: Timer? = null
|
private var errorTimer: Timer? = null
|
||||||
|
|
||||||
private var captchaInputLayout: TextInputLayout? = null
|
private var captchaInputLayout: TextInputLayout? = null
|
||||||
|
private var validationInputLayout: TextInputLayout? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@@ -59,11 +61,6 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
|||||||
setFragmentResultListener("validation") { _, bundle ->
|
setFragmentResultListener("validation") { _, bundle ->
|
||||||
lifecycleScope.launch { viewModel.getValidatedData(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) {
|
override fun onEvent(event: VKEvent) {
|
||||||
@@ -72,8 +69,12 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
|||||||
when (event) {
|
when (event) {
|
||||||
is ShowError -> showErrorSnackbar(event.errorDescription)
|
is ShowError -> showErrorSnackbar(event.errorDescription)
|
||||||
is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second)
|
is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second)
|
||||||
is ValidationRequired -> goToValidation()
|
|
||||||
|
CodeSent -> showValidationDialog()
|
||||||
|
|
||||||
|
is ValidationRequired -> showValidationRequired()
|
||||||
is SuccessAuth -> goToMain(event.haveAuthorized)
|
is SuccessAuth -> goToMain(event.haveAuthorized)
|
||||||
|
|
||||||
StartProgressEvent -> onProgressStarted()
|
StartProgressEvent -> onProgressStarted()
|
||||||
StopProgressEvent -> onProgressStopped()
|
StopProgressEvent -> onProgressStopped()
|
||||||
}
|
}
|
||||||
@@ -151,19 +152,19 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
|||||||
|
|
||||||
KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
|
KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
viewModel.login(
|
viewModel.login(
|
||||||
login = loginString,
|
login = loginString,
|
||||||
password = passwordString
|
password = passwordString
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 7/27/2021 extract strings to resources
|
// TODO: 7/27/2021 extract strings to resources
|
||||||
private fun validateInputData(
|
private fun validateInputData(
|
||||||
loginString: String?,
|
loginString: String?,
|
||||||
passwordString: String?,
|
passwordString: String?,
|
||||||
captchaCode: String? = null
|
captchaCode: String? = null,
|
||||||
|
validationCode: String? = null
|
||||||
): Boolean {
|
): Boolean {
|
||||||
var isValidated = true
|
var isValidated = true
|
||||||
|
|
||||||
@@ -182,6 +183,11 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
|||||||
setError("Input code", captchaInputLayout!!)
|
setError("Input code", captchaInputLayout!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
|
||||||
|
isValidated = false
|
||||||
|
setError("Input code", validationInputLayout!!)
|
||||||
|
}
|
||||||
|
|
||||||
return isValidated
|
return isValidated
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,17 +243,52 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
|||||||
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
viewModel.login(
|
||||||
viewModel.login(
|
login = lastLogin,
|
||||||
login = lastLogin,
|
password = lastPassword,
|
||||||
password = lastPassword,
|
captcha = captchaSid to captchaCode
|
||||||
captcha = captchaSid to captchaCode
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
captchaBinding.cancel.setOnClickListener { dialog.dismiss() }
|
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) {
|
private fun showErrorSnackbar(errorDescription: String) {
|
||||||
val snackbar = Snackbar.make(
|
val snackbar = Snackbar.make(
|
||||||
requireView(),
|
requireView(),
|
||||||
@@ -259,19 +300,10 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
|
|||||||
snackbar.show()
|
snackbar.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun goToValidation() {
|
private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch {
|
||||||
// findNavController().navigate(
|
if (haveAuthorized) delay(500)
|
||||||
// R.id.toValidation,
|
|
||||||
// bundleOf("redirectUrl" to redirectUrl)
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun goToMain(haveAuthorized: Boolean) {
|
findNavController().navigate(R.id.toMain)
|
||||||
lifecycleScope.launch {
|
|
||||||
if (haveAuthorized) delay(500)
|
|
||||||
|
|
||||||
findNavController().navigate(R.id.toMain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -26,13 +26,12 @@ class LoginViewModel @Inject constructor(
|
|||||||
private val repo: AuthRepo
|
private val repo: AuthRepo
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
suspend fun login(
|
fun login(
|
||||||
login: String,
|
login: String,
|
||||||
password: String,
|
password: String,
|
||||||
twoFa: Boolean = false,
|
|
||||||
twoFaCode: String? = null,
|
twoFaCode: String? = null,
|
||||||
captcha: Pair<String, String>? = null
|
captcha: Pair<String, String>? = null
|
||||||
) {
|
) = viewModelScope.launch {
|
||||||
makeJob(
|
makeJob(
|
||||||
{
|
{
|
||||||
repo.auth(
|
repo.auth(
|
||||||
@@ -43,7 +42,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
username = login,
|
username = login,
|
||||||
password = password,
|
password = password,
|
||||||
scope = VKAuth.scope,
|
scope = VKAuth.scope,
|
||||||
twoFaForceSms = twoFa,
|
twoFaForceSms = true,
|
||||||
twoFaCode = twoFaCode,
|
twoFaCode = twoFaCode,
|
||||||
captchaSid = captcha?.first,
|
captchaSid = captcha?.first,
|
||||||
captchaKey = captcha?.second
|
captchaKey = captcha?.second
|
||||||
@@ -63,13 +62,18 @@ class LoginViewModel @Inject constructor(
|
|||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
checkErrors(it)
|
checkErrors(it)
|
||||||
|
|
||||||
if (it !is VKException) return@makeJob
|
if (it !is VKException) return@makeJob
|
||||||
|
|
||||||
if (VKUtil.isValidationRequired(it)) {
|
if (VKUtil.isValidationRequired(it)) {
|
||||||
sendEvent(ValidationRequired(validationSid = it.validationSid))
|
it.validationSid?.let { sid ->
|
||||||
} else if (VKUtil.isCaptchaRequired(it) && it.captcha != null) {
|
sendEvent(ValidationRequired(validationSid = sid))
|
||||||
sendEvent(CaptchaRequired(it.captcha!!.first to it.captcha!!.second))
|
|
||||||
|
sendSms(sid)
|
||||||
|
}
|
||||||
|
} else if (VKUtil.isCaptchaRequired(it)) {
|
||||||
|
it.captcha?.let { captcha ->
|
||||||
|
sendEvent(CaptchaRequired(captcha.first to captcha.second))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStart = { sendEvent(StartProgressEvent) },
|
onStart = { sendEvent(StartProgressEvent) },
|
||||||
@@ -77,49 +81,12 @@ class LoginViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MoveVariableDeclarationIntoWhen")
|
fun sendSms(validationSid: String) = viewModelScope.launch {
|
||||||
private fun checkResponse(response: JSONObject) {
|
makeJob({ repo.sendSms(validationSid) },
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
onAnswer = { sendEvent(CodeSent) },
|
||||||
if (response.has("error")) {
|
onError = {},
|
||||||
sendEvent(StopProgressEvent)
|
onStart = {},
|
||||||
|
onEnd = {})
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getValidatedData(bundle: Bundle) {
|
suspend fun getValidatedData(bundle: Bundle) {
|
||||||
@@ -135,13 +102,10 @@ class LoginViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ShowError(val errorDescription: String) : VKEvent()
|
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()
|
data class CaptchaRequired(val captcha: Pair<String, String>) : VKEvent()
|
||||||
|
|
||||||
|
object CodeSent : VKEvent()
|
||||||
|
|
||||||
data class SuccessAuth(val haveAuthorized: Boolean = true) : 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="login_hint">Login</string>
|
||||||
|
|
||||||
<string name="conversations">Conversations</string>
|
<string name="conversations">Conversations</string>
|
||||||
|
<string name="code_hint">Code</string>
|
||||||
|
<string name="input_validation_code">Input code from sms</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user