Skip to content

Commit 9c33497

Browse files
authored
Merge pull request #10388 from nextcloud/backport/10354/stable-34.0
[stable-34.0] Improved file provider switch user experience
2 parents fdf1559 + 488a504 commit 9c33497

23 files changed

Lines changed: 948 additions & 296 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: GPL-2.0-or-later
4+
-->
5+
6+
# TC-0007: Switching the app-level File Provider mode
7+
8+
- **Platform:** macOS
9+
- **Related Issues:** None yet
10+
11+
## Description
12+
13+
The File Provider integration and classic sync folders (FinderSync extension) cannot run at the same time on macOS: as soon as any file provider domain exists, the system deactivates the FinderSync extension. The client therefore offers one app-level File Provider switch in the General settings. Enabling it discards all classic sync folder connections (local files are kept); disabling it removes all file provider domains without recreating classic folders. Both directions require explicit confirmation.
14+
15+
## Preconditions
16+
17+
- A client build with the file provider module (`Nextcloud-vfs-…`) on macOS 14 or later.
18+
- At least one configured account with one or more classic sync folders containing synced files.
19+
20+
## Test Steps
21+
22+
| Step | Action | Expected Result |
23+
|------|--------|-----------------|
24+
| 1 | Open Settings → General | A "Use macOS File Provider Integration" switch is shown with an explanatory text; it is off |
25+
| 2 | Open the account's settings page | The "Classic sync" panel with the folder list is shown; no File Provider panel exists |
26+
| 3 | Turn the File Provider switch on | A confirmation dialog appears, listing the affected accounts and their classic sync folder connections, stating that files stay on disk |
27+
| 4 | Click "Cancel" | Nothing changes: switch stays off, classic folders keep syncing |
28+
| 5 | Turn the switch on again and click "Enable File Provider" | The switch is disabled while setting up; afterwards every account appears in Finder under "Locations", all classic sync folder connections are removed, the local folders and their files remain on disk, FinderSync badges disappear, and the "Classic sync" panel is hidden in every account's settings |
29+
| 6 | Turn the switch off | A confirmation dialog explains that files leave Finder "Locations", unsynced items are preserved, and classic sync folders are not recreated automatically |
30+
| 7 | Click "Disable File Provider" | All file provider domains are removed; no classic folders are created; the "Classic sync" panel (with the "Add Folder Sync Connection" button) reappears in the account settings |
31+
| 8 | Add a new folder sync connection and restart the client | The classic folder syncs again and FinderSync badges work |
32+
33+
## Expected Results
34+
35+
- Classic sync folders and File Provider are never active at the same time.
36+
- No local files are deleted by either switch direction.
37+
- A deployment updating with both configured (or a client killed mid-switch) shows a "Choose how to sync your files" dialog at startup offering "Keep File Provider", "Keep classic sync folders" and "Decide later"; "Decide later" keeps both working, shows a warning banner in the affected account settings, and asks again on the next launch.
38+
- New accounts added while the mode is on automatically get a file provider domain; the account wizard skips the sync options step. With the mode off, the wizard offers classic folder setup only.

resources.qrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@
8080
<file>src/gui/wizard/qml/WizardComboBox.qml</file>
8181
<file>src/gui/wizard/qml/WizardDialogFrame.qml</file>
8282
<file>src/gui/wizard/qml/WizardTextField.qml</file>
83-
<file>src/gui/macOS/ui/FileProviderSettings.qml</file>
8483
<file>src/gui/macOS/ui/FileProviderFileDelegate.qml</file>
8584
<file>src/gui/integration/FileActionsWindow.qml</file>
8685
<file>src/gui/GovernanceLabelsDialog.qml</file>

src/gui/accountsettings.cpp

Lines changed: 106 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -196,16 +196,14 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
196196
connect(connectionSettingsButton, &QPushButton::clicked, this, &AccountSettings::showConnectionSettingsDialog);
197197

198198
_ui->verticalLayout_2->removeWidget(_ui->accountStatusPanel);
199-
_ui->verticalLayout_2->removeWidget(_ui->fileProviderPanel);
200199
_ui->verticalLayout_2->removeWidget(_ui->syncFoldersPanel);
201200
_ui->verticalLayout_2->removeWidget(_ui->connectionSettingsPanel);
202201
_ui->verticalLayout_2->removeWidget(_ui->accountActionsPanel);
203202
_ui->connectionSettingsPanel->hide();
204-
_ui->verticalLayout_2->insertWidget(0, _ui->fileProviderPanel);
205-
_ui->verticalLayout_2->insertWidget(1, _ui->syncFoldersPanel);
206-
_ui->verticalLayout_2->insertWidget(2, _encryptionPanel);
207-
_ui->verticalLayout_2->insertWidget(3, _ui->accountStatusPanel);
208-
_ui->verticalLayout_2->insertWidget(4, _ui->accountActionsPanel);
203+
_ui->verticalLayout_2->insertWidget(0, _ui->syncFoldersPanel);
204+
_ui->verticalLayout_2->insertWidget(1, _encryptionPanel);
205+
_ui->verticalLayout_2->insertWidget(2, _ui->accountStatusPanel);
206+
_ui->verticalLayout_2->insertWidget(3, _ui->accountActionsPanel);
209207

210208
_model->setAccountState(_accountState);
211209
_model->setParent(this);
@@ -215,9 +213,6 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
215213
_ui->syncFoldersPanelContents->setAutoFillBackground(false);
216214
_ui->syncFoldersPanelContents->setAttribute(Qt::WA_StyledBackground, false);
217215
_ui->syncFoldersPanelContents->setContentsMargins(0, 0, 0, 0);
218-
_ui->fileProviderPanelContents->setAutoFillBackground(false);
219-
_ui->fileProviderPanelContents->setAttribute(Qt::WA_StyledBackground, false);
220-
_ui->fileProviderPanelContents->setContentsMargins(0, 0, 0, 0);
221216
_ui->connectionSettingsPanelContents->setAutoFillBackground(false);
222217
_ui->connectionSettingsPanelContents->setAttribute(Qt::WA_StyledBackground, false);
223218
_ui->connectionSettingsPanelContents->setContentsMargins(0, 0, 0, 0);
@@ -244,30 +239,30 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
244239
new ToolTipUpdater(_ui->_folderList);
245240

246241
#if defined(BUILD_FILE_PROVIDER_MODULE)
247-
if (Mac::FileProvider::available()) {
248-
const auto fileProviderPanelContents = _ui->fileProviderPanelContents;
249-
const auto fpSettingsLayout = new QVBoxLayout(fileProviderPanelContents);
250-
const auto fpAccountUserIdAtHost = _accountState->account()->userIdAtHostWithPort();
251-
const auto fpSettingsController = Mac::FileProviderSettingsController::instance();
252-
const auto fpSettingsWidget = fpSettingsController->settingsViewWidget(fpAccountUserIdAtHost, fileProviderPanelContents,
253-
QQuickWidget::SizeRootObjectToView);
254-
fpSettingsLayout->setContentsMargins(0, 0, 0, 0);
255-
fpSettingsLayout->setSpacing(0);
256-
257-
fpSettingsWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
258-
if (const auto fpSettingsWidgetLayout = fpSettingsWidget->layout()) {
259-
fpSettingsWidgetLayout->setContentsMargins(0, 0, 0, 0);
260-
}
261-
fpSettingsLayout->addWidget(fpSettingsWidget, 1);
262-
fileProviderPanelContents->setLayout(fpSettingsLayout);
263-
} else {
264-
// macOS 13 Ventura: the file provider feature is unsupported there.
265-
// This branch can be removed once Ventura is no longer supported.
266-
_ui->fileProviderPanel->setVisible(false);
267-
}
268-
#else
269-
_ui->fileProviderPanel->setVisible(false);
242+
// The File Provider integration is an app-level mode, toggled in the General
243+
// settings. While it is enabled, classic sync folders are unavailable: hide the
244+
// "Classic sync" panel, except when the account still has classic folders
245+
// configured (a conflict the banner asks the user to resolve).
246+
const auto fpSettingsController = Mac::FileProviderSettingsController::instance();
247+
connect(fpSettingsController, &Mac::FileProviderSettingsController::fileProviderModeEnabledChanged,
248+
this, &AccountSettings::updateSyncFoldersPanelVisibility);
249+
connect(fpSettingsController, &Mac::FileProviderSettingsController::fileProviderModeEnabledChanged,
250+
this, &AccountSettings::refreshE2eEncryptionMessage);
251+
// The reconciliation "Keep File Provider" path applies the mode without emitting
252+
// fileProviderModeEnabledChanged (the flag was already on), so also refresh on the
253+
// bulk-apply completion to update a page that is open during the transition.
254+
connect(fpSettingsController, &Mac::FileProviderSettingsController::fileProviderModeApplyFinished,
255+
this, [this](bool, const QStringList &) {
256+
refreshE2eEncryptionMessage();
257+
updateSyncFoldersPanelVisibility();
258+
});
259+
connect(FolderMan::instance(), &FolderMan::folderListChanged,
260+
this, &AccountSettings::updateSyncFoldersPanelVisibility);
261+
connect(_ui->fileProviderConflictResolveButton, &QPushButton::clicked, this, [fpSettingsController] {
262+
fpSettingsController->performStartupReconciliation();
263+
});
270264
#endif
265+
updateSyncFoldersPanelVisibility();
271266

272267
const auto mouseCursorChanger = new MouseCursorChanger(this);
273268
mouseCursorChanger->folderList = _ui->_folderList;
@@ -329,7 +324,10 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
329324
// Connect E2E stuff
330325
if (_accountState->isConnected()) {
331326
setupE2eEncryption();
327+
_e2eEncryptionSetupDone = true;
332328
} else {
329+
// Not connected yet: setupE2eEncryption() (which builds the File-Provider-aware
330+
// message) runs on the first Connected transition in slotAccountStateChanged().
333331
_ui->encryptionMessageLabel->setText(tr("End-to-end encryption has not been initialized on this account."));
334332
}
335333
_ui->encryptionMessageLabel->setTextFormat(Qt::RichText);
@@ -940,6 +938,13 @@ void AccountSettings::slotFolderWizardAccepted()
940938
QStringList() << QLatin1String("/"));
941939
folderMan->scheduleAllFolders();
942940
emit folderChanged();
941+
} else {
942+
// addFolder can refuse (e.g. classic sync folders are unavailable while the
943+
// File Provider integration is enabled). Don't leave the user believing the
944+
// wizard succeeded.
945+
QMessageBox::warning(this, tr("Folder creation failed"),
946+
tr("<p>Could not add the folder sync connection for <i>%1</i>.</p>")
947+
.arg(Utility::escape(QDir::toNativeSeparators(definition.localPath))));
943948
}
944949
}
945950

@@ -1560,6 +1565,15 @@ void AccountSettings::slotAccountStateChanged()
15601565
refreshSelectiveSyncStatus();
15611566

15621567
if (state == AccountState::State::Connected) {
1568+
// Build the encryption message on the first connect if the account was still
1569+
// connecting when this page was constructed — otherwise the panel would be
1570+
// revealed below still showing the not-connected placeholder instead of the
1571+
// File-Provider-aware text. Guarded so e2e initialization is set up only once;
1572+
// File-Provider-mode changes are handled separately via the controller signals.
1573+
if (!_e2eEncryptionSetupDone) {
1574+
setupE2eEncryption();
1575+
_e2eEncryptionSetupDone = true;
1576+
}
15631577
checkClientSideEncryptionState();
15641578
}
15651579
}
@@ -1936,19 +1950,40 @@ QAction *AccountSettings::addActionToEncryptionMessage(const QString &actionTitl
19361950

19371951
void AccountSettings::setupE2eEncryptionMessage()
19381952
{
1939-
#ifdef BUILD_FILE_PROVIDER_MODULE
1940-
const auto encryptionMessage = tr("This account supports end-to-end encryption, but it needs to be set up first.") + QStringLiteral(" ") + tr("The File Provider extension does not support end-to-end encryption yet.");
1941-
#else
1942-
const auto encryptionMessage = tr("This account supports end-to-end encryption, but it needs to be set up first.");
1943-
#endif
1944-
_ui->encryptionMessageLabel->setText(encryptionMessage);
19451953
setEncryptionMessageIcon(Theme::createColorAwareIcon(QStringLiteral(":/client/theme/info.svg")));
19461954
setEncryptionPanelVisible(false);
19471955

1956+
#ifdef BUILD_FILE_PROVIDER_MODULE
1957+
if (Mac::FileProvider::available() && Mac::FileProviderSettingsController::instance()->fileProviderModeEnabled()) {
1958+
// The File Provider extension does not support end-to-end encryption yet, so it
1959+
// cannot be set up while File Provider mode is enabled. Replace the whole setup
1960+
// prompt with an informational message and offer no "Set up encryption" action.
1961+
_ui->encryptionMessageLabel->setText(tr("The File Provider extension does not support end-to-end encryption yet."));
1962+
return;
1963+
}
1964+
#endif
1965+
1966+
_ui->encryptionMessageLabel->setText(tr("This account supports end-to-end encryption, but it needs to be set up first."));
1967+
19481968
auto *const actionSetupE2e = addActionToEncryptionMessage(tr("Set up encryption"), e2EeUiActionSetupEncryptionId);
19491969
connect(actionSetupE2e, &QAction::triggered, this, &AccountSettings::slotE2eEncryptionGenerateKeys);
19501970
}
19511971

1972+
void AccountSettings::refreshE2eEncryptionMessage()
1973+
{
1974+
// Only the not-yet-initialized setup prompt depends on the File Provider mode; once
1975+
// encryption is initialized the message shown is independent of it.
1976+
if (_accountState->account()->e2e()->isInitialized()) {
1977+
return;
1978+
}
1979+
1980+
// Rebuild from scratch so a stale "Set up encryption" action cannot linger (and to
1981+
// avoid re-connecting the same action twice).
1982+
removeActionFromEncryptionMessage(e2EeUiActionSetupEncryptionId);
1983+
setupE2eEncryptionMessage();
1984+
checkClientSideEncryptionState();
1985+
}
1986+
19521987
void AccountSettings::setEncryptionPanelVisible(bool visible)
19531988
{
19541989
_ui->encryptionMessage->setVisible(visible);
@@ -1957,6 +1992,39 @@ void AccountSettings::setEncryptionPanelVisible(bool visible)
19571992
}
19581993
}
19591994

