Add account recover (#635)

This commit is contained in:
doteq 2020-02-27 00:10:11 +01:00 committed by GitHub
parent 96c1bb4c69
commit 18d6ec6961
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 867 additions and 35 deletions

View File

@ -1,9 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>

View File

@ -122,7 +122,7 @@ configurations.all {
}
dependencies {
implementation "io.github.wulkanowy:sdk:7e89883"
implementation "io.github.wulkanowy:sdk:6789442"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0"
@ -173,7 +173,7 @@ dependencies {
implementation "com.jakewharton.threetenabp:threetenabp:1.2.2"
implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation "fr.bipi.treessence:treessence:0.3.0"
implementation "fr.bipi.treessence:treessence:0.3.2"
implementation "com.mikepenz:aboutlibraries-core:7.1.0"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'

View File

@ -0,0 +1,19 @@
package io.github.wulkanowy.data.repositories.recover
import io.github.wulkanowy.sdk.Sdk
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecoverRemote @Inject constructor(private val sdk: Sdk) {
fun getReCaptchaSiteKey(host: String, symbol: String): Single<Pair<String, String>> {
return sdk.getPasswordResetCaptchaCode(host, symbol)
}
fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): Single<String> {
return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
}
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.data.repositories.recover
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecoverRepository @Inject constructor(private val settings: InternetObservingSettings, private val remote: RecoverRemote) {
fun getReCaptchaSiteKey(host: String, symbol: String): Single<Pair<String, String>> {
return ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getReCaptchaSiteKey(host, symbol)
else Single.error(UnknownHostException())
}
}
fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): Single<String> {
return ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.sendRecoverRequest(url, symbol, email, reCaptchaResponse)
else Single.error(UnknownHostException())
}
}
}

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
import io.github.wulkanowy.utils.setOnSelectPageListener
@ -52,7 +53,8 @@ class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {
LoginFormFragment.newInstance(),
LoginSymbolFragment.newInstance(),
LoginStudentSelectFragment.newInstance(),
LoginAdvancedFragment.newInstance()
LoginAdvancedFragment.newInstance(),
LoginRecoverFragment.newInstance()
))
}
@ -99,4 +101,8 @@ class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {
fun onAdvancedLoginClick() {
presenter.onAdvancedLoginClick()
}
fun onRecoverClick() {
presenter.onRecoverClick()
}
}

View File

@ -43,5 +43,8 @@ class LoginErrorHandler @Inject constructor(
super.clear()
onBadCredentials = {}
onStudentDuplicate = {}
onInvalidToken = {}
onInvalidPin = {}
onInvalidSymbol = {}
}
}

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.di.scopes.PerFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
@ -37,4 +38,8 @@ internal abstract class LoginModule {
@PerFragment
@ContributesAndroidInjector
abstract fun bindLoginSelectStudentFragment(): LoginStudentSelectFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindLoginRecoverFragment(): LoginRecoverFragment
}

View File

@ -49,11 +49,15 @@ class LoginPresenter @Inject constructor(
view?.switchView(3)
}
fun onRecoverClick() {
view?.switchView(4)
}
fun onViewSelected(index: Int) {
view?.apply {
when (index) {
0 -> showActionBar(false)
1, 2 -> showActionBar(true)
1, 2, 3, 4 -> showActionBar(true)
}
}
}
@ -62,7 +66,7 @@ class LoginPresenter @Inject constructor(
Timber.i("Back pressed in login view")
view?.apply {
when (currentViewIndex) {
1, 2, 3 -> switchView(0)
1, 2, 3, 4 -> switchView(0)
else -> default()
}
}

View File

@ -3,6 +3,8 @@ package io.github.wulkanowy.ui.modules.login.advanced
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
@ -98,8 +100,9 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values)))
with(loginFormHost) {
setText(hostKeys.getOrElse(0) { "" })
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() }
}
}
@ -212,30 +215,30 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
}
override fun showOnlyHybridModeInputs() {
loginFormUsernameLayout.visibility = View.VISIBLE
loginFormPassLayout.visibility = View.VISIBLE
loginFormHostLayout.visibility = View.VISIBLE
loginFormPinLayout.visibility = View.GONE
loginFormSymbolLayout.visibility = View.VISIBLE
loginFormTokenLayout.visibility = View.GONE
loginFormUsernameLayout.visibility = VISIBLE
loginFormPassLayout.visibility = VISIBLE
loginFormHostLayout.visibility = VISIBLE
loginFormPinLayout.visibility = GONE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = GONE
}
override fun showOnlyScrapperModeInputs() {
loginFormUsernameLayout.visibility = View.VISIBLE
loginFormPassLayout.visibility = View.VISIBLE
loginFormHostLayout.visibility = View.VISIBLE
loginFormPinLayout.visibility = View.GONE
loginFormSymbolLayout.visibility = View.VISIBLE
loginFormTokenLayout.visibility = View.GONE
loginFormUsernameLayout.visibility = VISIBLE
loginFormPassLayout.visibility = VISIBLE
loginFormHostLayout.visibility = VISIBLE
loginFormPinLayout.visibility = GONE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = GONE
}
override fun showOnlyMobileApiModeInputs() {
loginFormUsernameLayout.visibility = View.GONE
loginFormPassLayout.visibility = View.GONE
loginFormHostLayout.visibility = View.GONE
loginFormPinLayout.visibility = View.VISIBLE
loginFormSymbolLayout.visibility = View.VISIBLE
loginFormTokenLayout.visibility = View.VISIBLE
loginFormUsernameLayout.visibility = GONE
loginFormPassLayout.visibility = GONE
loginFormHostLayout.visibility = GONE
loginFormPinLayout.visibility = VISIBLE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = VISIBLE
}
override fun showSoftKeyboard() {
@ -247,11 +250,11 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
}
override fun showProgress(show: Boolean) {
loginFormProgress.visibility = if (show) View.VISIBLE else View.GONE
loginFormProgress.visibility = if (show) VISIBLE else GONE
}
override fun showContent(show: Boolean) {
loginFormContainer.visibility = if (show) View.VISIBLE else View.GONE
loginFormContainer.visibility = if (show) VISIBLE else GONE
}
override fun notifyParentAccountLogged(students: List<Student>) {

View File

@ -74,14 +74,15 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() }
loginFormFaq.setOnClickListener { presenter.onFaqClick() }
loginFormContactEmail.setOnClickListener { presenter.onEmailClick() }
loginFormRecoverLink.setOnClickListener { presenter.onRecoverClick() }
loginFormPass.setOnEditorActionListener { _, id, _ ->
if (id == IME_ACTION_DONE || id == IME_NULL) loginFormSignIn.callOnClick() else false
}
with(loginFormHost) {
setText(hostKeys.getOrElse(0) { "" })
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() }
}
}
@ -167,6 +168,10 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
(activity as? LoginActivity)?.onAdvancedLoginClick()
}
override fun onRecoverClick() {
(activity as? LoginActivity)?.onRecoverClick()
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()

View File

@ -108,6 +108,10 @@ class LoginFormPresenter @Inject constructor(
view?.openEmail()
}
fun onRecoverClick() {
view?.onRecoverClick()
}
private fun validateCredentials(login: String, password: String): Boolean {
var isCorrect = true

View File

@ -54,4 +54,6 @@ interface LoginFormView : BaseView {
fun openEmail()
fun openAdvancedLogin()
fun onRecoverClick()
}

View File

@ -0,0 +1,209 @@
package io.github.wulkanowy.ui.modules.login.recover
import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ArrayAdapter
import androidx.core.widget.doOnTextChanged
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.form.LoginSymbolAdapter
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.showSoftInput
import kotlinx.android.synthetic.main.fragment_login_recover.*
import javax.inject.Inject
class LoginRecoverFragment : BaseFragment(), LoginRecoverView {
@Inject
lateinit var presenter: LoginRecoverPresenter
companion object {
fun newInstance() = LoginRecoverFragment()
}
private lateinit var hostKeys: Array<String>
private lateinit var hostValues: Array<String>
override val recoverHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(loginRecoverHost.text.toString())).orEmpty()
override val recoverNameValue: String
get() = loginRecoverName.text.toString().trim()
override val recoverSymbolValue: String
get() = loginRecoverSymbol.text.toString().trim()
override val emailHintString: String
get() = getString(R.string.login_email_hint)
override val loginPeselEmailHintString: String
get() = getString(R.string.login_login_pesel_email_hint)
override val invalidEmailString: String
get() = getString(R.string.login_invalid_email)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_login_recover, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
loginRecoverWebView.setBackgroundColor(Color.TRANSPARENT)
hostKeys = resources.getStringArray(R.array.hosts_keys)
hostValues = resources.getStringArray(R.array.hosts_values)
loginRecoverName.doOnTextChanged { _, _, _, _ -> presenter.onNameTextChanged() }
loginRecoverHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() }
loginRecoverButton.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() }
loginRecoverLogin.setOnClickListener { (activity as LoginActivity).switchView(0) }
loginRecoverSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values)))
with(loginRecoverHost) {
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setOnClickListener { if (loginRecoverFormContainer.visibility == GONE) dismissDropDown() }
}
}
override fun setDefaultCredentials(username: String, symbol: String) {
loginRecoverName.setText(username)
loginRecoverSymbol.setText(symbol)
}
override fun setErrorNameRequired() {
with(loginRecoverNameLayout) {
requestFocus()
error = getString(R.string.login_field_required)
}
}
override fun setUsernameHint(hint: String) {
loginRecoverNameLayout.hint = hint
}
override fun setUsernameError(message: String) {
with(loginRecoverNameLayout) {
requestFocus()
error = message
}
}
override fun clearUsernameError() {
loginRecoverNameLayout.error = null
}
override fun showSymbol(show: Boolean) {
loginRecoverSymbolLayout.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
loginRecoverProgress.visibility = if (show) VISIBLE else GONE
}
override fun showRecoverForm(show: Boolean) {
loginRecoverFormContainer.visibility = if (show) VISIBLE else GONE
}
override fun showCaptcha(show: Boolean) {
loginRecoverCaptchaContainer.visibility = if (show) VISIBLE else GONE
}
override fun showErrorView(show: Boolean) {
loginRecoverError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
loginRecoverErrorMessage.text = message
}
override fun showSuccessView(show: Boolean) {
loginRecoverSuccess.visibility = if (show) VISIBLE else GONE
}
override fun setSuccessTitle(title: String) {
loginRecoverSuccessTitle.text = title
}
override fun setSuccessMessage(message: String) {
loginRecoverSuccessMessage.text = message
}
override fun showSoftKeyboard() {
activity?.showSoftInput()
}
override fun hideSoftKeyboard() {
activity?.hideSoftInput()
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
override fun loadReCaptcha(siteKey: String, url: String) {
val html = """
<div style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);" id="recaptcha"></div>
<script src="https://www.google.com/recaptcha/api.js?onload=cl&render=explicit&hl=pl" async defer></script>
<script>var cl=()=>grecaptcha.render("recaptcha",{
sitekey:'$siteKey',
callback:e =>Android.captchaCallback(e)})</script>
""".trimIndent()
with(loginRecoverWebView) {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
private var recoverWebViewSuccess: Boolean = true
override fun onPageFinished(view: WebView?, url: String?) {
if (recoverWebViewSuccess) {
showCaptcha(true)
showProgress(false)
} else {
showProgress(false)
showErrorView(true)
}
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
recoverWebViewSuccess = false
}
}
loadDataWithBaseURL(url, html, "text/html", "UTF-8", null)
addJavascriptInterface(object {
@JavascriptInterface
fun captchaCallback(reCaptchaResponse: String) {
activity?.runOnUiThread {
presenter.onReCaptchaVerified(reCaptchaResponse)
}
}
}, "Android")
}
}
override fun onResume() {
super.onResume()
presenter.updateFields()
}
override fun onDestroyView() {
super.onDestroyView()
loginRecoverWebView.destroy()
presenter.onDetachView()
}
}

