Revision control
Copy as Markdown
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
package org.mozilla.tv.firefox.fxa
import android.net.Uri
import androidx.fragment.app.FragmentManager
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.AuthType
import mozilla.components.service.fxa.FxaAuthData
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.toAuthType
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.tv.firefox.ScreenController
import org.mozilla.tv.firefox.session.SessionRepo
import org.mozilla.tv.firefox.telemetry.SentryIntegration
private val logger = Logger("FxaLoginUseCase")
/**
* Manages login with a Firefox Account (FxA).
*
* To begin the login flow, we call [beginLogin]: this loads the login page into the engine view. This login page
* does the heavy lifting: most error states (e.g. invalid credentials) are handled in the engine view and are not
* reported to us. On success, our observer in this class transmits the results from the engine view back to the FxA
* library. After that, control of FxA state is returned to the [fxaRepo].
*
* This class is designed to allows repos to interact with each other without references to each other,
* allowing each repo to operate independently of other repos, simplifying state.
*
* FxaRepo SessionRepo
* | |
* ------------
* |
* FxaLoginUseCase
*/
class FxaLoginUseCase(
private val fxaRepo: FxaRepo,
private val sessionRepo: SessionRepo,
private val screenController: ScreenController,
private val sentryIntegration: SentryIntegration = SentryIntegration
) {
private val _onLoginSuccess = PublishSubject.create<Unit>()
val onLoginSuccess: Observable<Unit> = _onLoginSuccess.hide()
/**
* Opens the browser screen and loads the FxA login URL to begin the login flow.
*/
fun beginLogin(fragmentManager: FragmentManager) {
// TODO: should we throw an error if we're already authenticated when this is called?
GlobalScope.launch(Dispatchers.Main) { // main thread: we modify UI state.
// a-c#3713: this await will never resume if the user is already logged in.
val loginUri = fxaRepo.beginLoginInternalAsync().await()
// This may be null when the FxA library fails to do things internally, mostly likely due to network issues.
if (loginUri == null) {
sentryIntegration.captureAndLogError(
logger, IllegalStateException("beginAuthenticationAsync returned null loginUri"))
return@launch
}
// TODO: if user is already signing in, we should consider not reloading the page.
screenController.showBrowserScreenForUrl(fragmentManager, loginUri)
}
}
init {
// @Suppress cannot be used on an initializer so we call a helper method.
attachFxaLoginSuccessObserver()
}
@Suppress("CheckResult") // no need to dispose: sessionRepo is active for the duration of the app.
private fun attachFxaLoginSuccessObserver() {
fun isLoginSuccessUri(uri: String): Boolean = uri.startsWith(FxaRepo.REDIRECT_URI)
fun extractLoginSuccessKeys(uriStr: String): LoginSuccessKeys? {
val uri = Uri.parse(uriStr)
val authType = uri.getQueryParameter("action").toAuthType()
val code = uri.getQueryParameter("code")
val state = uri.getQueryParameter("state")
return if (code != null && state != null) LoginSuccessKeys(authType = authType, code = code, state = state) else null
}
fun Observable<String>.filterMapLoginSuccessKeys(): Observable<LoginSuccessKeys> =
this.flatMap { url ->
val loginSuccessKeys = extractLoginSuccessKeys(url)
if (loginSuccessKeys != null) {
Observable.just(loginSuccessKeys)
} else {
// Since we received a login success URL, this is never expected. However, since this action is
// controlled by a server, we don't want to crash the app so we log to Sentry instead.
sentryIntegration.captureAndLogError(
logger, IllegalStateException("Received success URI but success keys cannot be found"))
Observable.empty()
}
}
sessionRepo.state
.map { it.currentUrl }
.distinctUntilChanged()
.filter { isLoginSuccessUri(it) }
.filterMapLoginSuccessKeys()
.subscribe { loginSuccessKeys ->
fxaRepo.accountManager.finishAuthenticationAsync(loginSuccessKeys)
_onLoginSuccess.onNext(Unit)
}
}
private data class LoginSuccessKeys(val authType: AuthType, val code: String, val state: String)
private fun FxaAccountManager.finishAuthenticationAsync(keys: LoginSuccessKeys) {
@Suppress("DeferredResultUnused") // We don't care to wait until completion.
finishAuthenticationAsync(FxaAuthData(keys.authType, keys.code, keys.state))
}
}