Issue #418: Basic Firefox Accounts integration
This patch integrates relevant a-c components, and updates the current Preferences UI to allow signing-in and displaying basic account information (email, displayName). Currently there's no Accounts UI, and so singing out or otherwise interacting with the account isn't possible.master
parent
9a9740bfd3
commit
46789f4c62
|
@ -178,6 +178,7 @@ dependencies {
|
||||||
implementation Deps.mozilla_browser_storage_sync
|
implementation Deps.mozilla_browser_storage_sync
|
||||||
implementation Deps.mozilla_browser_toolbar
|
implementation Deps.mozilla_browser_toolbar
|
||||||
|
|
||||||
|
implementation Deps.mozilla_feature_accounts
|
||||||
implementation Deps.mozilla_feature_awesomebar
|
implementation Deps.mozilla_feature_awesomebar
|
||||||
implementation Deps.mozilla_feature_contextmenu
|
implementation Deps.mozilla_feature_contextmenu
|
||||||
implementation Deps.mozilla_feature_customtabs
|
implementation Deps.mozilla_feature_customtabs
|
||||||
|
@ -190,6 +191,7 @@ dependencies {
|
||||||
implementation Deps.mozilla_feature_findinpage
|
implementation Deps.mozilla_feature_findinpage
|
||||||
implementation Deps.mozilla_feature_session_bundling
|
implementation Deps.mozilla_feature_session_bundling
|
||||||
|
|
||||||
|
implementation Deps.mozilla_service_firefox_accounts
|
||||||
implementation Deps.mozilla_service_fretboard
|
implementation Deps.mozilla_service_fretboard
|
||||||
implementation Deps.mozilla_service_glean
|
implementation Deps.mozilla_service_glean
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import mozilla.components.browser.errorpages.ErrorPages
|
||||||
import mozilla.components.browser.errorpages.ErrorType
|
import mozilla.components.browser.errorpages.ErrorType
|
||||||
import mozilla.components.concept.engine.EngineSession
|
import mozilla.components.concept.engine.EngineSession
|
||||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.settings.AboutPage
|
import org.mozilla.fenix.settings.AboutPage
|
||||||
import org.mozilla.fenix.settings.SettingsFragment
|
import org.mozilla.fenix.settings.SettingsFragment
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
||||||
return RequestInterceptor.InterceptionResponse.Content(page, encoding = base64)
|
return RequestInterceptor.InterceptionResponse.Content(page, encoding = base64)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> context.components.services.accountsAuthFeature.interceptor.onLoadRequest(session, uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* 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
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.service.fxa.Config
|
||||||
|
import mozilla.components.service.fxa.FxaAccountManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component group for background services. These are the components that need to be accessed from within a
|
||||||
|
* background worker.
|
||||||
|
*/
|
||||||
|
class BackgroundServices(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val CLIENT_ID = "a2270f727f45f648"
|
||||||
|
const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID"
|
||||||
|
const val SUCCESS_PATH = "connect_another_device?showSuccessMessage=true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is slightly messy - here we need to know the union of all "scopes"
|
||||||
|
// needed by components which rely on FxA integration. If this list
|
||||||
|
// grows too far we probably want to find a way to determine the set
|
||||||
|
// at runtime.
|
||||||
|
private val scopes: Array<String> = arrayOf("profile", "https://identity.mozilla.com/apps/oldsync")
|
||||||
|
private val config = Config.release(CLIENT_ID, REDIRECT_URL)
|
||||||
|
|
||||||
|
val accountManager = FxaAccountManager(context, config, scopes).also { it.init() }
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import android.content.Context
|
||||||
* Provides access to all components.
|
* Provides access to all components.
|
||||||
*/
|
*/
|
||||||
class Components(private val context: Context) {
|
class Components(private val context: Context) {
|
||||||
|
val backgroundServices by lazy { BackgroundServices(context) }
|
||||||
|
val services by lazy { Services(backgroundServices.accountManager, useCases.tabsUseCases) }
|
||||||
val core by lazy { Core(context) }
|
val core by lazy { Core(context) }
|
||||||
val search by lazy { Search(context) }
|
val search by lazy { Search(context) }
|
||||||
val useCases by lazy { UseCases(context, core.sessionManager, search.searchEngineManager) }
|
val useCases by lazy { UseCases(context, core.sessionManager, search.searchEngineManager) }
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* 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
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.components
|
||||||
|
|
||||||
|
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
|
||||||
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
|
import mozilla.components.service.fxa.FxaAccountManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component group which encapsulates foreground-friendly services.
|
||||||
|
*/
|
||||||
|
class Services(
|
||||||
|
private val accountManager: FxaAccountManager,
|
||||||
|
private val tabsUseCases: TabsUseCases
|
||||||
|
) {
|
||||||
|
val accountsAuthFeature by lazy {
|
||||||
|
FirefoxAccountsAuthFeature(
|
||||||
|
accountManager,
|
||||||
|
tabsUseCases,
|
||||||
|
redirectUrl = BackgroundServices.REDIRECT_URL,
|
||||||
|
successPath = BackgroundServices.SUCCESS_PATH
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,10 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import mozilla.components.service.fxa.AccountObserver
|
||||||
|
import mozilla.components.service.fxa.FirefoxAccountShaped
|
||||||
|
import mozilla.components.service.fxa.FxaUnauthorizedException
|
||||||
|
import mozilla.components.service.fxa.Profile
|
||||||
import mozilla.components.support.ktx.android.graphics.toDataUri
|
import mozilla.components.support.ktx.android.graphics.toDataUri
|
||||||
import org.mozilla.fenix.BuildConfig
|
import org.mozilla.fenix.BuildConfig
|
||||||
import org.mozilla.fenix.FenixApplication
|
import org.mozilla.fenix.FenixApplication
|
||||||
|
@ -38,10 +42,11 @@ import org.mozilla.fenix.R.string.pref_key_accessibility
|
||||||
import org.mozilla.fenix.R.string.pref_key_language
|
import org.mozilla.fenix.R.string.pref_key_language
|
||||||
import org.mozilla.fenix.R.string.pref_key_data_choices
|
import org.mozilla.fenix.R.string.pref_key_data_choices
|
||||||
import org.mozilla.fenix.R.string.pref_key_about
|
import org.mozilla.fenix.R.string.pref_key_about
|
||||||
|
import org.mozilla.fenix.R.string.pref_key_sign_in
|
||||||
|
import org.mozilla.fenix.R.string.pref_key_account
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope {
|
class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope, AccountObserver {
|
||||||
|
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
get() = Dispatchers.Main + job
|
get() = Dispatchers.Main + job
|
||||||
|
@ -49,6 +54,7 @@ class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
job = Job()
|
job = Job()
|
||||||
|
setupAccountUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
@ -103,6 +109,46 @@ class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupAccountUI() {
|
||||||
|
val signIn = context?.getPreferenceKey(pref_key_sign_in)
|
||||||
|
val firefoxAccountKey = context?.getPreferenceKey(pref_key_account)
|
||||||
|
|
||||||
|
val preferenceSignIn = findPreference<Preference>(signIn)
|
||||||
|
val preferenceFirefoxAccount = findPreference<Preference>(firefoxAccountKey)
|
||||||
|
|
||||||
|
preferenceSignIn.isVisible = true
|
||||||
|
preferenceFirefoxAccount.isVisible = false
|
||||||
|
preferenceSignIn.onPreferenceClickListener = getClickListenerForSignIn()
|
||||||
|
|
||||||
|
val accountManager = requireComponents.backgroundServices.accountManager
|
||||||
|
// Observe account changes to keep the UI up-to-date.
|
||||||
|
accountManager.register(this, owner = this)
|
||||||
|
|
||||||
|
// TODO an authenticated state will mark 'preferenceSignIn' as invisible; currently that behaviour is non-ideal:
|
||||||
|
// a "sign in" UI will be displayed at first, and then quickly animate away.
|
||||||
|
// Ideally we don't want it to be displayed at all.
|
||||||
|
accountManager.authenticatedAccount()?.let { setIsAuthenticated(it) }
|
||||||
|
accountManager.accountProfile()?.let { updateAccountProfile(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getClickListenerForSignIn(): OnPreferenceClickListener {
|
||||||
|
return OnPreferenceClickListener {
|
||||||
|
requireComponents.services.accountsAuthFeature.beginAuthentication()
|
||||||
|
// TODO the "back button" behaviour here is pretty poor. The sign-in web content populates session history,
|
||||||
|
// so pressing "back" after signing in won't take us back into the settings screen, but rather up the
|
||||||
|
// session history stack.
|
||||||
|
// We could auto-close this tab once we get to the end of the authentication process?
|
||||||
|
// Via an interceptor, perhaps.
|
||||||
|
view?.let {
|
||||||
|
Navigation.findNavController(it)
|
||||||
|
.navigate(
|
||||||
|
SettingsFragmentDirections.actionGlobalBrowser(null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shrinks the wordmark resolution on first run to ensure About Page loads quickly
|
* Shrinks the wordmark resolution on first run to ensure About Page loads quickly
|
||||||
*/
|
*/
|
||||||
|
@ -189,6 +235,53 @@ class SettingsFragment : PreferenceFragmentCompat(), CoroutineScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- AccountObserver interfaces ---
|
||||||
|
override fun onAuthenticated(account: FirefoxAccountShaped) {
|
||||||
|
setIsAuthenticated(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(error: Exception) {
|
||||||
|
// TODO we could display some error states in this UI.
|
||||||
|
when (error) {
|
||||||
|
is FxaUnauthorizedException -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoggedOut() {
|
||||||
|
setIsLoggedOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProfileUpdated(profile: Profile) {
|
||||||
|
updateAccountProfile(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Account UI helpers ---
|
||||||
|
private fun setIsAuthenticated(account: FirefoxAccountShaped) {
|
||||||
|
val preferenceSignIn = findPreference<Preference>(context?.getPreferenceKey(pref_key_sign_in))
|
||||||
|
val preferenceFirefoxAccount = findPreference<Preference>(context?.getPreferenceKey(pref_key_account))
|
||||||
|
|
||||||
|
preferenceSignIn.isVisible = false
|
||||||
|
preferenceSignIn.onPreferenceClickListener = null
|
||||||
|
preferenceFirefoxAccount.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setIsLoggedOut() {
|
||||||
|
val preferenceSignIn = findPreference<Preference>(context?.getPreferenceKey(pref_key_sign_in))
|
||||||
|
val preferenceFirefoxAccount = findPreference<Preference>(context?.getPreferenceKey(pref_key_account))
|
||||||
|
|
||||||
|
preferenceSignIn.isVisible = true
|
||||||
|
|
||||||
|
// TODO this isn't quite right, as we'll have an "Account" preference category title still visible on the screen
|
||||||
|
preferenceFirefoxAccount.isVisible = false
|
||||||
|
preferenceSignIn.onPreferenceClickListener = getClickListenerForSignIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateAccountProfile(profile: Profile) {
|
||||||
|
val preferenceFirefoxAccount = findPreference<Preference>(context?.getPreferenceKey(pref_key_account))
|
||||||
|
preferenceFirefoxAccount.title = profile.displayName.orEmpty()
|
||||||
|
preferenceFirefoxAccount.summary = profile.email.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val wordmarkScalingFactor = 2
|
const val wordmarkScalingFactor = 2
|
||||||
const val wordmarkPath = "wordmark.b64"
|
const val wordmarkPath = "wordmark.b64"
|
||||||
|
|
|
@ -102,6 +102,8 @@
|
||||||
<!-- Preference description for banner about signing in -->
|
<!-- Preference description for banner about signing in -->
|
||||||
<string name="preferences_sign_in_description">Get your tabs, bookmarks, logins,
|
<string name="preferences_sign_in_description">Get your tabs, bookmarks, logins,
|
||||||
history and more on all your devices</string>
|
history and more on all your devices</string>
|
||||||
|
<!-- Preference shown instead of account display name while account profile information isn't available yet. -->
|
||||||
|
<string name="preferences_account_default_name">Firefox Account</string>
|
||||||
<!-- Preference for language -->
|
<!-- Preference for language -->
|
||||||
<string name="preferences_language">Language</string>
|
<string name="preferences_language">Language</string>
|
||||||
<!-- Preference for data choices -->
|
<!-- Preference for data choices -->
|
||||||
|
|
|
@ -18,8 +18,7 @@
|
||||||
<androidx.preference.Preference
|
<androidx.preference.Preference
|
||||||
android:icon="@drawable/ic_shortcuts"
|
android:icon="@drawable/ic_shortcuts"
|
||||||
android:key="@string/pref_key_account"
|
android:key="@string/pref_key_account"
|
||||||
android:summary="user@email.com"
|
android:title="@string/preferences_account_default_name" />
|
||||||
android:title="User Name" />
|
|
||||||
</androidx.preference.PreferenceCategory>
|
</androidx.preference.PreferenceCategory>
|
||||||
|
|
||||||
<androidx.preference.PreferenceCategory
|
<androidx.preference.PreferenceCategory
|
||||||
|
|
|
@ -74,6 +74,7 @@ object Deps {
|
||||||
const val mozilla_browser_errorpages = "org.mozilla.components:browser-errorpages:${Versions.mozilla_android_components}"
|
const val mozilla_browser_errorpages = "org.mozilla.components:browser-errorpages:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_browser_storage_sync = "org.mozilla.components:browser-storage-sync:${Versions.mozilla_android_components}"
|
const val mozilla_browser_storage_sync = "org.mozilla.components:browser-storage-sync:${Versions.mozilla_android_components}"
|
||||||
|
|
||||||
|
const val mozilla_feature_accounts = "org.mozilla.components:feature-accounts:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_feature_awesomebar = "org.mozilla.components:feature-awesomebar:${Versions.mozilla_android_components}"
|
const val mozilla_feature_awesomebar = "org.mozilla.components:feature-awesomebar:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_feature_contextmenu = "org.mozilla.components:feature-contextmenu:${Versions.mozilla_android_components}"
|
const val mozilla_feature_contextmenu = "org.mozilla.components:feature-contextmenu:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_feature_customtabs = "org.mozilla.components:feature-customtabs:${Versions.mozilla_android_components}"
|
const val mozilla_feature_customtabs = "org.mozilla.components:feature-customtabs:${Versions.mozilla_android_components}"
|
||||||
|
@ -89,6 +90,7 @@ object Deps {
|
||||||
const val mozilla_feature_findinpage = "org.mozilla.components:feature-findinpage:${Versions.mozilla_android_components}"
|
const val mozilla_feature_findinpage = "org.mozilla.components:feature-findinpage:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_feature_session_bundling = "org.mozilla.components:feature-session-bundling:${Versions.mozilla_android_components}"
|
const val mozilla_feature_session_bundling = "org.mozilla.components:feature-session-bundling:${Versions.mozilla_android_components}"
|
||||||
|
|
||||||
|
const val mozilla_service_firefox_accounts = "org.mozilla.components:service-firefox-accounts:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_service_fretboard = "org.mozilla.components:service-fretboard:${Versions.mozilla_android_components}"
|
const val mozilla_service_fretboard = "org.mozilla.components:service-fretboard:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_service_glean = "org.mozilla.components:service-glean:${Versions.mozilla_android_components}"
|
const val mozilla_service_glean = "org.mozilla.components:service-glean:${Versions.mozilla_android_components}"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue