1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 00:39:08 -05:00

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"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" /> <option name="LINE_SEPARATOR" value="&#10;" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS"> <option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value> <value>

View File

@ -122,7 +122,7 @@ configurations.all {
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:7e89883" implementation "io.github.wulkanowy:sdk:6789442"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0" implementation "androidx.core:core-ktx:1.2.0"
@ -173,7 +173,7 @@ dependencies {
implementation "com.jakewharton.threetenabp:threetenabp:1.2.2" implementation "com.jakewharton.threetenabp:threetenabp:1.2.2"
implementation "com.jakewharton.timber:timber:4.7.1" implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.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.mikepenz:aboutlibraries-core:7.1.0"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3' 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.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment 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.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.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
import io.github.wulkanowy.utils.setOnSelectPageListener import io.github.wulkanowy.utils.setOnSelectPageListener
@ -52,7 +53,8 @@ class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {
LoginFormFragment.newInstance(), LoginFormFragment.newInstance(),
LoginSymbolFragment.newInstance(), LoginSymbolFragment.newInstance(),
LoginStudentSelectFragment.newInstance(), LoginStudentSelectFragment.newInstance(),
LoginAdvancedFragment.newInstance() LoginAdvancedFragment.newInstance(),
LoginRecoverFragment.newInstance()
)) ))
} }
@ -99,4 +101,8 @@ class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {
fun onAdvancedLoginClick() { fun onAdvancedLoginClick() {
presenter.onAdvancedLoginClick() presenter.onAdvancedLoginClick()
} }
fun onRecoverClick() {
presenter.onRecoverClick()
}
} }

View File

