Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: GPL-2.0-or-later
-->

# TC-0007: Switching the app-level File Provider mode

- **Platform:** macOS
- **Related Issues:** None yet

## Description

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.

## Preconditions

- A client build with the file provider module (`Nextcloud-vfs-…`) on macOS 14 or later.
- At least one configured account with one or more classic sync folders containing synced files.

## Test Steps

| Step | Action | Expected Result |
|------|--------|-----------------|
| 1 | Open Settings → General | A "Use macOS File Provider Integration" switch is shown with an explanatory text; it is off |
| 2 | Open the account's settings page | The "Classic sync" panel with the folder list is shown; no File Provider panel exists |
| 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 |
| 4 | Click "Cancel" | Nothing changes: switch stays off, classic folders keep syncing |
| 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 |
| 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 |
| 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 |
| 8 | Add a new folder sync connection and restart the client | The classic folder syncs again and FinderSync badges work |

## Expected Results

- Classic sync folders and File Provider are never active at the same time.
- No local files are deleted by either switch direction.
- 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.
- 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.
1 change: 0 additions & 1 deletion resources.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
<file>src/gui/wizard/qml/WizardComboBox.qml</file>
<file>src/gui/wizard/qml/WizardDialogFrame.qml</file>
<file>src/gui/wizard/qml/WizardTextField.qml</file>
<file>src/gui/macOS/ui/FileProviderSettings.qml</file>
<file>src/gui/macOS/ui/FileProviderFileDelegate.qml</file>
<file>src/gui/integration/FileActionsWindow.qml</file>
<file>src/gui/GovernanceLabelsDialog.qml</file>
Expand Down
144 changes: 106 additions & 38 deletions src/gui/accountsettings.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2014 ownCloud GmbH
Expand Down Expand Up @@ -196,16 +196,14 @@
connect(connectionSettingsButton, &QPushButton::clicked, this, &AccountSettings::showConnectionSettingsDialog);

_ui->verticalLayout_2->removeWidget(_ui->accountStatusPanel);
_ui->verticalLayout_2->removeWidget(_ui->fileProviderPanel);
_ui->verticalLayout_2->removeWidget(_ui->syncFoldersPanel);
_ui->verticalLayout_2->removeWidget(_ui->connectionSettingsPanel);
_ui->verticalLayout_2->removeWidget(_ui->accountActionsPanel);
_ui->connectionSettingsPanel->hide();
_ui->verticalLayout_2->insertWidget(0, _ui->fileProviderPanel);
_ui->verticalLayout_2->insertWidget(1, _ui->syncFoldersPanel);
_ui->verticalLayout_2->insertWidget(2, _encryptionPanel);
_ui->verticalLayout_2->insertWidget(3, _ui->accountStatusPanel);
_ui->verticalLayout_2->insertWidget(4, _ui->accountActionsPanel);
_ui->verticalLayout_2->insertWidget(0, _ui->syncFoldersPanel);
_ui->verticalLayout_2->insertWidget(1, _encryptionPanel);
_ui->verticalLayout_2->insertWidget(2, _ui->accountStatusPanel);
_ui->verticalLayout_2->insertWidget(3, _ui->accountActionsPanel);

_model->setAccountState(_accountState);
_model->setParent(this);
Expand All @@ -215,9 +213,6 @@
_ui->syncFoldersPanelContents->setAutoFillBackground(false);
_ui->syncFoldersPanelContents->setAttribute(Qt::WA_StyledBackground, false);
_ui->syncFoldersPanelContents->setContentsMargins(0, 0, 0, 0);
_ui->fileProviderPanelContents->setAutoFillBackground(false);
_ui->fileProviderPanelContents->setAttribute(Qt::WA_StyledBackground, false);
_ui->fileProviderPanelContents->setContentsMargins(0, 0, 0, 0);
_ui->connectionSettingsPanelContents->setAutoFillBackground(false);
_ui->connectionSettingsPanelContents->setAttribute(Qt::WA_StyledBackground, false);
_ui->connectionSettingsPanelContents->setContentsMargins(0, 0, 0, 0);
Expand All @@ -244,30 +239,30 @@
new ToolTipUpdater(_ui->_folderList);

#if defined(BUILD_FILE_PROVIDER_MODULE)
if (Mac::FileProvider::available()) {
const auto fileProviderPanelContents = _ui->fileProviderPanelContents;
const auto fpSettingsLayout = new QVBoxLayout(fileProviderPanelContents);
const auto fpAccountUserIdAtHost = _accountState->account()->userIdAtHostWithPort();
const auto fpSettingsController = Mac::FileProviderSettingsController::instance();
const auto fpSettingsWidget = fpSettingsController->settingsViewWidget(fpAccountUserIdAtHost, fileProviderPanelContents,
QQuickWidget::SizeRootObjectToView);
fpSettingsLayout->setContentsMargins(0, 0, 0, 0);
fpSettingsLayout->setSpacing(0);

fpSettingsWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
if (const auto fpSettingsWidgetLayout = fpSettingsWidget->layout()) {
fpSettingsWidgetLayout->setContentsMargins(0, 0, 0, 0);
}
fpSettingsLayout->addWidget(fpSettingsWidget, 1);
fileProviderPanelContents->setLayout(fpSettingsLayout);
} else {
// macOS 13 Ventura: the file provider feature is unsupported there.
// This branch can be removed once Ventura is no longer supported.
_ui->fileProviderPanel->setVisible(false);
}
#else
_ui->fileProviderPanel->setVisible(false);
// The File Provider integration is an app-level mode, toggled in the General
// settings. While it is enabled, classic sync folders are unavailable: hide the
// "Classic sync" panel, except when the account still has classic folders
// configured (a conflict the banner asks the user to resolve).
const auto fpSettingsController = Mac::FileProviderSettingsController::instance();
connect(fpSettingsController, &Mac::FileProviderSettingsController::fileProviderModeEnabledChanged,
this, &AccountSettings::updateSyncFoldersPanelVisibility);
connect(fpSettingsController, &Mac::FileProviderSettingsController::fileProviderModeEnabledChanged,
this, &AccountSettings::refreshE2eEncryptionMessage);
// The reconciliation "Keep File Provider" path applies the mode without emitting
// fileProviderModeEnabledChanged (the flag was already on), so also refresh on the
// bulk-apply completion to update a page that is open during the transition.
connect(fpSettingsController, &Mac::FileProviderSettingsController::fileProviderModeApplyFinished,
this, [this](bool, const QStringList &) {
refreshE2eEncryptionMessage();
updateSyncFoldersPanelVisibility();
});
connect(FolderMan::instance(), &FolderMan::folderListChanged,
this, &AccountSettings::updateSyncFoldersPanelVisibility);
connect(_ui->fileProviderConflictResolveButton, &QPushButton::clicked, this, [fpSettingsController] {
fpSettingsController->performStartupReconciliation();
});
#endif
updateSyncFoldersPanelVisibility();

const auto mouseCursorChanger = new MouseCursorChanger(this);
mouseCursorChanger->folderList = _ui->_folderList;
Expand Down Expand Up @@ -329,7 +324,10 @@
// Connect E2E stuff
if (_accountState->isConnected()) {
setupE2eEncryption();
_e2eEncryptionSetupDone = true;
} else {
// Not connected yet: setupE2eEncryption() (which builds the File-Provider-aware
// message) runs on the first Connected transition in slotAccountStateChanged().
_ui->encryptionMessageLabel->setText(tr("End-to-end encryption has not been initialized on this account."));
}
_ui->encryptionMessageLabel->setTextFormat(Qt::RichText);
Expand Down Expand Up @@ -940,6 +938,13 @@
QStringList() << QLatin1String("/"));
folderMan->scheduleAllFolders();
emit folderChanged();
} else {
// addFolder can refuse (e.g. classic sync folders are unavailable while the
// File Provider integration is enabled). Don't leave the user believing the
// wizard succeeded.
QMessageBox::warning(this, tr("Folder creation failed"),
tr("<p>Could not add the folder sync connection for <i>%1</i>.</p>")
.arg(Utility::escape(QDir::toNativeSeparators(definition.localPath))));
}
}

Expand Down Expand Up @@ -1454,7 +1459,7 @@
slotAccountStateChanged();
}

void AccountSettings::slotAccountStateChanged()

Check warning on line 1462 in src/gui/accountsettings.cpp

View workflow job for this annotation

GitHub Actions / build

src/gui/accountsettings.cpp:1462:23 [readability-function-cognitive-complexity]

function 'slotAccountStateChanged' has cognitive complexity of 26 (threshold 25)

Check failure on line 1462 in src/gui/accountsettings.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 31 to the 25 allowed.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9rIWyhM-GMQlnKSkrT&open=AZ9rIWyhM-GMQlnKSkrT&pullRequest=10388
{
const auto state = _accountState ? _accountState->state() : AccountState::Disconnected;
if (state != AccountState::Disconnected) {
Expand Down Expand Up @@ -1560,6 +1565,15 @@
refreshSelectiveSyncStatus();

if (state == AccountState::State::Connected) {
// Build the encryption message on the first connect if the account was still
// connecting when this page was constructed — otherwise the panel would be
// revealed below still showing the not-connected placeholder instead of the
// File-Provider-aware text. Guarded so e2e initialization is set up only once;
// File-Provider-mode changes are handled separately via the controller signals.
if (!_e2eEncryptionSetupDone) {
setupE2eEncryption();
_e2eEncryptionSetupDone = true;
}
checkClientSideEncryptionState();
}
}
Expand Down Expand Up @@ -1936,19 +1950,40 @@

void AccountSettings::setupE2eEncryptionMessage()
{
#ifdef BUILD_FILE_PROVIDER_MODULE
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.");
#else
const auto encryptionMessage = tr("This account supports end-to-end encryption, but it needs to be set up first.");
#endif
_ui->encryptionMessageLabel->setText(encryptionMessage);
setEncryptionMessageIcon(Theme::createColorAwareIcon(QStringLiteral(":/client/theme/info.svg")));
setEncryptionPanelVisible(false);

#ifdef BUILD_FILE_PROVIDER_MODULE
if (Mac::FileProvider::available() && Mac::FileProviderSettingsController::instance()->fileProviderModeEnabled()) {
// The File Provider extension does not support end-to-end encryption yet, so it
// cannot be set up while File Provider mode is enabled. Replace the whole setup
// prompt with an informational message and offer no "Set up encryption" action.
_ui->encryptionMessageLabel->setText(tr("The File Provider extension does not support end-to-end encryption yet."));
return;
}
#endif

_ui->encryptionMessageLabel->setText(tr("This account supports end-to-end encryption, but it needs to be set up first."));

auto *const actionSetupE2e = addActionToEncryptionMessage(tr("Set up encryption"), e2EeUiActionSetupEncryptionId);
connect(actionSetupE2e, &QAction::triggered, this, &AccountSettings::slotE2eEncryptionGenerateKeys);
}

void AccountSettings::refreshE2eEncryptionMessage()
{
// Only the not-yet-initialized setup prompt depends on the File Provider mode; once
// encryption is initialized the message shown is independent of it.
if (_accountState->account()->e2e()->isInitialized()) {
return;
}

// Rebuild from scratch so a stale "Set up encryption" action cannot linger (and to
// avoid re-connecting the same action twice).
removeActionFromEncryptionMessage(e2EeUiActionSetupEncryptionId);
setupE2eEncryptionMessage();
checkClientSideEncryptionState();
}

void AccountSettings::setEncryptionPanelVisible(bool visible)
{
_ui->encryptionMessage->setVisible(visible);
Expand All @@ -1957,6 +1992,39 @@
}
}

void AccountSettings::updateSyncFoldersPanelVisibility()

Check warning on line 1995 in src/gui/accountsettings.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9rIWyiM-GMQlnKSkrU&open=AZ9rIWyiM-GMQlnKSkrU&pullRequest=10388
{
#if defined(BUILD_FILE_PROVIDER_MODULE)
const auto fpModeOn = Mac::FileProvider::available()
&& Mac::FileProviderSettingsController::instance()->fileProviderModeEnabled();

auto hasClassicFolders = false;
const auto folderMap = FolderMan::instance()->map();

for (const auto folder : folderMap) {
if (folder->accountState() == _accountState) {
hasClassicFolders = true;
break;
}
}

// Hide the classic sync settings while File Provider mode is on — except when this
// account still has classic folders configured (mixed state): those must stay
// visible, together with the banner asking the user to resolve the conflict.
_ui->syncFoldersPanel->setVisible(!fpModeOn || hasClassicFolders);
_ui->fileProviderConflictBanner->setVisible(fpModeOn && hasClassicFolders);

if (fpModeOn && hasClassicFolders && _ui->fileProviderConflictBannerIcon->pixmap().isNull()) {
const auto iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, this);
const auto warningIcon = Theme::createColorAwareIcon(QStringLiteral(":/client/theme/black/warning.svg"));
_ui->fileProviderConflictBannerIcon->setPixmap(warningIcon.pixmap(iconSize, iconSize));
}
#else
_ui->syncFoldersPanel->setVisible(true);
_ui->fileProviderConflictBanner->setVisible(false);
#endif
}

void AccountSettings::setEncryptionMessageIcon(const QIcon &icon)
{
if (icon.isNull()) {
Expand Down
3 changes: 3 additions & 0 deletions src/gui/accountsettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#ifndef ACCOUNTSETTINGS_H
#define ACCOUNTSETTINGS_H

#include <QWidget>

Check failure on line 10 in src/gui/accountsettings.h

View workflow job for this annotation

GitHub Actions / build

src/gui/accountsettings.h:10:10 [clang-diagnostic-error]

'QWidget' file not found
#include <QUrl>
#include <QPointer>
#include <QHash>
Expand Down Expand Up @@ -141,12 +141,14 @@
void checkClientSideEncryptionState();
void removeActionFromEncryptionMessage(const QString &actionId);
void setEncryptionPanelVisible(bool visible);
void updateSyncFoldersPanelVisibility();

Check warning on line 144 in src/gui/accountsettings.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ9rIWxOM-GMQlnKSkrS&open=AZ9rIWxOM-GMQlnKSkrS&pullRequest=10388

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

void setupE2eEncryptionMessage();
void refreshE2eEncryptionMessage();
void setEncryptionMessageIcon(const QIcon &icon);
void updateEncryptionMessageActions();

Expand All @@ -164,6 +166,7 @@
QAction *_addAccountAction = nullptr;

bool _menuShown = false;
bool _e2eEncryptionSetupDone = false;
QFrame *_encryptionPanel = nullptr;

QHash<QString, QMetaObject::Connection> _folderConnections;
Expand Down
94 changes: 54 additions & 40 deletions src/gui/accountsettings.ui
Original file line number Diff line number Diff line change
Expand Up @@ -133,46 +133,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="fileProviderPanel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<layout class="QVBoxLayout" name="fileProviderPanelLayout">
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="fileProviderPanelTitle">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>File Provider</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="fileProviderPanelContents" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="syncFoldersPanel">
<property name="sizePolicy">
Expand Down Expand Up @@ -206,6 +166,60 @@
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="fileProviderConflictBanner" native="true">
<property name="visible">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="fileProviderConflictBannerLayout">
<item>
<widget class="QLabel" name="fileProviderConflictBannerIcon">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="fileProviderConflictBannerLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>File Provider is enabled, but this account still has classic sync folders. The Finder integration stays unavailable until this is resolved.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="fileProviderConflictResolveButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Resolve…</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="syncFoldersPanelContents" native="true">
<property name="sizePolicy">
Expand Down
Loading
Loading