Add an Online Accounts page

GNOME Online Accounts is a daemon plus a D-Bus API, and the daemon
already runs in this session -- gvfs activates it, and all four accounts
on this machine work without gnome-shell involved anywhere. Only the
PANEL was GNOME's. The accounts themselves are ordinary D-Bus objects
that anything may read and modify.

So everything except the initial sign-in is now native: the account
list, per-service toggles for mail, calendar, contacts, files, photos,
music and chat, and removal. That is the whole Online Accounts panel
apart from one OAuth handshake.

Signing in is the exception, and only for OAuth providers. The daemon's
AddAccount takes credentials as an argument -- it stores them, it does
not obtain them -- and the code that runs Google's OAuth exchange lives
in libgoa-backend, which Fedora ships without a GIR binding, so it is
reachable from C only. Reimplementing it would mean our own Google
client credentials. That step is handed to GNOME's panel and the page
says so, because a hand-off the user does not expect reads as a bug.
Password-based providers (Nextcloud, IMAP, WebDAV) could be added
natively later; their credential keys are known now.

Accounts needing re-authentication are surfaced first, which turned up
something immediately: both Google accounts on this machine report
attention_needed, meaning their tokens have expired and they have
stopped syncing. GOA has known that all along and nothing outside its
own panel ever said so.

Every write re-reads the account list rather than assuming it landed.
GOA can refuse, and a toggle that springs back is the honest outcome.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-18 10:34:41 -04:00
parent e4409ed6aa
commit 52d896b054
7 changed files with 367 additions and 1 deletions
@@ -0,0 +1,122 @@
// Online Accounts.
//
// GNOME's panel of the same name, except for one step. The accounts are GNOME
// Online Accounts objects on D-Bus, the daemon already runs here, and listing,
// per-service toggles and removal are all done natively on this page.
//
// Signing in to an OAuth provider is handed to GNOME's panel, because the
// credentials are OAuth tokens and the code that obtains them ships without a
// scriptable binding. That is stated on the page rather than hidden behind a
// button that looks native, because a hand-off the user does not expect reads
// as a bug.
//
// Accounts needing attention are surfaced first. GOA knows when a token has
// expired and nothing outside its own panel says so, which is how an account
// quietly stops syncing for weeks.
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
title: "Online Accounts"
lede: OnlineAccounts.attentionCount > 0
? OnlineAccounts.attentionCount + (OnlineAccounts.attentionCount === 1
? " account needs you to sign in again."
: " accounts need you to sign in again.")
: "Accounts your mail, calendar, contacts, and files come from."
Component.onCompleted: if (!OnlineAccounts.scanned) OnlineAccounts.refresh()
SettingsCard {
visible: !OnlineAccounts.available
title: "Accounts unavailable"
subtitle: OnlineAccounts.lastError
}
SettingsCard {
visible: OnlineAccounts.scanned && OnlineAccounts.available && OnlineAccounts.accounts.length === 0
title: "No accounts yet"
subtitle: "Adding one lets mail, calendar, contacts, and file managers share a single sign-in."
ActionRow {
label: "Add an account"
detail: "Google, Nextcloud, Microsoft Exchange, IMAP, WebDAV, and Kerberos"
action: "Add account"
divider: false
onTriggered: SystemSettings.openGnomePanel("online-accounts")
}
}
Repeater {
model: OnlineAccounts.accounts
SettingsCard {
id: accountCard
required property var modelData
title: accountCard.modelData.identity || accountCard.modelData.providerName
subtitle: accountCard.modelData.needsAttention
? accountCard.modelData.providerName + " · sign-in expired, so this account has stopped syncing"
: accountCard.modelData.providerName
// Only when it is true, because it is the one thing on this page
// that needs acting on.
ActionRow {
visible: accountCard.modelData.needsAttention
label: "Sign in again"
detail: "Re-authorising uses the provider's own sign-in page, which GNOME's panel hosts"
action: "Sign in"
onTriggered: SystemSettings.openGnomePanel("online-accounts")
}
Repeater {
model: accountCard.modelData.services
SettingRow {
id: serviceRow
required property var modelData
required property int index
label: serviceRow.modelData.label
detail: serviceRow.modelData.enabled
? "Applications using " + serviceRow.modelData.label.toLowerCase() + " can see this account"
: "Hidden from applications"
controlWidth: 48
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: serviceRow.modelData.enabled
onToggled: value => OnlineAccounts.setService(
accountCard.modelData.path, serviceRow.modelData.key, value)
}
}
}
ActionRow {
label: "Remove this account"
detail: "Signs out and removes it from every application that was using it"
action: "Remove"
divider: false
onTriggered: OnlineAccounts.remove(accountCard.modelData.path)
}
}
}
SettingsCard {
visible: OnlineAccounts.available && OnlineAccounts.accounts.length > 0
title: "Add another account"
subtitle: "Signing in happens on the provider's own page. GNOME's panel hosts that step; everything after it is managed here."
ActionRow {
label: "Add an account"
detail: "Google, Nextcloud, Microsoft Exchange, IMAP, WebDAV, and Kerberos"
action: "Add account"
divider: false
onTriggered: SystemSettings.openGnomePanel("online-accounts")
}
}
}
@@ -102,6 +102,7 @@ Rectangle {
case "mouse": return mousePage;
case "privacy": return privacyPage;
case "region": return regionPage;
case "accounts": return onlineAccountsPage;
case "accessibility": return accessibilityPage;
case "power": return powerPage;
case "datetime": return dateTimePage;
@@ -159,6 +160,7 @@ Rectangle {
Component { id: mousePage; MousePage {} }
Component { id: privacyPage; PrivacyPage {} }
Component { id: regionPage; RegionPage {} }
Component { id: onlineAccountsPage; OnlineAccountsPage {} }
Component { id: servicesPage; ServicesPage {} }
Component { id: aboutPage; AboutPage {} }
@@ -35,6 +35,7 @@ Rectangle {
{ page: "mouse", label: "Mouse & Touchpad", icon: "\u{F037D}" },
{ page: "privacy", label: "Privacy & Security", icon: "\u{F0483}" },
{ page: "region", label: "Region & Language", icon: "\u{F0AC2}" },
{ page: "accounts", label: "Online Accounts", icon: "\u{F0004}" },
{ page: "accessibility", label: "Accessibility", icon: "\u{F0208}" },
{ page: "power", label: "Power & Lock", icon: "\u{F0425}" },
{ page: "datetime", label: "Date & Time", icon: "\u{F0954}" },
@@ -51,3 +51,4 @@ TextEntryRow 1.0 TextEntryRow.qml
PrivacyPage 1.0 PrivacyPage.qml
RegionPage 1.0 RegionPage.qml
SearchPicker 1.0 SearchPicker.qml
OnlineAccountsPage 1.0 OnlineAccountsPage.qml