@ -43,5 +43,8 @@ class LoginErrorHandler @Inject constructor(
super.clear() super.clear()
onBadCredentials = {} onBadCredentials = {}
onStudentDuplicate = {} 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.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment 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.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.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
@ -37,4 +38,8 @@ internal abstract class LoginModule {
@PerFragment @PerFragment
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun bindLoginSelectStudentFragment(): LoginStudentSelectFragment abstract fun bindLoginSelectStudentFragment(): LoginStudentSelectFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindLoginRecoverFragment(): LoginRecoverFragment
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -54,4 +54,6 @@ interface LoginFormView : BaseView {
fun openEmail() fun openEmail()
fun openAdvancedLogin() 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" android:hint="@string/login_password_hint"
app:errorEnabled="true" app:errorEnabled="true"
app:errorIconDrawable="@null" app:errorIconDrawable="@null"
app:layout_constraintBottom_toTopOf="@+id/loginFormHostLayout" app:layout_constraintBottom_toTopOf="@+id/loginFormRecoverLink"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout" app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout"
@ -174,6 +174,22 @@
</com.google.android.material.textfield.TextInputLayout> </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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFormHostLayout" android:id="@+id/loginFormHostLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
@ -181,7 +197,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:layout_marginLeft="24dp" android:layout_marginLeft="24dp"
android:layout_marginTop="16dp" android:layout_marginTop="8dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:layout_marginRight="24dp" android:layout_marginRight="24dp"
android:hint="@string/login_host_hint" android:hint="@string/login_host_hint"
@ -189,7 +205,7 @@
app:layout_constraintBottom_toTopOf="@+id/loginFormSignIn" app:layout_constraintBottom_toTopOf="@+id/loginFormSignIn"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormPassLayout"> app:layout_constraintTop_toBottomOf="@+id/loginFormRecoverLink">
<AutoCompleteTextView <AutoCompleteTextView
android:id="@+id/loginFormHost" 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_header_symbol">Podaj symbol</string>
<string name="login_nickname_hint">Nazwa użytkownika</string> <string name="login_nickname_hint">Nazwa użytkownika</string>
<string name="login_email_hint">Email</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_password_hint">Hasło</string>
<string name="login_host_hint">Dziennik</string> <string name="login_host_hint">Dziennik</string>
<string name="login_symbol_hint">Symbol</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_api">Mobilne API</string>
<string name="login_type_scrapper">Scraper</string> <string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybrydowe</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_pin">Nieprawidłowy PIN</string>
<string name="login_invalid_token">Nieprawidłowy token</string> <string name="login_invalid_token">Nieprawidłowy token</string>
<string name="login_expired_token">Token stracił ważność</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_invalid_symbol">Niepoprawny symbol</string>
<string name="login_incorrect_symbol">Nie znaleziono ucznia. Sprawdź 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_field_required">To pole jest wymagane</string>
<string name="login_duplicate_student">Wybrany uczeń jest już zalogowany</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_select_student">Wybierz uczniów do zalogowania w aplikacji</string>
<string name="login_advanced">Inne opcje</string> <string name="login_advanced">Inne opcje</string>
<string name="login_privacy_policy">Polityka prywatności</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_email">Email</string>
<string name="login_contact_discord">Discord</string> <string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Wyślij email</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> <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_header_symbol">Впишите \"symbol\"</string>
<string name="login_nickname_hint">Имя пользователя</string> <string name="login_nickname_hint">Имя пользователя</string>
<string name="login_email_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_password_hint">Пароль</string>
<string name="login_host_hint">Дневник</string> <string name="login_host_hint">Дневник</string>
<string name="login_type_api">Мобильный API</string> <string name="login_type_api">Мобильный API</string>
@ -39,12 +40,14 @@
<string name="login_pin_hint">PIN</string> <string name="login_pin_hint">PIN</string>
<string name="login_api_key_hint">клавиша API</string> <string name="login_api_key_hint">клавиша API</string>
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_symbol_hint_optional">Символ (необязательно)</string>
<string name="login_sign_in">Войти</string> <string name="login_sign_in">Войти</string>
<string name="login_invalid_password">Слишком короткий пароль</string> <string name="login_invalid_password">Слишком короткий пароль</string>
<string name="login_incorrect_password">Указаны неверные данные</string> <string name="login_incorrect_password">Указаны неверные данные</string>
<string name="login_invalid_pin">Недействительный PIN</string> <string name="login_invalid_pin">Недействительный PIN</string>
<string name="login_invalid_token">Недействительный token</string> <string name="login_invalid_token">Недействительный token</string>
<string name="login_expired_token">Токен просрочен</string> <string name="login_expired_token">Токен просрочен</string>
<string name="login_invalid_email">Неверный адрес электронной почты</string>
<string name="login_invalid_symbol">Недействительный symbol</string> <string name="login_invalid_symbol">Недействительный symbol</string>
<string name="login_incorrect_symbol">Не удалось найти ученика. Пожалуйста, проверьте \"symbol\"</string> <string name="login_incorrect_symbol">Не удалось найти ученика. Пожалуйста, проверьте \"symbol\"</string>
<string name="login_field_required">Это поле обязательно</string> <string name="login_field_required">Это поле обязательно</string>
@ -57,6 +60,9 @@
<string name="login_contact_email">Электронная почта</string> <string name="login_contact_email">Электронная почта</string>
<string name="login_contact_discord">Discord</string> <string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Отправить письмо</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> <string name="login_signed_in">Студент уже вошел в систему</string>

View File

@ -30,6 +30,7 @@
<string name="login_header_symbol">Enter the symbol</string> <string name="login_header_symbol">Enter the symbol</string>
<string name="login_nickname_hint">Username</string> <string name="login_nickname_hint">Username</string>
<string name="login_email_hint">Email</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_password_hint">Password</string>
<string name="login_host_hint">Register</string> <string name="login_host_hint">Register</string>
<string name="login_type_api">Mobile API</string> <string name="login_type_api">Mobile API</string>
@ -39,12 +40,14 @@
<string name="login_pin_hint">PIN</string> <string name="login_pin_hint">PIN</string>
<string name="login_api_key_hint">API key</string> <string name="login_api_key_hint">API key</string>
<string name="login_symbol_hint">Symbol</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_sign_in">Sign in</string>
<string name="login_invalid_password">Password too short</string> <string name="login_invalid_password">Password too short</string>
<string name="login_incorrect_password">Login details are incorrect</string> <string name="login_incorrect_password">Login details are incorrect</string>
<string name="login_invalid_pin">Invalid PIN</string> <string name="login_invalid_pin">Invalid PIN</string>
<string name="login_invalid_token">Invalid token</string> <string name="login_invalid_token">Invalid token</string>
<string name="login_expired_token">Token expired</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_invalid_symbol">Invalid symbol</string>
<string name="login_incorrect_symbol">Student not found. Check the symbol</string> <string name="login_incorrect_symbol">Student not found. Check the symbol</string>
<string name="login_field_required">This field is required</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_intent_title">Send email</string>
<string name="login_email_subject" translatable="false">Zgłoszenie: Problemy z logowaniem</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_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> <string name="login_signed_in">Student is already signed in</string>