View File

@ -0,0 +1,157 @@
package io.github.wulkanowy.ui.modules.login.recover
import io.github.wulkanowy.data.repositories.recover.RecoverRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.ifNullOrBlank
import timber.log.Timber
import javax.inject.Inject
class LoginRecoverPresenter @Inject constructor(
schedulers: SchedulersProvider,
studentRepository: StudentRepository,
private val loginErrorHandler: RecoverErrorHandler,
private val analytics: FirebaseAnalyticsHelper,
private val recoverRepository: RecoverRepository
) : BasePresenter<LoginRecoverView>(loginErrorHandler, studentRepository, schedulers) {
private lateinit var lastError: Throwable
override fun onAttachView(view: LoginRecoverView) {
super.onAttachView(view)
view.initView()
with(loginErrorHandler) {
showErrorMessage = ::showErrorMessage
onInvalidUsername = ::onInvalidUsername
onInvalidCaptcha = ::onInvalidCaptcha
}
}
fun onNameTextChanged() {
view?.clearUsernameError()
}
fun onHostSelected() {
view?.run {
if ("fakelog" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf", "Default")
clearUsernameError()
updateFields()
}
}
fun updateFields() {
view?.run {
if ("fakelog" in recoverHostValue || "vulcan" in recoverHostValue) {
showSymbol(true)
setUsernameHint(emailHintString)
} else {
showSymbol(false)
setUsernameHint(loginPeselEmailHintString)
}
}
}
fun onRecoverClick() {
val username = view?.recoverNameValue.orEmpty()
val host = view?.recoverHostValue.orEmpty()
val symbol = view?.recoverSymbolValue.ifNullOrBlank { "Default" }
if (username.isEmpty()) {
view?.setErrorNameRequired()
return
}
if (("fakelog" in host || "vulcan" in host) && "@" !in username) {
view?.setUsernameError(view?.invalidEmailString.orEmpty())
return
}
disposable.add(recoverRepository.getReCaptchaSiteKey(host, symbol)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.run {
hideSoftKeyboard()
showRecoverForm(false)
showProgress(true)
showErrorView(false)
showCaptcha(false)
}
}
.subscribe({ (resetUrl, siteKey) ->
view?.loadReCaptcha(siteKey, resetUrl)
}) {
Timber.e("Obtain captcha site key result: An exception occurred")
errorHandler.dispatch(it)
})
}
fun onReCaptchaVerified(reCaptchaResponse: String) {
val username = view?.recoverNameValue.orEmpty()
val host = view?.recoverHostValue.orEmpty()
val symbol = view?.recoverSymbolValue.ifNullOrBlank { "Default" }
with(disposable) {
clear()
add(recoverRepository.sendRecoverRequest(host, symbol, username, reCaptchaResponse)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.run {
showProgress(true)
showRecoverForm(false)
showCaptcha(false)
}
}
.doFinally {
view?.showProgress(false)
}
.subscribe({
view?.run {
showSuccessView(true)
setSuccessTitle(it.substringBefore(". "))
setSuccessMessage(it.substringAfter(". "))
}
analytics.logEvent("account_recover", "register" to host, "symbol" to symbol, "success" to true)
}) {
Timber.e("Send recover request result: An exception occurred")
errorHandler.dispatch(it)
analytics.logEvent("account_recover", "register" to host, "symbol" to symbol, "success" to false)
})
}
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun showErrorMessage(message: String, error: Throwable) {
view?.run {
lastError = error
showProgress(false)
setErrorDetails(message)
showErrorView(true)
}
}
private fun onInvalidUsername(message: String) {
view?.run {
setUsernameError(message)
showRecoverForm(true)
}
}
private fun onInvalidCaptcha(message: String, error: Throwable) {
view?.run {
lastError = error
setErrorDetails(message)
showCaptcha(false)
showRecoverForm(false)
showErrorView(true)
}
}
}

View File

@ -0,0 +1,54 @@
package io.github.wulkanowy.ui.modules.login.recover
import io.github.wulkanowy.ui.base.BaseView
interface LoginRecoverView : BaseView {
val recoverHostValue: String
val recoverNameValue: String
val recoverSymbolValue: String
val emailHintString: String
val loginPeselEmailHintString: String
val invalidEmailString: String
fun initView()
fun setDefaultCredentials(username: String, symbol: String)
fun clearUsernameError()
fun showSymbol(show: Boolean)
fun setErrorNameRequired()
fun setUsernameHint(hint: String)
fun setUsernameError(message: String)
fun showSoftKeyboard()
fun hideSoftKeyboard()
fun showProgress(show: Boolean)
fun showRecoverForm(show: Boolean)
fun showCaptcha(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showSuccessView(show: Boolean)
fun setSuccessMessage(message: String)
fun setSuccessTitle(title: String)
fun loadReCaptcha(siteKey: String, url: String)
}

View File

@ -0,0 +1,32 @@
package io.github.wulkanowy.ui.modules.login.recover
import android.content.res.Resources
import com.readystatesoftware.chuck.api.ChuckCollector
import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException
import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException
import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class RecoverErrorHandler @Inject constructor(
resources: Resources,
chuckCollector: ChuckCollector
) : ErrorHandler(resources, chuckCollector) {
var onInvalidUsername: (String) -> Unit = {}
var onInvalidCaptcha: (String, Throwable) -> Unit = { _, _ -> }
override fun proceed(error: Throwable) {
when (error) {
is InvalidEmailException, is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty())
is InvalidCaptchaException -> onInvalidCaptcha(error.localizedMessage.orEmpty(), error)
else -> super.proceed(error)
}
}
override fun clear() {
super.clear()
onInvalidUsername = {}
}
}

View File

@ -154,7 +154,7 @@
android:hint="@string/login_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null"
app:layout_constraintBottom_toTopOf="@+id/loginFormHostLayout"
app:layout_constraintBottom_toTopOf="@+id/loginFormRecoverLink"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout"
@ -174,6 +174,22 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginFormRecoverLink"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:text="@string/login_recover_button"
android:textAppearance="?android:textAppearance"
app:backgroundTint="?android:windowBackground"
app:fontFamily="sans-serif-medium"
app:layout_constraintBottom_toTopOf="@id/loginFormHostLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormPassLayout"
tools:visibility="visible" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFormHostLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
@ -181,7 +197,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:hint="@string/login_host_hint"
@ -189,7 +205,7 @@
app:layout_constraintBottom_toTopOf="@+id/loginFormSignIn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormPassLayout">
app:layout_constraintTop_toBottomOf="@+id/loginFormRecoverLink">
<AutoCompleteTextView
android:id="@+id/loginFormHost"

View File

@ -0,0 +1,272 @@
<FrameLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.modules.login.recover.LoginRecoverFragment">
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/loginRecoverProgress"
style="@style/Widget.MaterialProgressBar.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none"
tools:visibility="invisible">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginRecoverFormContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:visibility="visible">
<TextView
android:id="@+id/loginFormHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:gravity="center_horizontal"
android:text="@string/login_recover_title"
android:textSize="16sp"
app:fontFamily="sans-serif-light"
app:layout_constraintBottom_toTopOf="@+id/loginRecoverNameLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginRecoverNameLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:hint="@string/login_email_hint"
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/loginRecoverHostLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormHeader">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginRecoverName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="emailAddress"
android:inputType="textEmailAddress"
android:maxLines="1"
tools:targetApi="o" />
<requestFocus />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginRecoverHostLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:hint="@string/login_host_hint"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/loginRecoverSymbolLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginRecoverNameLayout">
<AutoCompleteTextView
android:id="@+id/loginRecoverHost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:editable="false"
tools:ignore="Deprecated,LabelFor" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginRecoverSymbolLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:hint="@string/login_symbol_hint_optional"
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/loginRecoverButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginRecoverHostLayout">
<AutoCompleteTextView
android:id="@+id/loginRecoverSymbol"
style="@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeActionLabel="@string/login_sign_in"
android:imeOptions="actionDone"
android:inputType="textAutoComplete|textNoSuggestions"
android:maxLines="1"
tools:ignore="LabelFor" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/loginRecoverButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:text="@string/login_recover"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginRecoverSymbolLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginRecoverCaptchaContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<WebView
android:id="@+id/loginRecoverWebView"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/loginRecoverError"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables"
tools:visibility="invisible">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_error"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/loginRecoverErrorMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:padding="8dp"
android:text="@string/error_unknown"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/loginRecoverErrorDetails"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginRecoverErrorRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/loginRecoverSuccess"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none"
android:visibility="invisible"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_all_done"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/loginRecoverSuccessTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="35dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="35dp"
android:gravity="center"
android:textSize="20sp"
tools:text="Pomyślnie wysłano email!" />
<TextView
android:id="@+id/loginRecoverSuccessMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="35dp"
android:lineSpacingMultiplier="1.5"
android:scrollbars="vertical"
android:text="@string/login_recover"
android:textSize="14sp"
tools:text="@tools:sample/lorem/random" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginRecoverLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="30dp"
android:layout_marginBottom="30dp"
android:text="@android:string/ok" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -29,9 +29,11 @@
<string name="login_header_symbol">Podaj symbol</string>
<string name="login_nickname_hint">Nazwa użytkownika</string>
<string name="login_email_hint">Email</string>
<string name="login_login_pesel_email_hint">Login, PESEL lub e-mail</string>
<string name="login_password_hint">Hasło</string>
<string name="login_host_hint">Dziennik</string>
<string name="login_symbol_hint">Symbol</string>
<string name="login_symbol_hint_optional">Symbol (opcjonalnie)</string>
<string name="login_type_api">Mobilne API</string>
<string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybrydowe</string>
@ -44,11 +46,12 @@
<string name="login_invalid_pin">Nieprawidłowy PIN</string>
<string name="login_invalid_token">Nieprawidłowy token</string>
<string name="login_expired_token">Token stracił ważność</string>
<string name="login_invalid_email">Niepoprawny adres email</string>
<string name="login_invalid_symbol">Niepoprawny symbol</string>
<string name="login_incorrect_symbol">Nie znaleziono ucznia. Sprawdź symbol</string>
<string name="login_field_required">To pole jest wymagane</string>
<string name="login_duplicate_student">Wybrany uczeń jest już zalogowany</string>
<string name="login_symbol_helper">Symbol znajdziesz na stronie dziennika w Uczeń -> Dostęp Mobilny -> Zarejestruj urządzenie mobilne</string>
<string name="login_symbol_helper">Symbol znajdziesz na stronie dziennika w Uczeń -> Dostęp Mobilny -> Zarejestruj urządzenie mobilne</string>
<string name="login_select_student">Wybierz uczniów do zalogowania w aplikacji</string>
<string name="login_advanced">Inne opcje</string>
<string name="login_privacy_policy">Polityka prywatności</string>
@ -56,6 +59,9 @@
<string name="login_contact_email">Email</string>
<string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Wyślij email</string>
<string name="login_recover_button">Nie pamiętam hasła</string>
<string name="login_recover_title">Przywróć swoje konto</string>
<string name="login_recover">Przywróć</string>
<string name="login_signed_in">Uczeń jest już zalogowany</string>

View File

@ -30,6 +30,7 @@
<string name="login_header_symbol">Впишите \"symbol\"</string>
<string name="login_nickname_hint">Имя пользователя</string>
<string name="login_email_hint">Электронная почта</string>
<string name="login_login_pesel_email_hint">Логин, PESEL или электронная почта</string>
<string name="login_password_hint">Пароль</string>
<string name="login_host_hint">Дневник</string>
<string name="login_type_api">Мобильный API</string>
@ -39,12 +40,14 @@
<string name="login_pin_hint">PIN</string>
<string name="login_api_key_hint">клавиша API</string>
<string name="login_symbol_hint">Symbol</string>
<string name="login_symbol_hint_optional">Символ (необязательно)</string>
<string name="login_sign_in">Войти</string>
<string name="login_invalid_password">Слишком короткий пароль</string>
<string name="login_incorrect_password">Указаны неверные данные</string>
<string name="login_invalid_pin">Недействительный PIN</string>
<string name="login_invalid_token">Недействительный token</string>
<string name="login_expired_token">Токен просрочен</string>
<string name="login_invalid_email">Неверный адрес электронной почты</string>
<string name="login_invalid_symbol">Недействительный symbol</string>
<string name="login_incorrect_symbol">Не удалось найти ученика. Пожалуйста, проверьте \"symbol\"</string>
<string name="login_field_required">Это поле обязательно</string>
@ -57,6 +60,9 @@
<string name="login_contact_email">Электронная почта</string>
<string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Отправить письмо</string>
<string name="login_recover">восстановление</string>
<string name="login_recover_button">Я не помню пароль</string>
<string name="login_recover_title">Восстановите свой аккаунт</string>
<string name="login_signed_in">Студент уже вошел в систему</string>

View File

@ -30,6 +30,7 @@
<string name="login_header_symbol">Enter the symbol</string>
<string name="login_nickname_hint">Username</string>
<string name="login_email_hint">Email</string>
<string name="login_login_pesel_email_hint">Login, PESEL or e-mail</string>
<string name="login_password_hint">Password</string>
<string name="login_host_hint">Register</string>
<string name="login_type_api">Mobile API</string>
@ -39,12 +40,14 @@
<string name="login_pin_hint">PIN</string>
<string name="login_api_key_hint">API key</string>
<string name="login_symbol_hint">Symbol</string>
<string name="login_symbol_hint_optional">Symbol (optional)</string>
<string name="login_sign_in">Sign in</string>
<string name="login_invalid_password">Password too short</string>
<string name="login_incorrect_password">Login details are incorrect</string>
<string name="login_invalid_pin">Invalid PIN</string>
<string name="login_invalid_token">Invalid token</string>
<string name="login_expired_token">Token expired</string>
<string name="login_invalid_email">Invalid email</string>
<string name="login_invalid_symbol">Invalid symbol</string>
<string name="login_incorrect_symbol">Student not found. Check the symbol</string>
<string name="login_field_required">This field is required</string>
@ -59,6 +62,9 @@
<string name="login_email_intent_title">Send email</string>
<string name="login_email_subject" translatable="false">Zgłoszenie: Problemy z logowaniem</string>
<string name="login_email_text" translatable="false">Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\n\nOpis problemu:</string>
<string name="login_recover_button">I forgot my password</string>
<string name="login_recover_title">Recover your account</string>
<string name="login_recover">Recover</string>
<string name="login_signed_in">Student is already signed in</string>