Skip to content

refactor: migrate app passcode flows to SwiftUI - #4419

Merged
marinofaggiana merged 23 commits into
masterfrom
passcode
Oct 28, 2026
Merged

marinofaggiana merged 23 commits into
masterfrom
passcode

Conversation

@marinofaggiana

@marinofaggiana marinofaggiana commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Replace the legacy TOPasscodeViewController implementation with shared SwiftUI passcode components for the main app, Share Extension and E2EE authentication, while preserving existing Keychain credentials and preferences.

Behavior

  • Present the mandatory app lock in a dedicated window per scene, above existing sheets, alerts and app popups.
  • After three failed attempts, including attempts to change or disable the passcode, lock the app instead of allowing users to close the authentication sheet and continue accessing files. Expiring the delay only enables another authentication attempt; it does not unlock the app. The requirement persists across backgrounding and relaunch, even when passcode-at-startup is disabled.
  • Preserve the cumulative failure count across temporary lockouts. The default policy remains a 60-second pause after every three errors, with an optional app reset at ten errors. Reset threshold and delay use the branding parameters; successful authentication clears both counters.
  • Request biometric authentication when enabling Touch ID / Face ID in Settings, including the initial Face ID permission request when applicable. Enable the preference only after successful authentication; cancellation or failure leaves it disabled.
  • Add error haptics and a synchronized shake of the error message and six passcode dots for incorrect codes and mismatched confirmation codes. Respect Reduce Motion.
  • Use “Turn passcode on” / “Turn passcode off” action labels and larger keypad buttons.
  • Keep Share Extension authentication inside its panel, with scrolling for smaller layouts and a close button that cancels the entire extension without exposing protected content.
  • Remove obsolete passcode wrappers and update third-party acknowledgements.

UI tests and CI

Add seven XCUITests in PasscodeUITests:

Scenario Verification
Create, change and disable Reject the old code after changing it; disable protection using the new code.
Disable-flow lockout Accumulate failures across separate sheet openings, enforce the mandatory app lock, and preserve the original deadline across backgrounding and process relaunch.
Change-flow lockout Preserve failures across reopening, make Settings inaccessible at the threshold, and require successful authentication even after the delay expires.
Mandatory app lock Prevent dismissal and interaction with the underlying Settings screen after returning from background.
Existing sheet Cover Acknowledgements and preserve the sheet after unlocking.
Existing alert Cover the Clear cache confirmation and preserve the alert after unlocking.
Reset threshold Enter ten incorrect codes across three lockouts, verify cleanup completes, then relaunch without setup flags and verify login is required and the old passcode is gone.

The main xcode.yml workflow conditionally includes these tests when the PR changes passcode code, Settings integration, preferences, scene/app lifecycle, Share integration, branding, UI-test infrastructure, project configuration or the workflow itself. Otherwise it skips PasscodeUITests and continues running the other enabled tests. There is no separate passcode workflow.

The test environment uses an English simulator and server. The login helper reuses an existing account or performs the real web login, including handling the password-saving prompt. Removing the asynchronous account deletion at launch avoids opening an interface whose account is being deleted underneath it.

Debug-only test setup resets passcode state before each test. The reset-threshold test uses 10-second lockouts while keeping all ten failures and three lockouts; persistence tests retain the normal 60-second delay. With UI_TESTING_NO_EXIT, real reset cleanup completes and exposes a completion marker before XCTest terminates the app itself, avoiding the exit(0) / Thread Sanitizer finalization issue. Normal app execution retains exit(0).

CI uploads TestResult.xcresult even after test failure when available. The bundle includes test results and retained diagnostic screenshots/UI hierarchies for the instrumented failure paths. Console output is available in the Run Xcode Test step.

Running locally

  1. Use Xcode 27.1 or newer and a dedicated simulator with its system language set to English. The reset test deliberately removes the app’s local data and credentials.
  2. Start a Nextcloud stable30 test server at http://localhost:8080, with admin / admin. Tests/Server.sh provides the Docker setup. Keep Docker and the server running throughout the suite.
  3. Verify the server responds and uses English:
    docker exec --user www-data xcode-test-server php /var/www/html/occ config:system:set force_language --value=en
    curl --fail --show-error http://localhost:8080/status.php
  4. In Xcode, select the Nextcloud scheme and the dedicated simulator. In the Test navigator (⌘6), run NextcloudUITests → PasscodeUITests. Individual tests can also be run independently.