1995+
void AccountSettings::updateSyncFoldersPanelVisibility()
1996+
{
1997+
#if defined(BUILD_FILE_PROVIDER_MODULE)
1998+
const auto fpModeOn = Mac::FileProvider::available()
1999+
&& Mac::FileProviderSettingsController::instance()->fileProviderModeEnabled();
2000+
2001+
auto hasClassicFolders = false;
2002+
const auto folderMap = FolderMan::instance()->map();
2003+
2004+
for (const auto folder : folderMap) {
2005+
if (folder->accountState() == _accountState) {
2006+
hasClassicFolders = true;
2007+
break;
2008+
}
2009+
}
2010+
2011+
// Hide the classic sync settings while File Provider mode is on — except when this
2012+
// account still has classic folders configured (mixed state): those must stay
2013+
// visible, together with the banner asking the user to resolve the conflict.
2014+
_ui->syncFoldersPanel->setVisible(!fpModeOn || hasClassicFolders);
2015+
_ui->fileProviderConflictBanner->setVisible(fpModeOn && hasClassicFolders);
2016+
2017+
if (fpModeOn && hasClassicFolders && _ui->fileProviderConflictBannerIcon->pixmap().isNull()) {
2018+
const auto iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, this);
2019+
const auto warningIcon = Theme::createColorAwareIcon(QStringLiteral(":/client/theme/black/warning.svg"));
2020+
_ui->fileProviderConflictBannerIcon->setPixmap(warningIcon.pixmap(iconSize, iconSize));
2021+
}
2022+
#else
2023+
_ui->syncFoldersPanel->setVisible(true);
2024+
_ui->fileProviderConflictBanner->setVisible(false);
2025+
#endif
2026+
}
2027+
19602028
void AccountSettings::setEncryptionMessageIcon(const QIcon &icon)
19612029
{
19622030
if (icon.isNull()) {

src/gui/accountsettings.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,14 @@ private slots:
141141
void checkClientSideEncryptionState();
142142
void removeActionFromEncryptionMessage(const QString &actionId);
143143
void setEncryptionPanelVisible(bool visible);
144+
void updateSyncFoldersPanelVisibility();
144145

145146
private:
146147
bool event(QEvent *) override;
147148
QAction *addActionToEncryptionMessage(const QString &actionTitle, const QString &actionId);
148149

149150
void setupE2eEncryptionMessage();
151+
void refreshE2eEncryptionMessage();
150152
void setEncryptionMessageIcon(const QIcon &icon);
151153
void updateEncryptionMessageActions();
152154

@@ -164,6 +166,7 @@ private slots:
164166
QAction *_addAccountAction = nullptr;
165167

166168
bool _menuShown = false;
169+
bool _e2eEncryptionSetupDone = false;
167170
QFrame *_encryptionPanel = nullptr;
168171

169172
QHash<QString, QMetaObject::Connection> _folderConnections;

src/gui/accountsettings.ui

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -133,46 +133,6 @@
133133
</layout>
134134
</widget>
135135
</item>
136-
<item>
137-
<widget class="QFrame" name="fileProviderPanel">
138-
<property name="sizePolicy">
139-
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
140-
<horstretch>0</horstretch>
141-
<verstretch>0</verstretch>
142-
</sizepolicy>
143-
</property>
144-
<property name="frameShape">
145-
<enum>QFrame::Shape::NoFrame</enum>
146-
</property>
147-
<layout class="QVBoxLayout" name="fileProviderPanelLayout">
148-
<property name="sizeConstraint">
149-
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
150-
</property>
151-
<item>
152-
<widget class="QLabel" name="fileProviderPanelTitle">
153-
<property name="font">
154-
<font>
155-
<bold>true</bold>
156-
</font>
157-
</property>
158-
<property name="text">
159-
<string>File Provider</string>
160-
</property>
161-
</widget>
162-
</item>
163-
<item>
164-
<widget class="QWidget" name="fileProviderPanelContents" native="true">
165-
<property name="sizePolicy">
166-
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
167-
<horstretch>0</horstretch>
168-
<verstretch>0</verstretch>
169-
</sizepolicy>
170-
</property>
171-
</widget>
172-
</item>
173-
</layout>
174-
</widget>
175-
</item>
176136
<item>
177137
<widget class="QFrame" name="syncFoldersPanel">
178138
<property name="sizePolicy">
@@ -206,6 +166,60 @@
206166
</property>
207167
</widget>
208168
</item>
169+
<item>
170+
<widget class="QWidget" name="fileProviderConflictBanner" native="true">
171+
<property name="visible">
172+
<bool>false</bool>
173+
</property>
174+
<layout class="QHBoxLayout" name="fileProviderConflictBannerLayout">
175+
<item>
176+
<widget class="QLabel" name="fileProviderConflictBannerIcon">
177+
<property name="sizePolicy">
178+
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
179+
<horstretch>0</horstretch>
180+
<verstretch>0</verstretch>
181+
</sizepolicy>
182+
</property>
183+
<property name="text">
184+
<string/>
185+
</property>
186+
<property name="alignment">
187+
<set>Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop</set>
188+
</property>
189+
</widget>
190+
</item>
191+
<item>
192+
<widget class="QLabel" name="fileProviderConflictBannerLabel">
193+
<property name="sizePolicy">
194+
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
195+
<horstretch>0</horstretch>
196+
<verstretch>0</verstretch>
197+
</sizepolicy>
198+
</property>
199+
<property name="text">
200+
<string>File Provider is enabled, but this account still has classic sync folders. The Finder integration stays unavailable until this is resolved.</string>
201+
</property>
202+
<property name="wordWrap">
203+
<bool>true</bool>
204+
</property>
205+
</widget>
206+
</item>
207+
<item>
208+
<widget class="QPushButton" name="fileProviderConflictResolveButton">
209+
<property name="sizePolicy">
210+
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
211+
<horstretch>0</horstretch>
212+
<verstretch>0</verstretch>
213+
</sizepolicy>
214+
</property>
215+
<property name="text">
216+
<string>Resolve…</string>
217+
</property>
218+
</widget>
219+
</item>
220+
</layout>
221+
</widget>
222+
</item>
209223
<item>
210224
<widget class="QWidget" name="syncFoldersPanelContents" native="true">
211225
<property name="sizePolicy">

0 commit comments

Comments
 (0)