diff --git a/Habitica/res/layout/activity_login.xml b/Habitica/res/layout/activity_login.xml
index 6aa208a94..9e6027050 100644
--- a/Habitica/res/layout/activity_login.xml
+++ b/Habitica/res/layout/activity_login.xml
@@ -137,7 +137,8 @@
android:hint="@string/username"
android:theme="@style/LoginEditTextTheme"
android:textColor="@color/white_75_alpha"
- android:textColorHighlight="@color/white"/>
+ android:textColorHighlight="@color/white"
+ android:isCredential="true"/>
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml
index 99a73bdff..684b8f389 100644
--- a/Habitica/res/values/strings.xml
+++ b/Habitica/res/values/strings.xml
@@ -1234,7 +1234,7 @@
Disconnected %s
Password saved
Add Email and Password
- Password needs to be typed correctly twice
+ The entered passwords do not match
Invalid Email address
Purchase successful
Starting Objectives
@@ -1576,6 +1576,9 @@
Habitoween
Turkey Day
Search Equipment
+ Error getting credentials for authentication.
+ Received invalid credentials.
+ Unknown error during authentication.
- You
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ActivityExtension.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ActivityExtension.kt
index 30f595876..8b303d185 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ActivityExtension.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ActivityExtension.kt
@@ -3,8 +3,20 @@ package com.habitrpg.android.habitica.extensions
import android.app.Activity
import android.content.Context
import android.view.inputmethod.InputMethodManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.habitrpg.common.habitica.helpers.launchCatching
+import kotlinx.coroutines.CoroutineScope
fun Activity.hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(window.decorView.windowToken, 0)
}
+
+fun AppCompatActivity.lifecycleLaunchWhen(state: Lifecycle.State, function: suspend CoroutineScope.() -> Unit) {
+ lifecycleScope.launchCatching {
+ repeatOnLifecycle(state, function)
+ }
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/AuthenticationErrors-Extensions.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/AuthenticationErrors-Extensions.kt
new file mode 100644
index 000000000..2cbdba99a
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/AuthenticationErrors-Extensions.kt
@@ -0,0 +1,16 @@
+package com.habitrpg.android.habitica.extensions
+
+enum class AuthenticationErrors {
+ // Google Auth
+ GET_CREDENTIALS_ERROR,
+ INVALID_CREDENTIALS,
+ // Validation
+ PASSWORD_TOO_SHORT,
+ PASSWORD_MISMATCH,
+ MISSING_FIELDS;
+
+ var minPasswordLength: Int = 6
+
+ val isValidationError: Boolean
+ get() = this == PASSWORD_TOO_SHORT || this == PASSWORD_MISMATCH || this == MISSING_FIELDS
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt
index 9ce438733..2840cb434 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt
@@ -92,8 +92,8 @@ class AppConfigManager(contentRepository: ContentRepository) :
return remoteConfig.getBoolean("showSubscriptionBanner")
}
- fun minimumPasswordLength(): Long {
- return remoteConfig.getLong("minimumPasswordLength")
+ fun minimumPasswordLength(): Int {
+ return remoteConfig.getLong("minimumPasswordLength").toInt()
}
fun enableTaskDisplayMode(): Boolean {
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BaseActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BaseActivity.kt
index 63b5e7f64..7cdbfb7d8 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BaseActivity.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BaseActivity.kt
@@ -20,7 +20,9 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceManager
import com.habitrpg.android.habitica.HabiticaApplication
import com.habitrpg.android.habitica.R
@@ -39,6 +41,7 @@ import com.habitrpg.common.habitica.extensions.isUsingNightModeResources
import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.helpers.LanguageHelper
import com.habitrpg.common.habitica.helpers.launchCatching
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileNotFoundException
@@ -314,4 +317,5 @@ abstract class BaseActivity : AppCompatActivity() {
overridePendingTransition(R.anim.activity_fade_in, R.anim.activity_fade_out)
startActivity(intent)
}
+
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/LoginActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/LoginActivity.kt
index b4472f5ce..ba51aab68 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/LoginActivity.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/LoginActivity.kt
@@ -1,12 +1,11 @@
package com.habitrpg.android.habitica.ui.activities
-import android.accounts.AccountManager
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
-import android.app.Activity
+import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
@@ -15,127 +14,50 @@ import android.text.InputType
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.UnderlineSpan
-import android.view.MenuItem
import android.view.View
import android.view.Window
-import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.LinearLayout
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.content.ContextCompat
+import androidx.activity.addCallback
+import androidx.activity.viewModels
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.google.android.gms.tasks.Tasks
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.Wearable
-import com.google.firebase.analytics.FirebaseAnalytics
import com.habitrpg.android.habitica.R
-import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.databinding.ActivityLoginBinding
import com.habitrpg.android.habitica.extensions.addCancelButton
import com.habitrpg.android.habitica.extensions.addOkButton
+import com.habitrpg.android.habitica.extensions.lifecycleLaunchWhen
import com.habitrpg.android.habitica.extensions.updateStatusBarColor
-import com.habitrpg.android.habitica.helpers.Analytics
import com.habitrpg.android.habitica.helpers.AppConfigManager
-import com.habitrpg.android.habitica.helpers.EventCategory
-import com.habitrpg.android.habitica.helpers.HitType
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
+import com.habitrpg.android.habitica.extensions.AuthenticationErrors
import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
-import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.helpers.launchCatching
import com.habitrpg.common.habitica.models.auth.UserAuthResponse
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class LoginActivity : BaseActivity() {
private lateinit var binding: ActivityLoginBinding
-
- @Inject
- lateinit var apiClient: ApiClient
-
- @Inject
- lateinit var sharedPrefs: SharedPreferences
+ val viewModel by viewModels()
@Inject
lateinit var configManager: AppConfigManager
- @Inject
- lateinit var viewModel: AuthenticationViewModel
-
- private var isRegistering: Boolean = false
private var isShowingForm: Boolean = false
- private val loginClick =
- View.OnClickListener {
- binding.PBAsyncTask.visibility = View.VISIBLE
- if (isRegistering) {
- registerWithPassword()
- } else {
- loginWithPassword()
- }
- }
-
- private fun loginWithPassword() {
- val username: String = binding.username.text.toString().trim { it <= ' ' }
- val password: String = binding.password.text.toString()
- if (username.isEmpty() || password.isEmpty()) {
- showValidationError(R.string.login_validation_error_fieldsmissing)
- return
- }
- lifecycleScope.launch(
- ExceptionHandler.coroutine {
- hideProgress()
- ExceptionHandler.reportError(it)
- }
- ) {
- val response = apiClient.connectUser(username, password)
- if (response != null) {
- handleAuthResponse(response)
- } else {
- hideProgress()
- }
- }
- }
-
- private fun registerWithPassword() {
- val username: String = binding.username.text.toString().trim { it <= ' ' }
- val email: String = binding.email.text.toString().trim { it <= ' ' }
- val password: String = binding.password.text.toString()
- val confirmPassword: String = binding.confirmPassword.text.toString()
- if (username.isEmpty() || password.isEmpty() || email.isEmpty() || confirmPassword.isEmpty()) {
- showValidationError(R.string.login_validation_error_fieldsmissing)
- return
- }
- if (password.length < configManager.minimumPasswordLength()) {
- showValidationError(
- getString(
- R.string.password_too_short,
- configManager.minimumPasswordLength()
- )
- )
- return
- }
- lifecycleScope.launch(
- ExceptionHandler.coroutine {
- hideProgress()
- ExceptionHandler.reportError(it)
- }
- ) {
- val response = apiClient.registerUser(username, email, password, confirmPassword)
- if (response != null) {
- handleAuthResponse(response)
- } else {
- hideProgress()
- }
- }
- }
-
override fun getLayoutResId(): Int {
return R.layout.activity_login
}
@@ -152,71 +74,96 @@ class LoginActivity : BaseActivity() {
// Set default values to avoid null-responses when requesting unedited settings
PreferenceManager.setDefaultValues(this, R.xml.preferences_fragment, false)
- binding.loginBtn.setOnClickListener(loginClick)
-
- val content = SpannableString(binding.forgotPassword.text)
- content.setSpan(UnderlineSpan(), 0, content.length, 0)
- binding.forgotPassword.text = content
- binding.privacyPolicy.movementMethod = LinkMovementMethod.getInstance()
-
- this.isRegistering = true
-
- val additionalData = HashMap()
- additionalData["page"] = this.javaClass.simpleName
-
- binding.backgroundContainer.post {
- binding.backgroundContainer.scrollTo(
- 0,
- binding.backgroundContainer.bottom
- )
- }
+ configureSpecialUI()
binding.backgroundContainer.isScrollable = false
- window.statusBarColor = ContextCompat.getColor(this, R.color.black_20_alpha)
- window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ setupOnClickListeners()
+ setupViewmodelObserving()
+ }
+ private fun setupViewmodelObserving() {
+ lifecycleLaunchWhen(Lifecycle.State.RESUMED) {
+ viewModel.showAuthProgress.collect { showProgress ->
+ binding.progressView.isVisible = showProgress
+ }
+ }
+ lifecycleLaunchWhen(Lifecycle.State.RESUMED) {
+ viewModel.isRegistering.collect { isRegistering ->
+ if (isRegistering) {
+ configureForRegistering()
+ } else {
+ configureForLogin()
+ }
+ }
+ }
+ lifecycleLaunchWhen(Lifecycle.State.RESUMED) {
+ viewModel.authenticationError
+ .filterNotNull()
+ .collect { showError(it) }
+ }
+ lifecycleLaunchWhen(Lifecycle.State.RESUMED) {
+ viewModel.authenticationSuccess
+ .filterNotNull()
+ .collect { didRegister ->
+ if (didRegister) {
+ startSetupActivity()
+ } else {
+ startMainActivity()
+ }
+ }
+ }
+ }
+
+ private fun setupOnClickListeners() {
+ onBackPressedDispatcher.addCallback(this) {
+ if (isShowingForm) {
+ hideForm()
+ } else {
+ finish()
+ }
+ }
binding.newGameButton.setOnClickListener { newGameButtonClicked() }
binding.showLoginButton.setOnClickListener { showLoginButtonClicked() }
binding.backButton.setOnClickListener { backButtonClicked() }
binding.forgotPassword.setOnClickListener { onForgotPasswordClicked() }
binding.googleLoginButton.setOnClickListener {
- binding.googleLoginProgress.visibility = View.VISIBLE
- viewModel.handleGoogleLogin(this, pickAccountResult)
+ viewModel.startGoogleAuth(this)
+ }
+ binding.submitButton.setOnClickListener {
+ if (viewModel.isRegistering.value) {
+ registerWithPassword()
+ } else {
+ loginWithPassword()
+ }
+ }
+ }
+
+ private fun configureSpecialUI() {
+ val content = SpannableString(binding.forgotPassword.text)
+ content.setSpan(UnderlineSpan(), 0, content.length, 0)
+ binding.forgotPassword.text = content
+ binding.privacyPolicy.movementMethod = LinkMovementMethod.getInstance()
+
+ binding.backgroundContainer.post {
+ binding.backgroundContainer.scrollTo(
+ 0,
+ binding.backgroundContainer.bottom,
+ )
}
}
override fun loadTheme(
sharedPreferences: SharedPreferences,
- forced: Boolean
+ forced: Boolean,
) {
super.loadTheme(sharedPreferences, forced)
window.updateStatusBarColor(R.color.black_20_alpha, false)
}
- override fun onBackPressed() {
- if (isShowingForm) {
- hideForm()
- } else {
- super.onBackPressed()
- }
- }
-
private fun resetLayout() {
- if (this.isRegistering) {
- if (binding.email.visibility == View.GONE) {
- show(binding.email)
- }
- if (binding.confirmPassword.visibility == View.GONE) {
- show(binding.confirmPassword)
- }
- } else {
- if (binding.email.visibility == View.VISIBLE) {
- hide(binding.email)
- }
- if (binding.confirmPassword.visibility == View.VISIBLE) {
- hide(binding.confirmPassword)
- }
- }
+ val isRegistering = viewModel.isRegistering.value
+ binding.email.isVisible = isRegistering
+ binding.confirmPassword.isVisible = isRegistering
}
private fun startMainActivity() {
@@ -233,154 +180,104 @@ class LoginActivity : BaseActivity() {
finish()
}
- private fun toggleRegistering() {
- this.isRegistering = (!this.isRegistering)
- this.setRegistering()
- }
-
- private fun setRegistering() {
- if (this.isRegistering) {
- binding.loginBtn.text = getString(R.string.register_btn)
- binding.username.setHint(R.string.username)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- binding.username.setAutofillHints("newUsername")
- binding.password.setAutofillHints("newPassword")
- }
- binding.password.imeOptions = EditorInfo.IME_ACTION_NEXT
- binding.googleLoginButton.setText(R.string.register_btn_google)
- } else {
- binding.loginBtn.text = getString(R.string.login_btn)
- binding.username.setHint(R.string.email_username)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- binding.username.setAutofillHints("username")
- binding.password.setAutofillHints("password")
- }
- binding.password.imeOptions = EditorInfo.IME_ACTION_DONE
- binding.googleLoginButton.setText(R.string.login_btn_google)
+ private fun configureForRegistering() {
+ binding.submitButton.text = getString(R.string.register_btn)
+ binding.username.setHint(R.string.username)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ binding.username.setAutofillHints("newUsername")
+ binding.password.setAutofillHints("newPassword")
}
+ binding.password.imeOptions = EditorInfo.IME_ACTION_NEXT
+ binding.googleLoginButton.setText(R.string.register_btn_google)
+
this.resetLayout()
}
- private fun handleAuthResponse(response: UserAuthResponse) {
- viewModel.handleAuthResponse(response)
- try {
- val messageClient: MessageClient = Wearable.getMessageClient(this)
- val capabilityClient: CapabilityClient = Wearable.getCapabilityClient(this)
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- val info =
- Tasks.await(
- capabilityClient.getCapability(
- "receive_message",
- CapabilityClient.FILTER_REACHABLE
- )
- )
- info.nodes.forEach {
- Tasks.await(
- messageClient.sendMessage(
- it.id,
- "/auth",
- "${response.id}:${response.apiToken}".toByteArray()
- )
- )
- }
- } catch (e: Exception) {
- // Wearable API is not available on this device.
+ private fun configureForLogin() {
+ binding.submitButton.text = getString(R.string.login_btn)
+ binding.username.setHint(R.string.email_username)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ binding.username.setAutofillHints("username")
+ binding.password.setAutofillHints("password")
+ }
+ binding.password.imeOptions = EditorInfo.IME_ACTION_DONE
+ binding.googleLoginButton.setText(R.string.login_btn_google)
+ this.resetLayout()
+ }
+
+ private fun loginWithPassword() {
+ val username: String = binding.username.text.toString()
+ val password: String = binding.password.text.toString()
+ viewModel.validateInputs(username, password)?.let {
+ showError(it)
+ return
+ }
+ viewModel.login(username, password)
+ }
+
+ private fun registerWithPassword() {
+ val username = binding.username.text.toString()
+ val email = binding.email.text.toString()
+ val password = binding.password.text.toString()
+ val confirmPassword = binding.confirmPassword.text.toString()
+ viewModel.validateInputs(username, password, email, confirmPassword)?.let {
+ showError(it)
+ return
+ }
+ viewModel.register(username, email, password, confirmPassword)
+ }
+
+ private fun sendAuthToWearables(response: UserAuthResponse) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val messageClient: MessageClient = Wearable.getMessageClient(this@LoginActivity)
+ val capabilityClient: CapabilityClient = Wearable.getCapabilityClient(this@LoginActivity)
+ try {
+ val info =
+ Tasks.await(
+ capabilityClient.getCapability(
+ "receive_message",
+ CapabilityClient.FILTER_REACHABLE,
+ ),
+ )
+ info.nodes.forEach {
+ Tasks.await(
+ messageClient.sendMessage(
+ it.id,
+ "/auth",
+ "${response.id}:${response.apiToken}".toByteArray(),
+ ),
+ )
}
- }
- } catch (e: Exception) {
- // Wearable API is not available on this device.
- }
- handleAuthResponse(response.newUser)
- }
-
- private fun handleAuthResponse(isNew: Boolean) {
- hideProgress()
- dismissKeyboard()
-
- if (isRegistering) {
- FirebaseAnalytics.getInstance(this).logEvent("user_registered", null)
- }
- lifecycleScope.launch(ExceptionHandler.coroutine()) {
- userRepository.retrieveUser(true, true)
- if (isNew) {
- startSetupActivity()
- } else {
- startMainActivity()
- Analytics.sendEvent("login", EventCategory.BEHAVIOUR, HitType.EVENT)
+ } catch (_: Exception) {
+ // Wearable API is not available on this device.
}
}
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.action_toggleRegistering -> toggleRegistering()
- }
- return super.onOptionsItemSelected(item)
- }
-
- private fun hideProgress() {
- runOnUiThread {
- binding.googleLoginProgress.visibility = View.GONE
- binding.PBAsyncTask.visibility = View.GONE
- }
- }
-
- private fun showValidationError(resourceMessageString: Int) {
- showValidationError(getString(resourceMessageString))
- }
-
- private fun showValidationError(message: String) {
- binding.PBAsyncTask.visibility = View.GONE
+ private fun showError(error: AuthenticationErrors) {
val alert = HabiticaAlertDialog(this)
- alert.setTitle(R.string.login_validation_error_title)
- alert.setMessage(message)
+ if (error.isValidationError) {
+ alert.setTitle(R.string.login_validation_error_title)
+ } else {
+ alert.setTitle(R.string.authentication_error_title)
+ }
+ alert.setMessage(error.translatedMessage(this))
alert.addOkButton()
alert.show()
}
- private val pickAccountResult =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == Activity.RESULT_OK) {
- viewModel.googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
- viewModel.handleGoogleLoginResult(
- this,
- recoverFromPlayServicesErrorResult
- ) { isNew ->
- handleAuthResponse(isNew)
- }
- } else {
- binding.googleLoginProgress.visibility = View.GONE
- }
- }
-
- private val recoverFromPlayServicesErrorResult =
- registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) {
- if (it.resultCode != Activity.RESULT_CANCELED) {
- viewModel.handleGoogleLoginResult(this, null) { isNew ->
- handleAuthResponse(isNew)
- }
- }
- }
-
private fun newGameButtonClicked() {
- isRegistering = true
+ viewModel.isRegistering.value = true
showForm()
- setRegistering()
}
private fun showLoginButtonClicked() {
- isRegistering = false
+ viewModel.isRegistering.value = false
showForm()
- setRegistering()
}
private fun backButtonClicked() {
- if (isShowingForm) {
- hideForm()
- }
+ hideForm()
}
private fun showForm() {
@@ -394,7 +291,7 @@ class LoginActivity : BaseActivity() {
val scaleLogoAnimation =
ValueAnimator.ofInt(
binding.logoView.measuredHeight,
- (binding.logoView.measuredHeight * 0.75).toInt()
+ (binding.logoView.measuredHeight * 0.75).toInt(),
)
scaleLogoAnimation.addUpdateListener { valueAnimator ->
val value = valueAnimator.animatedValue as? Int ?: 0
@@ -402,7 +299,7 @@ class LoginActivity : BaseActivity() {
layoutParams.height = value
binding.logoView.layoutParams = layoutParams
}
- if (isRegistering) {
+ if (viewModel.isRegistering.value) {
newGameAlphaAnimation.startDelay = 600
newGameAlphaAnimation.duration = 400
showLoginAlphaAnimation.duration = 400
@@ -414,7 +311,7 @@ class LoginActivity : BaseActivity() {
binding.loginScrollview.visibility = View.VISIBLE
binding.loginScrollview.alpha = 1f
}
- }
+ },
)
} else {
showLoginAlphaAnimation.startDelay = 600
@@ -428,7 +325,7 @@ class LoginActivity : BaseActivity() {
binding.loginScrollview.visibility = View.VISIBLE
binding.loginScrollview.alpha = 1f
}
- }
+ },
)
}
val backAlphaAnimation =
@@ -438,7 +335,7 @@ class LoginActivity : BaseActivity() {
panAnimation,
newGameAlphaAnimation,
showLoginAlphaAnimation,
- scaleLogoAnimation
+ scaleLogoAnimation,
)
showAnimation.play(backAlphaAnimation).after(panAnimation)
for (i in 0 until binding.formWrapper.childCount) {
@@ -453,12 +350,15 @@ class LoginActivity : BaseActivity() {
}
private fun hideForm() {
+ if (!isShowingForm) {
+ return
+ }
isShowingForm = false
val panAnimation =
ObjectAnimator.ofInt(
binding.backgroundContainer,
"scrollY",
- binding.backgroundContainer.bottom
+ binding.backgroundContainer.bottom,
).setDuration(1000)
val newGameAlphaAnimation =
ObjectAnimator.ofFloat(binding.newGameButton, View.ALPHA, 1.toFloat()).setDuration(700)
@@ -468,7 +368,7 @@ class LoginActivity : BaseActivity() {
val scaleLogoAnimation =
ValueAnimator.ofInt(
binding.logoView.measuredHeight,
- (binding.logoView.measuredHeight * 1.333333).toInt()
+ (binding.logoView.measuredHeight * 1.333333).toInt(),
)
scaleLogoAnimation.addUpdateListener { valueAnimator ->
val value = valueAnimator.animatedValue as? Int
@@ -487,7 +387,7 @@ class LoginActivity : BaseActivity() {
binding.showLoginButton.visibility = View.VISIBLE
binding.loginScrollview.visibility = View.INVISIBLE
}
- }
+ },
)
val backAlphaAnimation =
ObjectAnimator.ofFloat(binding.backButton, View.ALPHA, 0.toFloat()).setDuration(800)
@@ -496,7 +396,7 @@ class LoginActivity : BaseActivity() {
panAnimation,
scrollViewAlphaAnimation,
backAlphaAnimation,
- scaleLogoAnimation
+ scaleLogoAnimation,
)
showAnimation.play(newGameAlphaAnimation).after(scrollViewAlphaAnimation)
showAnimation.play(showLoginAlphaAnimation).after(scrollViewAlphaAnimation)
@@ -515,7 +415,7 @@ class LoginActivity : BaseActivity() {
val lp =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.MATCH_PARENT
+ LinearLayout.LayoutParams.MATCH_PARENT,
)
input.layoutParams = lp
val alertDialog = HabiticaAlertDialog(this)
@@ -543,14 +443,15 @@ class LoginActivity : BaseActivity() {
dismissKeyboard()
super.finish()
}
+}
- companion object {
- fun show(v: View) {
- v.visibility = View.VISIBLE
- }
+fun AuthenticationErrors.translatedMessage(context: Context): String {
+ return when (this) {
+ AuthenticationErrors.GET_CREDENTIALS_ERROR -> context.getString(R.string.auth_get_credentials_error)
+ AuthenticationErrors.INVALID_CREDENTIALS -> context.getString(R.string.auth_invalid_credentials)
- fun hide(v: View) {
- v.visibility = View.GONE
- }
+ AuthenticationErrors.MISSING_FIELDS -> context.getString(R.string.login_validation_error_fieldsmissing)
+ AuthenticationErrors.PASSWORD_MISMATCH -> context.getString(R.string.password_not_matching)
+ AuthenticationErrors.PASSWORD_TOO_SHORT -> context.getString(R.string.password_too_short, minPasswordLength)
}
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/stable/StableFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/stable/StableFragment.kt
index 2529265f2..1742af4d0 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/stable/StableFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/stable/StableFragment.kt
@@ -18,8 +18,6 @@ import dagger.hilt.android.AndroidEntryPoint
class StableFragment : BaseMainFragment() {
override var binding: FragmentViewpagerBinding? = null
- private val viewModel: StableViewModel by viewModels()
-
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt
index e522e54ed..05401f835 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt
@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.util.PatternsCompat
import androidx.core.view.isVisible
+import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.Preference
@@ -49,14 +50,11 @@ class AccountPreferenceFragment :
BasePreferencesFragment(),
SharedPreferences.OnSharedPreferenceChangeListener,
AccountUpdateConfirmed {
+ val viewModel: AuthenticationViewModel by viewModels()
+
@Inject
lateinit var hostConfig: HostConfig
- @Inject
- lateinit var apiClient: ApiClient
-
- @Inject
- lateinit var viewModel: AuthenticationViewModel
private lateinit var accountDialog: HabiticaAccountDialog
override var user: User? = null
@@ -89,11 +87,11 @@ class AccountPreferenceFragment :
val user = user ?: return
configurePreference(
findPreference("username"),
- user.authentication?.localAuthentication?.username
+ user.authentication?.localAuthentication?.username,
)
configurePreference(
findPreference("email"),
- user.authentication?.localAuthentication?.email ?: getString(R.string.not_set)
+ user.authentication?.localAuthentication?.email ?: getString(R.string.not_set),
)
findPreference("confirm_username")?.isVisible =
user.flags?.verifiedUsername != true
@@ -147,7 +145,7 @@ class AccountPreferenceFragment :
private fun configurePreference(
preference: Preference?,
- value: String?
+ value: String?,
) {
(preference as? EditTextPreference)?.let {
it.text = value
@@ -189,14 +187,14 @@ class AccountPreferenceFragment :
updateUser(
"profile.name",
user?.profile?.name,
- getString(R.string.display_name)
+ getString(R.string.display_name),
)
"photo_url" ->
updateUser(
"profile.imageUrl",
user?.profile?.imageUrl,
- getString(R.string.photo_url)
+ getString(R.string.photo_url),
)
"about" -> updateUser("profile.blurb", user?.profile?.blurb, getString(R.string.about))
@@ -205,7 +203,7 @@ class AccountPreferenceFragment :
disconnect("google", "Google")
} else {
activity?.let {
- viewModel.handleGoogleLogin(it, pickAccountResult)
+ viewModel.startGoogleAuth(it)
}
}
}
@@ -228,15 +226,14 @@ class AccountPreferenceFragment :
private fun disconnect(
network: String,
- networkName: String
+ networkName: String,
) {
context?.let { context ->
val dialog = HabiticaAlertDialog(context)
dialog.setTitle(R.string.are_you_sure)
dialog.addButton(R.string.disconnect, true) { _, _ ->
lifecycleScope.launch {
- apiClient.disconnectSocial(network)
- userRepository.retrieveUser(true, true)
+ viewModel.removeSocialAuth(network)
displayDisconnectSuccess(networkName)
}
}
@@ -245,52 +242,24 @@ class AccountPreferenceFragment :
}
}
- private val pickAccountResult =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == Activity.RESULT_OK) {
- viewModel.googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
- activity?.let { it1 ->
- viewModel.handleGoogleLoginResult(
- it1,
- recoverFromPlayServicesErrorResult
- ) { _ ->
- displayAuthenticationSuccess(getString(R.string.google))
- }
- }
- }
- }
-
private fun displayAuthenticationSuccess(network: String) {
(activity as? SnackbarActivity)?.showSnackbar(
content = context?.getString(R.string.added_social_auth, network),
- displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS
+ displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
)
}
private fun displayDisconnectSuccess(network: String) {
(activity as? SnackbarActivity)?.showSnackbar(
content = context?.getString(R.string.removed_social_auth, network),
- displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS
+ displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
)
}
- private val recoverFromPlayServicesErrorResult =
- registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) {
- if (it.resultCode != Activity.RESULT_CANCELED) {
- activity?.let { it1 ->
- viewModel.handleGoogleLoginResult(it1, null) { _ ->
- displayAuthenticationSuccess(getString(R.string.google))
- }
- }
- }
- }
-
private fun updateUser(
path: String,
value: String?,
- title: String
+ title: String,
) {
showSingleEntryDialog(value, title) {
if (value != it) {
@@ -325,11 +294,11 @@ class AccountPreferenceFragment :
userRepository.updatePassword(
oldPasswordEditText?.text ?: "",
passwordEditText.text ?: "",
- passwordRepeatEditText.text ?: ""
+ passwordRepeatEditText.text ?: "",
)
(activity as? SnackbarActivity)?.showSnackbar(
content = context.getString(R.string.password_changed),
- displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS
+ displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
)
}
d.dismiss()
@@ -371,15 +340,15 @@ class AccountPreferenceFragment :
val email =
if (showEmail) emailEditText?.text else user?.authentication?.findFirstSocialEmail()
lifecycleScope.launchCatching {
- apiClient.registerUser(
+ viewModel.register(
user?.username ?: "",
email ?: "",
passwordEditText.text ?: "",
- passwordRepeatEditText.text ?: ""
+ passwordRepeatEditText.text ?: "",
)
(activity as? SnackbarActivity)?.showSnackbar(
content = context.getString(R.string.password_added),
- displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS
+ displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
)
}
dialog.dismiss()
@@ -413,7 +382,7 @@ class AccountPreferenceFragment :
lifecycleScope.launchCatching {
userRepository.updateEmail(
emailEditText.text.toString(),
- passwordEditText?.text.toString()
+ passwordEditText?.text.toString(),
)
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.retrieveUser(true, true)
@@ -448,7 +417,7 @@ class AccountPreferenceFragment :
value: String?,
title: String,
validator: ((String?) -> Boolean)? = null,
- onChange: (String?) -> Unit
+ onChange: (String?) -> Unit,
) {
val inflater = context?.layoutInflater
val view = inflater?.inflate(R.layout.dialog_edittext, null)
@@ -539,7 +508,7 @@ class AccountPreferenceFragment :
dialog.addButton(R.string.confirm, true) { _, _ ->
lifecycleScope.launchCatching {
userRepository.updateLoginName(
- user?.authentication?.localAuthentication?.username ?: ""
+ user?.authentication?.localAuthentication?.username ?: "",
)
}
}
@@ -558,7 +527,7 @@ class AccountPreferenceFragment :
private fun copyValue(
name: String,
- value: CharSequence?
+ value: CharSequence?,
) {
val clipboard: ClipboardManager? =
context?.let { getSystemService(it, ClipboardManager::class.java) }
@@ -566,14 +535,14 @@ class AccountPreferenceFragment :
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
(activity as? SnackbarActivity)?.showSnackbar(
content = context?.getString(R.string.copied_to_clipboard, name),
- displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS
+ displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS,
)
}
}
override fun onSharedPreferenceChanged(
p0: SharedPreferences?,
- p1: String?
+ p1: String?,
) {
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/AuthenticationViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/AuthenticationViewModel.kt
index 47954c2cd..385f7029f 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/AuthenticationViewModel.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/AuthenticationViewModel.kt
@@ -1,160 +1,156 @@
package com.habitrpg.android.habitica.ui.viewmodels
-import android.accounts.AccountManager
+
import android.app.Activity
-import android.content.ActivityNotFoundException
-import android.content.Intent
+import android.content.Context
import android.content.SharedPreferences
import android.os.Build
-import androidx.activity.result.ActivityResultLauncher
+import android.util.Log
import androidx.core.content.edit
-import com.google.android.gms.auth.GoogleAuthException
-import com.google.android.gms.auth.GoogleAuthUtil
-import com.google.android.gms.auth.GooglePlayServicesAvailabilityException
-import com.google.android.gms.auth.UserRecoverableAuthException
-import com.google.android.gms.common.ConnectionResult
-import com.google.android.gms.common.GoogleApiAvailability
-import com.google.android.gms.common.GooglePlayServicesUtil
+import androidx.credentials.CredentialManager
+import androidx.credentials.CustomCredential
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.android.gms.auth.api.identity.AuthorizationRequest
+import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.common.Scopes
-import com.habitrpg.android.habitica.R
+import com.google.android.gms.common.api.Scope
+import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
+import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
+import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
+import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.UserRepository
-import com.habitrpg.android.habitica.extensions.addCloseButton
+import com.habitrpg.android.habitica.extensions.AuthenticationErrors
import com.habitrpg.android.habitica.helpers.Analytics
+import com.habitrpg.android.habitica.helpers.AnalyticsTarget
+import com.habitrpg.android.habitica.helpers.AppConfigManager
+import com.habitrpg.android.habitica.helpers.EventCategory
+import com.habitrpg.android.habitica.helpers.HitType
import com.habitrpg.android.habitica.modules.AuthenticationHandler
-import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.common.habitica.api.HostConfig
+import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.helpers.KeyHelper
import com.habitrpg.common.habitica.helpers.launchCatching
import com.habitrpg.common.habitica.models.auth.UserAuthResponse
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.tasks.await
import javax.inject.Inject
-class AuthenticationViewModel
-@Inject
-constructor(
+@HiltViewModel
+class AuthenticationViewModel @Inject constructor(
val apiClient: ApiClient,
val userRepository: UserRepository,
val sharedPrefs: SharedPreferences,
val authenticationHandler: AuthenticationHandler,
+ val configManager: AppConfigManager,
val hostConfig: HostConfig,
- private val keyHelper: KeyHelper?
-) {
- var googleEmail: String? = null
+ private val keyHelper: KeyHelper?,
+) : ViewModel() {
+ private val _showAuthProgress = MutableStateFlow(false)
+ val showAuthProgress: Flow = _showAuthProgress
+ val isRegistering = MutableStateFlow(false)
+ private val _authenticationError: MutableSharedFlow = MutableSharedFlow()
+ val authenticationError: Flow = _authenticationError
+ .onEach { _showAuthProgress.value = false }
+ private val _authenticationSuccess = MutableStateFlow(null)
+ val authenticationSuccess: Flow = _authenticationSuccess
+ .onEach { _showAuthProgress.value = false }
- fun handleGoogleLogin(
- activity: Activity,
- pickAccountResult: ActivityResultLauncher
- ) {
- if (!checkPlayServices(activity)) {
+ fun validateInputs(
+ username: String,
+ password: String,
+ email: String? = null,
+ confirmPassword: String? = null,
+ ): AuthenticationErrors? {
+ if (username.isBlank() || password.isBlank()) {
+ return AuthenticationErrors.MISSING_FIELDS
+ }
+ if (isRegistering.value) {
+ if (email.isNullOrBlank()) {
+ return AuthenticationErrors.MISSING_FIELDS
+ }
+ if (password.length < configManager.minimumPasswordLength()) {
+ return AuthenticationErrors.PASSWORD_TOO_SHORT.apply {
+ minPasswordLength = configManager.minimumPasswordLength()
+ }
+ }
+ if (password != confirmPassword) {
+ return AuthenticationErrors.PASSWORD_MISMATCH
+ }
+ }
+ return null
+ }
+
+ fun login(username: String, password: String) {
+ _showAuthProgress.value = true
+ viewModelScope.launch {
+ try {
+ val response = apiClient.connectUser(username, password)
+ handleAuthResponse(response)
+ } catch (e: Exception) {
+ authenticationError()
+ Analytics.logException(e)
+ }
+ }
+ }
+
+ fun register(username: String, email: String, password: String, confirmPassword: String) {
+ _showAuthProgress.value = true
+ viewModelScope.launch {
+ try {
+ val response = apiClient.registerUser(username, email, password, confirmPassword)
+ handleAuthResponse(response)
+ } catch (e: Exception) {
+ authenticationError()
+ Analytics.logException(e)
+ }
+ }
+ }
+
+ suspend fun removeSocialAuth(network: String) {
+ apiClient.disconnectSocial(network)
+ userRepository.retrieveUser(true, true)
+ }
+
+ private fun authenticationError(error: AuthenticationErrors? = null) {
+ viewModelScope.launch { _authenticationError.emit(error) }
+ }
+
+ private fun handleAuthResponse(response: UserAuthResponse?) {
+ if (response == null) {
+ authenticationError()
return
}
- val accountTypes = arrayOf("com.google")
- val intent =
- AccountManager.newChooseAccountIntent(
- null,
- null,
- accountTypes,
- true,
- null,
- null,
- null,
- null
- )
try {
- pickAccountResult.launch(intent)
- } catch (e: ActivityNotFoundException) {
- val alert = HabiticaAlertDialog(activity)
- alert.setTitle(R.string.authentication_error_title)
- alert.setMessage(R.string.google_services_missing)
- alert.addCloseButton()
- alert.show()
- }
- }
-
- fun handleGoogleLoginResult(
- activity: Activity,
- recoverFromPlayServicesErrorResult: ActivityResultLauncher?,
- onSuccess: (Boolean) -> Unit
- ) {
- val scopesString = Scopes.PROFILE + " " + Scopes.EMAIL
- val scopes = "oauth2:$scopesString"
- var newUser: Boolean
- CoroutineScope(Dispatchers.IO).launchCatching({ throwable ->
- if (recoverFromPlayServicesErrorResult == null) return@launchCatching
- if (throwable is GoogleAuthException) {
- handleGoogleAuthException(
- throwable,
- activity,
- recoverFromPlayServicesErrorResult
- )
- }
- }) {
- val token = GoogleAuthUtil.getToken(activity, googleEmail ?: "", scopes)
- val response =
- apiClient.connectSocial("google", googleEmail ?: "", token) ?: return@launchCatching
- newUser = response.newUser
- handleAuthResponse(response)
- onSuccess(newUser)
- }
- }
-
- private fun handleGoogleAuthException(
- e: Exception,
- activity: Activity,
- recoverFromPlayServicesErrorResult: ActivityResultLauncher
- ) {
- if (e is GooglePlayServicesAvailabilityException) {
- GoogleApiAvailability.getInstance()
- GooglePlayServicesUtil.showErrorDialogFragment(
- e.connectionStatusCode,
- activity,
- null,
- REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR
- ) {
- }
- return
- } else if (e is UserRecoverableAuthException) {
- // Unable to authenticate, such as when the user has not yet granted
- // the app access to the account, but the user can fix this.
- // Forward the user to an activity in Google Play services.
- if (!activity.isFinishing) {
- val intent = e.intent ?: return
- recoverFromPlayServicesErrorResult.launch(intent)
- }
- }
- }
-
- private fun checkPlayServices(activity: Activity): Boolean {
- val googleAPI = GoogleApiAvailability.getInstance()
- val result = googleAPI.isGooglePlayServicesAvailable(activity)
- if (result != ConnectionResult.SUCCESS) {
- if (googleAPI.isUserResolvableError(result)) {
- googleAPI.getErrorDialog(
- activity,
- result,
- PLAY_SERVICES_RESOLUTION_REQUEST
- )?.show()
- }
- return false
- }
-
- return true
- }
-
- fun handleAuthResponse(userAuthResponse: UserAuthResponse) {
- try {
- saveTokens(userAuthResponse.apiToken, userAuthResponse.id)
+ saveTokens(response.apiToken, response.id)
} catch (e: Exception) {
Analytics.logException(e)
}
+
+ if (isRegistering.value) {
+ Analytics.sendEvent("user_registered", EventCategory.BEHAVIOUR, HitType.EVENT, target = AnalyticsTarget.FIREBASE)
+ } else {
+ Analytics.sendEvent("login", EventCategory.BEHAVIOUR, HitType.EVENT)
+ }
+ viewModelScope.launch(ExceptionHandler.coroutine()) {
+ userRepository.retrieveUser(true, true)
+ _authenticationSuccess.value = isRegistering.value
+ }
}
@Throws(Exception::class)
private fun saveTokens(
api: String,
- user: String
+ user: String,
) {
this.apiClient.updateAuthenticationCredentials(user, api)
authenticationHandler.updateUserID(user)
@@ -173,14 +169,69 @@ constructor(
if ((encryptedKey?.length ?: 0) > 5) {
putString(user, encryptedKey)
} else {
- // Something might have gone wrong with encryption, so fall back to this.
putString("APIToken", api)
}
}
}
- companion object {
- private const val REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR = 1001
- private const val PLAY_SERVICES_RESOLUTION_REQUEST = 9000
+ fun startGoogleAuth(context: Context) {
+ val googleIdOption = GetSignInWithGoogleOption.Builder(BuildConfig.GOOGLE_AUTH_CLIENT_ID)
+ .build()
+ val request = GetCredentialRequest.Builder()
+ .addCredentialOption(googleIdOption)
+ .build()
+
+ viewModelScope.launch {
+ try {
+ val result = CredentialManager.create(context).getCredential(
+ request = request,
+ context = context,
+ )
+ handleSignIn(context, result)
+ } catch (e: GetCredentialException) {
+ authenticationError(AuthenticationErrors.GET_CREDENTIALS_ERROR)
+ Log.e("AuthenticationViewModel", "Get Credential Exception", e)
+ }
+ }
+ }
+
+ private suspend fun handleSignIn(context: Context, result: GetCredentialResponse) {
+ val credential = result.credential
+
+ when (credential) {
+ is CustomCredential -> {
+ if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
+ try {
+ val googleIdTokenCredential = GoogleIdTokenCredential
+ .createFrom(credential.data)
+ val authorizationRequest = AuthorizationRequest.Builder()
+ .requestOfflineAccess(BuildConfig.GOOGLE_AUTH_CLIENT_ID)
+ .setRequestedScopes(
+ listOf(
+ Scope(Scopes.PROFILE),
+ Scope(Scopes.EMAIL),
+ )
+ )
+ .build()
+ val result = Identity.getAuthorizationClient(context)
+ .authorize(authorizationRequest).await()
+ if (result != null && result.accessToken != null) {
+ val response = result.accessToken?.let { apiClient.connectSocial("google", googleIdTokenCredential.id, it) }
+ handleAuthResponse(response)
+ }
+ } catch (e: GoogleIdTokenParsingException) {
+ authenticationError(AuthenticationErrors.INVALID_CREDENTIALS)
+ Log.e("AuthenticationViewModel", "Received an invalid google id token response", e)
+ }
+ } else {
+ authenticationError(AuthenticationErrors.INVALID_CREDENTIALS)
+ Log.e("AuthenticationViewModel", "Unexpected type of credential")
+ }
+ }
+ else -> {
+ authenticationError(AuthenticationErrors.INVALID_CREDENTIALS)
+ Log.e("AuthenticationViewModel", "Unexpected type of credential")
+ }
+ }
}
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/progress/HabiticaPullRefreshIndicator.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/progress/HabiticaPullRefreshIndicator.kt
index 0a501518a..00db3b94f 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/progress/HabiticaPullRefreshIndicator.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/progress/HabiticaPullRefreshIndicator.kt
@@ -52,12 +52,12 @@ fun HabiticaPullRefreshIndicator(
color = backgroundColor
) {
AnimatedVisibility(
- visible = isRefreshing || state.progress > 0f,
+ visible = isRefreshing || state.distanceFraction > 0f,
enter = fadeIn(),
exit = fadeOut()
) {
HabiticaCircularProgressView(
- partialDisplay = if (isRefreshing) 1f else state.progress,
+ partialDisplay = if (isRefreshing) 1f else state.distanceFraction,
animate = isRefreshing,
indicatorSize = 40.dp,
strokeWidth = 6.dp,