Additional device verification

  • Face ID / Touch ID authorization, successful authentication, rejection and cancellation.
  • Share Extension layout and cancellation on iPhone, iPad and changing window sizes, including access to the full keypad after cancelling biometrics.
  • E2EE authentication and independent app locking across multiple iPad scenes.
  • Error haptics, shake animation and Reduce Motion.
Screenshot 2026-10-23 alle 09 38 11

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Support passcode authentication, creation, changes and removal with biometrics and shared lockout protection. Keep the existing passcode flow active until callers are migrated.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Update lock settings after passcode completion and handle reset results. Add toolbar dismissal and require the current passcode when changing it.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Require authentication in Debug builds and verify the account before executing E2EE actions. Present cancellable passcode flows as sheets and handle completion during dismissal.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Use the shared passcode view for app unlock and enforced setup. Scope lifecycle handling to the presenting window scene and cancel optional operations when backgrounded.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana
marinofaggiana marked this pull request as draft October 23, 2026 07:42
@marinofaggiana marinofaggiana changed the title Passcode refactor: migrate app passcode flows to SwiftUI Oct 23, 2026
Replace TOPasscode in the main app and Share Extension, remove the legacy implementation, and add passcode UI tests.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana
marinofaggiana marked this pull request as ready for review October 23, 2026 08:56
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana
marinofaggiana marked this pull request as draft October 23, 2026 09:08
Clear only the retry counter and lockout deadline when the delay expires. Add UI coverage verifying app reset after ten failures across lockouts.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana
marinofaggiana marked this pull request as ready for review October 23, 2026 09:26
Present the mandatory lock in a dedicated window per scene and preserve underlying UI after unlocking.

Add UI regression coverage, improve authentication test handling, and enforce CI test timeouts with retained diagnostics.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Allow the initial HTTPS request to time out before expecting the localhost authentication page, and skip UI queries against authentication services that are not running.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Assisted-by: Codex:GPT-6
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana
marinofaggiana removed the request for review from mpivchev October 25, 2026 14:52
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Preserve the auto-upload suspension message until all reads finish, and discard cancelled or stale refreshes.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana marinofaggiana self-assigned this Oct 26, 2026
Embed the passcode view in the content controller while hiding the enclosing navigation bar, and constrain its width to the available space.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@mpivchev

Copy link
Copy Markdown
Collaborator
  1. Add vibration and shake the "Incorrect password" text and "Passcodes don't match" text.
  2. Enabling Touch/Face Id does not ask for permission. Only after reopening the app. Permission should be asked when enabling it.
  3. Rename "Lock: On" to "Turn passcode on" and "Turn passcode off"
  4. Pressing Change passcode and typing wrong passcode would lock you of more attempts, but you can close the window and continue using the app. The lock should make the app unusable. Please also test this with UI tests.

@mpivchev

Copy link
Copy Markdown
Collaborator

Actually now that I think about it, to me the "too many attempts" 60 seconds cooldown is a bit useless. What is the point of it? We are not really doing anything security wise.

We can remove it and instead turn on the "reset data after 10 attempts" on always?
@marinofaggiana @tobiasKaminsky what do you think?

@mpivchev

mpivchev commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

@marinofaggiana the UI tests should be included in the CI. Also keep in mind UI tests must be maintained with time so they are always green.

@marinofaggiana

Copy link
Copy Markdown
Member Author
  1. Add vibration and shake the "Incorrect password" text and "Passcodes don't match" text.
  2. Enabling Touch/Face Id does not ask for permission. Only after reopening the app. Permission should be asked when enabling it.
  3. Rename "Lock: On" to "Turn passcode on" and "Turn passcode off"
  4. Pressing Change passcode and typing wrong passcode would lock you of more attempts, but you can close the window and continue using the app. The lock should make the app unusable. Please also test this with UI tests.

done.

