mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-05-19 20:29:02 +00:00
Refactor login activity and update to new credentials manageer
This commit is contained in:
parent
584b274d85
commit
ed65bcec6b
11 changed files with 427 additions and 487 deletions
|
|
@ -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"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/email"
|
||||
|
|
@ -178,7 +179,7 @@
|
|||
android:autofillHints="newPassword"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/PB_AsyncTask"
|
||||
android:id="@+id/progress_view"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -186,37 +187,22 @@
|
|||
android:layout_gravity="center"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/login_btn"
|
||||
android:id="@+id/submit_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="42dp"
|
||||
android:text="@string/register_btn"
|
||||
android:layout_marginTop="@dimen/spacing_xlarge"
|
||||
style="@style/LoginButton"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_large"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
<Button
|
||||
android:id="@+id/google_login_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/diamond_button_height"
|
||||
android:layout_height="42dp"
|
||||
android:text="@string/login_btn_google"
|
||||
android:layout_marginTop="@dimen/spacing_large"
|
||||
android:drawableStart="@drawable/google_icon"
|
||||
style="@style/LoginButton"/>
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/google_login_progress"
|
||||
style="@style/LoginButtonProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:indicatorColor="@color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/apple_login_button"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -269,4 +255,4 @@
|
|||
android:alpha="0" />
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
|
|
|||
|
|
@ -1234,7 +1234,7 @@
|
|||
<string name="removed_social_auth">Disconnected %s</string>
|
||||
<string name="password_added">Password saved</string>
|
||||
<string name="add_email_and_password">Add Email and Password</string>
|
||||
<string name="password_not_matching">Password needs to be typed correctly twice</string>
|
||||
<string name="password_not_matching">The entered passwords do not match</string>
|
||||
<string name="email_invalid">Invalid Email address</string>
|
||||
<string name="successful_purchase_generic">Purchase successful</string>
|
||||
<string name="starting_objectives">Starting Objectives</string>
|
||||
|
|
@ -1576,6 +1576,9 @@
|
|||
<string name="habitoween" translatable="false">Habitoween</string>
|
||||
<string name="turkey_day">Turkey Day</string>
|
||||
<string name="search_equipment">Search Equipment</string>
|
||||
<string name="auth_get_credentials_error">Error getting credentials for authentication.</string>
|
||||
<string name="auth_invalid_credentials">Received invalid credentials.</string>
|
||||
<string name="auth_unknown_error">Unknown error during authentication.</string>
|
||||
|
||||
<plurals name="you_x_others">
|
||||
<item quantity="zero">You</item>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuthenticationViewModel>()
|
||||
|
||||
@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<String, Any>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
class StableFragment : BaseMainFragment<FragmentViewpagerBinding>() {
|
||||
override var binding: FragmentViewpagerBinding? = null
|
||||
|
||||
private val viewModel: StableViewModel by viewModels()
|
||||
|
||||
override fun createBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
|
|
|
|||
|
|
@ -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<Preference>("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?,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Boolean> = _showAuthProgress
|
||||
val isRegistering = MutableStateFlow(false)
|
||||
private val _authenticationError: MutableSharedFlow<AuthenticationErrors?> = MutableSharedFlow()
|
||||
val authenticationError: Flow<AuthenticationErrors?> = _authenticationError
|
||||
.onEach { _showAuthProgress.value = false }
|
||||
private val _authenticationSuccess = MutableStateFlow<Boolean?>(null)
|
||||
val authenticationSuccess: Flow<Boolean?> = _authenticationSuccess
|
||||
.onEach { _showAuthProgress.value = false }
|
||||
|
||||
fun handleGoogleLogin(
|
||||
activity: Activity,
|
||||
pickAccountResult: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
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<Intent>?,
|
||||
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<Intent>
|
||||
) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue