Skip to content

feat: TW-2991 display recovery key in the app#2944

Open
9clg6 wants to merge 2 commits intomainfrom
feat/TW-2991-display-recovery-key
Open

feat: TW-2991 display recovery key in the app#2944
9clg6 wants to merge 2 commits intomainfrom
feat/TW-2991-display-recovery-key

Conversation

@9clg6
Copy link
Collaborator

@9clg6 9clg6 commented Mar 19, 2026

Summary

Split from #2914 — this PR contains only the feature (no refactoring).

  • Fetch the recovery key from the ToM server via GetRecoveryWordsInteractor
  • Display it as a masked field in Privacy & Security settings
  • Show a security warning dialog before copying to clipboard
  • Add new SVG icon (ic_recovery_key.svg) and i18n strings
  • Add E2E test for recovery key copy-to-clipboard flow

Related PR (refacto): #2945

Ticket

Related issue: TW-2991

Test recommendations

  • Verify the recovery key item appears in Settings > Privacy & Security when the user has a recovery key
  • Verify it is hidden when the recovery key is unavailable
  • Tap the copy icon → confirm the warning dialog appears → confirm the key is copied to clipboard
  • Dismiss the warning dialog → confirm nothing is copied
  • Verify existing settings items still render and function correctly
Simulator Screenshot - iPhone 17 Pro Max - 2026-03-19 at 12 56 56 image

Summary by CodeRabbit

Release Notes

  • New Features

    • Added recovery key functionality with secure copy-to-clipboard capability in Security Settings.
    • Added localized recovery key warning message and confirmation text.
    • Integrated recovery key icon asset.
  • Tests

    • Added comprehensive integration tests for recovery key copying workflow.
    • Updated login automation tests with localized text.
  • Refactor

    • Optimized settings UI rendering performance through caching.
    • Simplified widget configuration code.
  • Chores

    • Fixed naming inconsistencies in style constants.
    • Updated analyzer configuration.

@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

Walkthrough

This pull request implements recovery key functionality across the application. It adds localization entries for recovery key-related messages, introduces a new integration test robot and test for recovery key copy workflows, updates the login robot to use localized strings, refactors settings UI components for caching and style consistency, implements recovery key fetching and copying in the security settings page, and adds supporting configuration changes. The modifications span analyzer configuration, localization assets, integration tests, and UI/settings code.

Possibly related PRs

Suggested reviewers

  • dab246
  • hoangdat
  • nqhhdev
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: displaying the recovery key in the app, which aligns with the primary objective and all file changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed PR description provides ticket reference, solution outline, test recommendations, and supporting screenshots, following most of the template structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/TW-2991-display-recovery-key
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

This PR has been deployed to https://linagora.github.io/twake-on-matrix/2944

@9clg6 9clg6 marked this pull request as ready for review March 19, 2026 10:30
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
lib/pages/chat/events/audio_message/audio_play_extension.dart (1)

6-6: Improve TODO comment clarity.

The TODO comment could be more specific. Consider clarifying "ignore linter" to "ignore directive" or "linter suppression" for better precision.

📝 Suggested rewording
-///TODO(clement): Remove the ignore linter when the experimental member use is no longer needed.
+///TODO(clement): Remove the ignore directive when the experimental member is no longer used or becomes stable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/chat/events/audio_message/audio_play_extension.dart` at line 6,
Update the TODO in audio_play_extension.dart to be more specific: replace
"ignore linter" with "linter suppression" or "ignore directive", state which
specific ignore is used (e.g., the analyzer rule name) and add when it can be
removed (e.g., "remove when experimental member use is stabilized/removed"), so
the TODO in the file's top comment clearly indicates the suppression, the rule,
and the removal condition.
integration_test/robots/login_robot.dart (1)

191-218: Broad exception catching limits debuggability; logging would help identify failures.

The catch (_) blocks swallow all exceptions indiscriminately. While the downstream $(ChatList).waitUntilVisible() assertion in login_scenario.dart will eventually fail for a broken login, transient failures are harder to diagnose without visibility into what exception was caught.

Additionally, the early return at line 198 means if the first tap throws any exception, the Captcha dialog handling (lines 201–218) is skipped entirely. If the Captcha is shown but the first tap fails for an unrelated reason, the flow will exit prematurely.

Patrol does not expose a specific exception type for native view lifecycle events (e.g., "view closed" or "disappeared"), so broader exception handling is the current pattern. Consider adding a log statement to the catch blocks for debugging visibility when test failures occur:

     try {
       await $.native.tap(getSignInBtn(), appId: getBrowserAppId());
     } catch (_) {
       // Browser closed after successful SSO login – expected.
+      // Log for debugging if SSO fails unexpectedly
+      // print('Sign-in tap threw exception: $_');
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration_test/robots/login_robot.dart` around lines 191 - 218, The
try/catch blocks around $.native.tap(getSignInBtn(), appId: getBrowserAppId())
and the second tap currently swallow all exceptions and the first catch returns
early, skipping the Captcha handling; update these catches to log the caught
exception (include the exception message/stack via the test logger) rather than
silently ignoring it, and remove the early return in the first catch so the code
continues to check CoreRobot($).existsOptionalNativeItems(...,
getOKBtnInVerifyCaptchaDialog(), ...) and attempt the Captcha flow; also add the
same logging to the second catch so any unexpected failure during the retry is
recorded (refer to getSignInBtn, getBrowserAppId, getOKBtnInVerifyCaptchaDialog,
CoreRobot.existsOptionalNativeItems, and $.native.tap to find the locations).
lib/pages/settings_dashboard/settings_security/settings_security_view.dart (1)

154-160: Keep the copy flow behind the explicit icon.

SettingsItemBuilder already makes the whole tile tappable, so wiring onTap: controller.copyRecoveryKey here turns the entire recovery-key row into a copy action. Since this copies a secret, I'd keep the row non-destructive and reserve copying for the trailing control.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/settings_dashboard/settings_security/settings_security_view.dart`
around lines 154 - 160, The recovery-key row currently wires the tile-level
onTap to controller.copyRecoveryKey, making the whole SettingsItemBuilder
tappable; remove or null out the tile onTap (the onTap property currently set to
controller.copyRecoveryKey) so only the trailingWidget InkWell (key:
'recovery_key_copy_button') triggers copyRecoveryKey, preserving non-destructive
behavior for the row while keeping the trailing copy icon functional.
integration_test/tests/setting/settings_recovery_key_test.dart (1)

26-51: Please cover the dismiss path too.

This only proves the confirm flow. A regression where the warning dialog is dismissed but the clipboard is still written would slip through.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration_test/tests/setting/settings_recovery_key_test.dart` around lines
26 - 51, The test only exercises the confirm path via
recoveryKeyRobot.tapCopyAndConfirm() so add a dismiss-path assertion: invoke the
dialog dismiss action (e.g., a new or existing
recoveryKeyRobot.tapCopyAndDismiss() or call to tap copy then tapCancel/dismiss
on the dialog), then assert no confirmation snackbar via
recoveryKeyRobot.verifySnackBarIsNotShown() (or invert verifySnackBarIsShown),
and verify the clipboard remains null/empty or still contains the masked value
by calling recoveryKeyRobot.getClipboardText() and asserting it equals
null/empty or equals '\u2022' * 32; update or add helper methods on
recoveryKeyRobot if needed to perform the dismiss action and negative snackbar
check.
lib/pages/settings_dashboard/settings_security/settings_security.dart (1)

50-55: Consider calling super.initState() first.

Flutter's convention recommends calling super.initState() before any other initialization logic. While the current ordering works, placing it first ensures any base class setup completes before subclass logic runs.

🔧 Suggested reorder
 `@override`
 void initState() {
+  super.initState();
   listenIgnoredUser();
   recoveryKeyFuture = _fetchRecoveryKey();
-  super.initState();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/settings_dashboard/settings_security/settings_security.dart` around
lines 50 - 55, Move the call to super.initState() to the top of the initState
method so base-class initialization runs before subclass work: in the
SettingsSecurityState class's initState, call super.initState() first, then
invoke listenIgnoredUser() and set recoveryKeyFuture = _fetchRecoveryKey(); to
preserve current behavior while following Flutter conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@assets/l10n/intl_en.arb`:
- Around line 1742-1747: Add the three new localization keys recoveryKey,
recoveryKeyWarningMessage, and recoveryKeyCopiedToClipboard (and their
corresponding metadata entries `@recoveryKey`, `@recoveryKeyWarningMessage`,
`@recoveryKeyCopiedToClipboard`) to every remaining intl_*.arb locale file that
currently lacks them; ensure each file mirrors the exact English strings and
empty metadata objects from intl_en.arb (so non-English locales have the keys
present for validation/fallback), and also add the two missing keys to
intl_ie.arb to complete its partial entry.

---

Nitpick comments:
In `@integration_test/robots/login_robot.dart`:
- Around line 191-218: The try/catch blocks around $.native.tap(getSignInBtn(),
appId: getBrowserAppId()) and the second tap currently swallow all exceptions
and the first catch returns early, skipping the Captcha handling; update these
catches to log the caught exception (include the exception message/stack via the
test logger) rather than silently ignoring it, and remove the early return in
the first catch so the code continues to check
CoreRobot($).existsOptionalNativeItems(..., getOKBtnInVerifyCaptchaDialog(),
...) and attempt the Captcha flow; also add the same logging to the second catch
so any unexpected failure during the retry is recorded (refer to getSignInBtn,
getBrowserAppId, getOKBtnInVerifyCaptchaDialog,
CoreRobot.existsOptionalNativeItems, and $.native.tap to find the locations).

In `@integration_test/tests/setting/settings_recovery_key_test.dart`:
- Around line 26-51: The test only exercises the confirm path via
recoveryKeyRobot.tapCopyAndConfirm() so add a dismiss-path assertion: invoke the
dialog dismiss action (e.g., a new or existing
recoveryKeyRobot.tapCopyAndDismiss() or call to tap copy then tapCancel/dismiss
on the dialog), then assert no confirmation snackbar via
recoveryKeyRobot.verifySnackBarIsNotShown() (or invert verifySnackBarIsShown),
and verify the clipboard remains null/empty or still contains the masked value
by calling recoveryKeyRobot.getClipboardText() and asserting it equals
null/empty or equals '\u2022' * 32; update or add helper methods on
recoveryKeyRobot if needed to perform the dismiss action and negative snackbar
check.

In `@lib/pages/chat/events/audio_message/audio_play_extension.dart`:
- Line 6: Update the TODO in audio_play_extension.dart to be more specific:
replace "ignore linter" with "linter suppression" or "ignore directive", state
which specific ignore is used (e.g., the analyzer rule name) and add when it can
be removed (e.g., "remove when experimental member use is stabilized/removed"),
so the TODO in the file's top comment clearly indicates the suppression, the
rule, and the removal condition.

In `@lib/pages/settings_dashboard/settings_security/settings_security_view.dart`:
- Around line 154-160: The recovery-key row currently wires the tile-level onTap
to controller.copyRecoveryKey, making the whole SettingsItemBuilder tappable;
remove or null out the tile onTap (the onTap property currently set to
controller.copyRecoveryKey) so only the trailingWidget InkWell (key:
'recovery_key_copy_button') triggers copyRecoveryKey, preserving non-destructive
behavior for the row while keeping the trailing copy icon functional.

In `@lib/pages/settings_dashboard/settings_security/settings_security.dart`:
- Around line 50-55: Move the call to super.initState() to the top of the
initState method so base-class initialization runs before subclass work: in the
SettingsSecurityState class's initState, call super.initState() first, then
invoke listenIgnoredUser() and set recoveryKeyFuture = _fetchRecoveryKey(); to
preserve current behavior while following Flutter conventions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 033c4b52-5f98-4f8b-99db-26487e42c5bf

📥 Commits

Reviewing files that changed from the base of the PR and between bc4b54a and 4c282c4.

⛔ Files ignored due to path filters (1)
  • assets/images/ic_recovery_key.svg is excluded by !**/*.svg
📒 Files selected for processing (12)
  • analysis_options.yaml
  • assets/l10n/intl_en.arb
  • integration_test/robots/login_robot.dart
  • integration_test/robots/setting/settings_recovery_key_robot.dart
  • integration_test/tests/setting/settings_recovery_key_test.dart
  • lib/pages/chat/events/audio_message/audio_play_extension.dart
  • lib/pages/settings_dashboard/settings/settings_item_builder.dart
  • lib/pages/settings_dashboard/settings/settings_view.dart
  • lib/pages/settings_dashboard/settings/settings_view_style.dart
  • lib/pages/settings_dashboard/settings_security/settings_security.dart
  • lib/pages/settings_dashboard/settings_security/settings_security_view.dart
  • lib/resource/image_paths.dart

@9clg6 9clg6 mentioned this pull request Mar 19, 2026
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.

1 participant