Preserve mandatory authentication across scenes and relaunches after lockout. Require biometric authorization before enabling Touch ID or Face ID, add passcode error feedback, and update lockout UI tests.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@mpivchev

mpivchev commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

@marinofaggiana the UI tests should be included in the CI. Also keep in mind UI tests must be maintained with time so they are always green.

There are two reasons why I did not include the tests in the CI. The first is that they take a long time to run (due to timeouts, retries, etc.), which would result in an excessively long wait each time. The second is that after the final attempt, the tenth one, the app automatically resets, which causes the test to exit as "failed" (exit(0)).

Right, but in this case the UI tests will be not super useful as no one will run them (let's be honest). You can add the UI tests as a separate CI workflow and either:

  1. Add a check to only run the workflow if the passcode UI was changed. This is possible via defining the Passcode files folder. This way if no modifications are done on the passcode UI the UI tests will be skipped. IMO this is the best way.
  2. You can make the workflow manual, so it is not run automatically.

Claude can do this.

@marinofaggiana

Copy link
Copy Markdown
Member Author

@marinofaggiana the UI tests should be included in the CI. Also keep in mind UI tests must be maintained with time so they are always green.

They require so much time, far too much time!

@marinofaggiana

Copy link
Copy Markdown
Member Author

@marinofaggiana the UI tests should be included in the CI. Also keep in mind UI tests must be maintained with time so they are always green.

There are two reasons why I did not include the tests in the CI. The first is that they take a long time to run (due to timeouts, retries, etc.), which would result in an excessively long wait each time. The second is that after the final attempt, the tenth one, the app automatically resets, which causes the test to exit as "failed" (exit(0)).

Right, but in this case the UI tests will be not super useful as no one will run them (let's be honest). You can add the UI tests as a separate CI workflow and either:

  1. Add a check to only run the workflow if the passcode UI was changed. This is possible via defining the Passcode files folder. This way if no modifications are done on the passcode UI the UI tests will be skipped. IMO this is the best way.
  2. You can make the workflow manual, so it is not run automatically.

Claude can do this.

good idea. I do

Shorten reset-test lockouts and let XCTest terminate the app after observing completed cleanup to avoid TSan exit issues. Run passcode UI tests when related files change and increase CI test timeouts.

Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
@marinofaggiana
marinofaggiana merged commit 48d27f2 into master Oct 28, 2026
5 of 6 checks passed
@marinofaggiana
marinofaggiana deleted the passcode branch October 28, 2026 06:57
xeniacloud pushed a commit to Krateos-BV/files-ios that referenced this pull request Oct 29, 2026
…(XEN-293)

All 21 PasscodeUITests (new from upstream nextcloud#4419) failed at login on
PR #35: BaseUIXCTestCase.logIn() waits for the intro "login" button
and types TestConstants.server (http://localhost:8080) into the server
field, but our single-tenant lock skips the intro and disables that
field, so the app showed only submitServerAddress and the QR button.

Restore upstream's login flow (intro shown, empty editable server field)
for UI_TESTING launches only, under #if DEBUG. The scheme's Test action
builds Debug and Archive builds Release, so no shipped binary contains
the escape hatch. Same call as talk-ios XEN-202. No test file touched.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfGtQofgcLaqMsHHJCGrNo
xeniacloud pushed a commit to Krateos-BV/files-ios that referenced this pull request Oct 29, 2026
The passcode UI tests time out on the xcode-27 runner because the app's
main thread is saturated serving XCTest accessibility snapshots. The
login helpers poll every 250 ms with about 8 queries a pass, and each
query snapshots the app on its main thread, where the http fallback,
the Login Flow v2 poll and the grant handling all run. Upstream's own
CI fails the same way on nextcloud#4419.

- NCLogin: dismiss the keyboard on the login button, as the Return key
  path already does. The keyboard keys were about half of every
  snapshot.
- BaseUIXCTestCase: poll once a second instead of every 250 ms in
  waitForAuthenticationConsent and authenticationWebView.

No test is skipped or disabled. Passes locally on the Mac Mini; only a
run on the xcode-27 runner can confirm the fix.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfGtQofgcLaqMsHHJCGrNo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants