Skip to content

feat(education): process-time user education (#90)#135

Merged
RaheemJnr merged 28 commits intomainfrom
feat/issue-90-education-feature
Apr 29, 2026
Merged

feat(education): process-time user education (#90)#135
RaheemJnr merged 28 commits intomainfrom
feat/issue-90-education-feature

Conversation

@RaheemJnr
Copy link
Copy Markdown
Owner

@RaheemJnr RaheemJnr commented Apr 29, 2026

Summary

Closes #90. Ships the user-education feature (process-time confusions only — sync, sync mode, block height, balance delay, "Pending", confirmations).

What's in this PR:

  • One-time sync coachmark on Home (anchored to the sync card, dismissible, persisted via WalletPreferences.hasSeenSyncCoachmark). 2-second grace prevents flashing for NEW_WALLET mode that completes in milliseconds.
  • ? bottom-sheet help — section ? on Home Sync + Activity sections; inline ? per option in the sync mode picker; inline ? on the CUSTOM block-height field.
  • SyncOptionsDialog → SyncOptionsSheet — AlertDialog → ModalBottomSheet. New "Pick this if…" copy per mode. RECENT now carries a "Recommended" badge.
  • FAQ screen at Settings → Help & FAQ. Hardcoded Compose accordion; 10 entries; deep-linkable from ? bottom sheets.
  • Block height removed from Home — Node Status (existing screen) still shows raw RPC data for power users.
  • All new copy is strings.xml-backed (single English locale; non-English locales tracked in i18n: add translations for non-English locales (Yoruba, Hausa, Pidgin English, Mandarin, Spanish, etc.) #132).

Setup-time confusions (mnemonic flow refresh, "what if I lose my phone") are intentionally deferred to a separate brainstorm + PR.

Architecture highlights

  • ui/education/EducationTopic.ktenum class (Saver-compatible with rememberSaveable) carrying titleRes, bodyRes, faqAnchor directly. Single source of truth.
  • ui/education/EducationSheet.kt — stateless ModalBottomSheet; caller controls visibility via the topic param.
  • ui/education/coachmark/Modifier.coachmarkAnchor(key) records bounds into a CompositionLocal-scoped registry; SyncCoachmark overlay reads them and draws a 4-rect Canvas scrim with tap-swallow.
  • HomeViewModel.shouldShowSyncCoachmark(seen, isCatchingUp, firstCatchingUpAtMs, nowMs) — pure helper, deterministically testable. Driven by a 1-second clock tick combined with prefs flag + sync progress.
  • SyncProgress.firstCatchingUpAtMs — process-lifetime wall-clock timestamp captured the first time isCatchingUp flips true. Reset on wallet switch.
  • Close-and-reopen pattern for SyncOptionsSheetEducationSheet — picker closes, education sheet opens, picker reopens with previous selection preserved on dismiss. rememberSaveable survives rotation.

Test plan

  • Unit tests: cd android && ./gradlew testDebugUnitTest — 548/548 passing. New tests added:
    • WalletPreferencesCoachmarkTest (2 tests)
    • SyncProgressFirstCatchingUpTest (5 tests on the pure edge-trigger helper)
    • EducationTopicTest (3 tests — non-zero res ids + uniqueness)
    • EducationTopicFaqLinkTest (2 tests — every topic anchor maps to a real FaqEntry; FAQ anchors are unique)
    • HomeViewModelCoachmarkTest (6 tests on the pure derivation, including clock-skew defensive case)
    • FaqRouteTest (3 tests — null/blank/populated anchor)
    • FaqViewModelTest (5 tests — initial state, blank handling, toggle, preserve-others)
  • Build: cd android && ./gradlew assembleDebug — BUILD SUCCESSFUL.
  • Lint: cd android && ./gradlew lintDebug — BUILD SUCCESSFUL, no new warnings introduced.
  • Manual smoke (must run before merge):
    1. Fresh install + new wallet → coachmark appears on Home after ~2s; "Got it" dismisses; does not reappear on app restart.
    2. Fresh install + NEW_WALLET sync mode → coachmark does not flash (sync near-instant).
    3. Tap section ? on Home Sync → education sheet opens; "Open FAQ" deep-links to sync entry expanded + scrolled into view.
    4. Tap section ? on Home Activity → same flow with activity entry.
    5. Open sync mode picker → tap inline ? on each option → picker closes, education sheet opens; dismiss → picker reopens with the previous selection.
    6. Repeat Build AuthScreen, PinEntryScreen, and SecuritySettingsScreen #5, then rotate device while education sheet is open → after recreation, picker resumes correctly on dismiss (rememberSaveable round-trip).
    7. Sync mode picker on a small phone (Pixel 4a-class) → all 4 options + custom block input + Apply/Cancel fit without clipping.
    8. Settings → Help & FAQ → FAQ screen loads; all 10 entries expand/collapse independently; deep-link param works.
    9. Block height absent from Home; still visible on Node Status.
    10. Run app under Spanish system locale → English fallback; no crash; no missing-resource exceptions.

Commit structure

15 conventional commits on this branch — one per task in the implementation plan:

  • c72bb36 feat(prefs): hasSeenSyncCoachmark flag
  • 95c5161 feat(sync): expose firstCatchingUpAtMs
  • 4467d1d + b75cbd8 feat(education): EducationTopic + collapse-into-enum refactor
  • 50707f0 feat(faq): FaqEntry data + round-trip test
  • 71b5b00 + 77adbd4 feat(education): EducationSheet + width-cap refactor
  • 2176ff4 feat(coachmark): Modifier.coachmarkAnchor + registry
  • d7f7a69 feat(coachmark): SyncCoachmark scrim + tooltip
  • eee5511 feat(home): showSyncCoachmark derivation
  • f45053d feat(sync): SyncOptionsSheet (ModalBottomSheet) with inline ? help
  • abd1f80 feat(nav): Screen.Faq route
  • 6f4c201 feat(faq): FaqScreen accordion + FaqViewModel
  • f47eada feat(home): integrate coachmark + ? help + education sheet
  • e4f1316 feat(settings): Help & FAQ row + EducationSheet integration

Related

Spec / plan

  • Spec: docs/superpowers/specs/2026-04-28-user-education-process-time-design.md
  • Plan: docs/superpowers/plans/2026-04-28-user-education-process-time.md

Summary by CodeRabbit

  • New Features

    • In-app education sheets and coachmarks for syncing, plus a persisted "seen" preference and FAQ deep-links.
    • Replaced sync-options dialog with a richer bottom-sheet sync-mode picker and clearer sync-mode UX and copy.
    • New FAQ screen reachable from multiple places and help entrypoints across the app.
  • Bug Fixes

    • More robust status updates so partial backend failures don't block UI refreshes.
    • Sync progress now tracks first "catching up" time to drive coachmark timing.
  • Tests

    • Added tests for coachmarks, FAQ routing/viewmodel, and sync-first-catching-up behavior.

Wire the education feature into HomeScreen:
- Provide CoachmarkRegistry via CompositionLocal so the sync card
  registers its bounds for the SyncCoachmark overlay
- Anchor the sync progress card with coachmarkAnchor("sync_card")
- Add `?` help icons to the Sync and Activity section headers,
  opening the EducationSheet for the matching topic
- Replace the user-facing block-height label in the sync card with
  plain-language status (Catching up / Up to date) — power users can
  still see the block height on the Node Status screen
- Wire the SyncOptionsSheet `onTopicHelp` callback (both settings and
  post-import call sites) to close-and-reopen via the EducationSheet,
  with the settings path resuming the picker on Got it
- Plumb onNavigateToFaq through NavGraph -> MainScreen -> HomeScreen
  so the EducationSheet's "Open FAQ" button deeplinks to the FAQ
  with the topic's anchor pre-expanded
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pocket-node Ready Ready Preview, Comment Apr 29, 2026 5:41pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Warning

Rate limit exceeded

@RaheemJnr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 49 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ece4d979-7724-4dd4-a7ed-ee3b22b7f390

📥 Commits

Reviewing files that changed from the base of the PR and between 74ac753 and b284b4a.

📒 Files selected for processing (3)
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeViewModel.kt
  • android/app/src/main/res/values/strings.xml
📝 Walkthrough

Walkthrough

Adds an education/FAQ system with coachmarks and a sync-options bottom sheet, tracks the first time syncing enters a "catching up" state via a timestamp on SyncProgress, persists coachmark acknowledgement in preferences, and wires FAQ deep-link routing and related UI/components.

Changes

Cohort / File(s) Summary
Sync progress & repo
android/app/src/main/java/com/rjnr/pocketnode/data/gateway/GatewayRepository.kt
Adds firstCatchingUpAtMs: Long? to SyncProgress, adds computeFirstCatchingUpAtMs(prev, catching, nowMs), introduces process-lifetime timestamp + syncPollingGeneration to avoid stale emissions, updates polling loop to compute/emit timestamp, and clears it on stop.
Preferences (coachmark)
android/app/src/main/java/com/rjnr/pocketnode/data/wallet/WalletPreferences.kt
Adds persisted boolean KEY_SYNC_COACHMARK_SEEN, exposes hasSeenSyncCoachmarkFlow: StateFlow<Boolean>, adds markSyncCoachmarkSeen() and resets coachmark state in clear().
Sync options UI
android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt, android/app/src/main/java/.../SyncOptionsDialog.kt (deleted)
Removes SyncOptionsDialog; introduces SyncOptionsSheet bottom sheet with selectable modes, saved custom block-height input, validation, optional explorer lookup button, and Cancel/Apply actions.
Education primitives & coachmarks
android/app/src/main/java/com/rjnr/pocketnode/ui/education/EducationTopic.kt, .../EducationSheet.kt, .../coachmark/CoachmarkAnchor.kt, .../coachmark/SyncCoachmark.kt
Adds EducationTopic enum and EducationSheet composable; adds CoachmarkRegistry, LocalCoachmarkRegistry, Modifier.coachmarkAnchor(key), and SyncCoachmark that scrims around an anchored rect and shows a tooltip with input interception.
Home / Settings / Onboarding wiring
android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt, .../HomeViewModel.kt, .../settings/SettingsScreen.kt, .../onboarding/MnemonicImportScreen.kt, android/app/src/main/java/com/rjnr/pocketnode/ui/MainScreen.kt
Wires coachmark registry into Home, replaces dialog→sheet for sync options across screens, adds onNavigateToFaq(anchor) propagation, HomeViewModel persists dialog visibility, adds 1s tick, showSyncCoachmark state, onCoachmarkDismissed() and shouldShowSyncCoachmark() with 2s grace logic.
FAQ system & navigation
android/app/src/main/java/com/rjnr/pocketnode/ui/navigation/NavGraph.kt, .../help/FaqEntry.kt, .../help/FaqScreen.kt, .../help/FaqViewModel.kt
Adds Screen.Faq route with optional anchor, FaqEntry/FaqEntries mapping, FaqScreen UI and FaqViewModel that initializes expanded anchors from SavedStateHandle and provides toggle/initialScrollIndex.
Strings/localization
android/app/src/main/res/values/strings.xml
Adds numerous string resources for education, sync-mode picker, coachmark/frequent FAQ content, home labels, and shared UI strings.
Status view resilience
android/app/src/main/java/com/rjnr/pocketnode/ui/screens/status/NodeStatusViewModel.kt
Makes repository reads/parsing resilient by isolating calls and parsing only when non-blank; ensures UI state updates with best-available values even if some fetches fail.
Tests added
android/app/src/test/java/.../SyncProgressFirstCatchingUpTest.kt, .../WalletPreferencesCoachmarkTest.kt, .../education/*, .../navigation/FaqRouteTest.kt, .../help/FaqViewModelTest.kt, .../home/HomeViewModelCoachmarkTest.kt
Adds unit and Robolectric tests for first-catching-timestamp logic, coachmark preference persistence, EducationTopic resources and FAQ anchor mapping, FAQ route encoding, FaqViewModel init/toggle, and HomeViewModel coachmark timing.

Sequence Diagrams

sequenceDiagram
    participant User
    participant HomeScreen
    participant HomeViewModel
    participant WalletPreferences
    participant Coachmark as SyncCoachmark

    User->>HomeScreen: observe sync progress
    HomeScreen->>HomeViewModel: subscribe to combined flows
    HomeViewModel->>HomeViewModel: computeFirstCatchingUpAtMs(prev, catching, nowMs)
    alt catching detected and grace elapsed
        HomeViewModel->>HomeScreen: emit showSyncCoachmark=true
        HomeScreen->>Coachmark: show=true (anchorKey)
        Coachmark->>HomeScreen: render scrim + tooltip
    end
    User->>Coachmark: Tap "Got it"
    Coachmark->>HomeViewModel: onCoachmarkDismissed()
    HomeViewModel->>WalletPreferences: markSyncCoachmarkSeen()
    WalletPreferences->>WalletPreferences: persist KEY_SYNC_COACHMARK_SEEN=true
    HomeViewModel->>HomeScreen: emit showSyncCoachmark=false
Loading
sequenceDiagram
    participant User
    participant Screen as Home/Settings
    participant SyncSheet as SyncOptionsSheet
    participant EduSheet as EducationSheet
    participant Nav as NavHost

    User->>Screen: Open sync options
    Screen->>SyncSheet: show=true
    User->>SyncSheet: Tap help icon
    SyncSheet->>EduSheet: open(topic)
    EduSheet->>Screen: onDismiss() (resume flag)
    alt user taps "Learn more"
        EduSheet->>Nav: navigate to Faq.routeWithAnchor(topic.faqAnchor)
        Nav->>Nav: open FaqScreen(anchor)
        FaqScreen->>FaqScreen: expand & scroll to anchor
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped where tiny timestamps lie,

I watched the sync catch up and sigh.
Two seconds wait, a friendly light,
A coachmark shows to teach what's right.
I nibbled help and sent you on your way. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR substantially implements the core education objectives from #90: sync coachmark, inline help via ? icons, EducationSheet, FAQ screen, and sync mode picker improvements. However, two noted issues (firstCatchingUpAtMs reset in stopSyncPolling, block height still in TopAppBar SyncingChip) indicate incomplete fixes for correctness and UI consistency goals. Address the reviewer's comments: ensure firstCatchingUpAtMs is reset in stopSyncPolling and remove block height from SyncingChip in TopAppBar to achieve full compliance with stated objectives.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(education): process-time user education (#90)' directly summarizes the main change: implementing a process-time user education feature as described in issue #90.
Out of Scope Changes check ✅ Passed The PR introduces necessary supporting changes (SyncProgress timestamp tracking, NodeStatusViewModel resilience, NavGraph FAQ routing) that are well-scoped to the education feature. Changes stay focused on implementing user education and sync UI improvements without unrelated modifications.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-90-education-feature

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
Review rate limit: 0/1 reviews remaining, refill in 43 minutes and 49 seconds.

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

Copy link
Copy Markdown
Contributor

@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: 5

🧹 Nitpick comments (4)
android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt (1)

73-77: Derive custom-input visibility from selectedMode to avoid state drift.

Line 77 introduces showCustomInput as a second state source for what is already represented by selectedMode. Keeping both mutable risks divergence in future edits.

♻️ Proposed simplification
-    var showCustomInput by remember(initialMode) { mutableStateOf(initialMode == SyncMode.CUSTOM) }
@@
-                    onClick = { selectedMode = SyncMode.NEW_WALLET; showCustomInput = false },
+                    onClick = { selectedMode = SyncMode.NEW_WALLET },
@@
-                    onClick = { selectedMode = SyncMode.RECENT; showCustomInput = false },
+                    onClick = { selectedMode = SyncMode.RECENT },
@@
-                    onClick = { selectedMode = SyncMode.CUSTOM; showCustomInput = true },
+                    onClick = { selectedMode = SyncMode.CUSTOM },
@@
-                    onClick = { selectedMode = SyncMode.FULL_HISTORY; showCustomInput = false },
+                    onClick = { selectedMode = SyncMode.FULL_HISTORY },
@@
-            if (showCustomInput) {
+            if (selectedMode == SyncMode.CUSTOM) {

Also applies to: 108-131, 146-154

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

In
`@android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt`
around lines 73 - 77, The visibility state showCustomInput must be removed and
derived directly from selectedMode to avoid state drift: delete the
remember/mutableStateOf declaration for showCustomInput and replace all usages
with a derived expression (selectedMode == SyncMode.CUSTOM) inside
SyncOptionsSheet (and other occurrences in the 108-131 and 146-154 areas);
ensure any code that previously updated showCustomInput now updates selectedMode
instead (e.g., when selecting the custom radio button or clearing input), and
keep customBlockHeight as its own state but render its input conditional on
(selectedMode == SyncMode.CUSTOM).
android/app/src/main/java/com/rjnr/pocketnode/ui/navigation/NavGraph.kt (1)

67-68: Apply URI encoding to the anchor parameter.

Query parameter values in Jetpack Navigation routes must be URI-encoded to safely handle reserved characters (?, &, #, spaces). While current faqAnchor values are safe, this change defensively protects against future unsafe values and follows official Navigation library conventions.

🔧 Proposed fix
     object Faq : Screen("faq?anchor={anchor}") {
         fun routeWithAnchor(anchor: String?): String =
-            if (anchor.isNullOrBlank()) "faq" else "faq?anchor=$anchor"
+            if (anchor.isNullOrBlank()) "faq" else "faq?anchor=${android.net.Uri.encode(anchor)}"
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/rjnr/pocketnode/ui/navigation/NavGraph.kt`
around lines 67 - 68, The routeWithAnchor(anchor: String?) function must
URI-encode the anchor value before embedding it into the navigation route to
avoid unsafe characters; update routeWithAnchor to return "faq" for null/blank
anchors and otherwise return "faq?anchor=" + URLEncoder.encode(anchor, "UTF-8")
(or URLEncoder.encode(anchor, StandardCharsets.UTF_8.toString())) and add the
necessary import for java.net.URLEncoder (or java.nio.charset.StandardCharsets)
so encoding is applied consistently.
android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt (1)

207-224: Consider replacing resumeSyncSheet boolean with a typed resume target.

Lines 207-224 introduce a second help entry path (showPostImportSyncDialog), but resume behavior is modeled as a single boolean and only reopens showSyncOptions(). A small enum/state-sealed target would make resume behavior explicit and future-proof for multiple picker origins.

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

In `@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt`
around lines 207 - 224, Replace the boolean resumeSyncSheet with a typed resume
target (sealed class or enum, e.g., ResumeTarget { ImportPostSync, OtherOrigin
}) and thread it through the UI state and viewModel so the origin of the
SyncOptionsSheet is explicit; update the code paths that currently toggle
resumeSyncSheet and call showSyncOptions() to instead set the resume target
(setResumeTarget(ResumeTarget.ImportPostSync) or similar) and call
showSyncOptions(target), change viewModel.hidePostImportSyncDialog() usages to
clear the resume target via a dedicated viewModel method, and in the
SyncOptionsSheet callbacks (onDismiss/onSelectMode) use the resume target to
decide the resume action (e.g., call viewModel.changeSyncMode(mode, customBlock,
target) or route back to the specific origin) so resume behavior is explicit and
future-proof for multiple picker origins (refer to SyncOptionsSheet,
showSyncOptions(), viewModel.hidePostImportSyncDialog(),
viewModel.changeSyncMode()).
android/app/src/test/java/com/rjnr/pocketnode/ui/screens/help/FaqViewModelTest.kt (1)

10-43: Consider adding direct coverage for initialScrollIndex().

This suite covers expansion state well, but it currently doesn’t assert the initialScrollIndex() behavior for valid/invalid anchors.

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

In
`@android/app/src/test/java/com/rjnr/pocketnode/ui/screens/help/FaqViewModelTest.kt`
around lines 10 - 43, Add tests that directly exercise
FaqViewModel.initialScrollIndex(): create VMs with a valid anchor (e.g.,
SavedStateHandle(mapOf("anchor" to "sync_mode"))), a blank anchor, and no
anchor, then call initialScrollIndex() and assert that a valid anchor yields a
non-negative index while blank or missing anchors yield a negative index (or the
sentinel your implementation uses); update FaqViewModelTest to include these
assertions alongside the existing expansion-state tests to cover both valid and
invalid anchor cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/com/rjnr/pocketnode/data/gateway/GatewayRepository.kt`:
- Around line 220-223: Reset the process-lifetime tracker firstCatchingUpAtMs
when polling stops to avoid stale timestamps: in the stopSyncPolling()
implementation, set firstCatchingUpAtMs = null (same place where wallet switch
clears it) so when start/restart polling begins the 2s coachmark grace is
honored; ensure the field name firstCatchingUpAtMs and the method
stopSyncPolling() are referenced exactly so the change is applied to the correct
code path.

In
`@android/app/src/main/java/com/rjnr/pocketnode/data/wallet/WalletPreferences.kt`:
- Around line 253-260: The flow can become stale after prefs are cleared because
_hasSeenSyncCoachmark is only initialized once and updated only by
markSyncCoachmarkSeen; register a
SharedPreferences.OnSharedPreferenceChangeListener in this class to keep the
StateFlow in sync: in an init block register a listener that, when
KEY_SYNC_COACHMARK_SEEN changes (or is removed), reads
prefs.getBoolean(KEY_SYNC_COACHMARK_SEEN, false) and sets
_hasSeenSyncCoachmark.value accordingly; ensure the listener references the same
prefs instance and consider unregistering it when the owning object is disposed
(if applicable).

In
`@android/app/src/main/java/com/rjnr/pocketnode/ui/education/coachmark/CoachmarkAnchor.kt`:
- Around line 36-41: The coachmarkAnchor modifier adds bounds to
registry.bounds[key] but never removes them; make the function `@Composable` (if
not already) and add a DisposableEffect tied to the key/registry that clears the
entry on unmount: after computing registry = LocalCoachmarkRegistry.current and
updating registry.bounds in Modifier.onGloballyPositioned, create
DisposableEffect(key, registry) { onDispose { registry.bounds.remove(key) } } so
registry.bounds entries are removed when the anchor composable disposes.

In `@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/help/FaqScreen.kt`:
- Around line 59-61: The back IconButton currently has Icon(...
contentDescription = null) which makes it inaccessible to screen readers; update
the Icon to use a localized content description by adding a "back" string to
resources (e.g., strings.xml) and set contentDescription =
stringResource(R.string.back) on the Icon (or IconButton) so screen readers
announce it; ensure you import androidx.compose.ui.res.stringResource and
reference the existing
onBack/IconButton/Icon(Icons.AutoMirrored.Filled.ArrowBack) symbols when making
the change.

In
`@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/onboarding/MnemonicImportScreen.kt`:
- Around line 236-244: The onTopicHelp parameter of SyncOptionsSheet is
currently a no-op, so help taps do nothing; wire it to an actual help action by
passing a handler that triggers the existing UI navigation or view model help
action (e.g., call viewModel.openSyncHelp() or viewModel.onHelpRequested("sync")
or navController.navigate("faq/sync") depending on your app navigation setup)
and ensure the viewModel exposes/implements that method (e.g.,
openSyncHelp/onHelpRequested) to show the education/FAQ UI; alternatively, hide
the help affordance in SyncOptionsSheet if no help flow is available.

---

Nitpick comments:
In
`@android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt`:
- Around line 73-77: The visibility state showCustomInput must be removed and
derived directly from selectedMode to avoid state drift: delete the
remember/mutableStateOf declaration for showCustomInput and replace all usages
with a derived expression (selectedMode == SyncMode.CUSTOM) inside
SyncOptionsSheet (and other occurrences in the 108-131 and 146-154 areas);
ensure any code that previously updated showCustomInput now updates selectedMode
instead (e.g., when selecting the custom radio button or clearing input), and
keep customBlockHeight as its own state but render its input conditional on
(selectedMode == SyncMode.CUSTOM).

In `@android/app/src/main/java/com/rjnr/pocketnode/ui/navigation/NavGraph.kt`:
- Around line 67-68: The routeWithAnchor(anchor: String?) function must
URI-encode the anchor value before embedding it into the navigation route to
avoid unsafe characters; update routeWithAnchor to return "faq" for null/blank
anchors and otherwise return "faq?anchor=" + URLEncoder.encode(anchor, "UTF-8")
(or URLEncoder.encode(anchor, StandardCharsets.UTF_8.toString())) and add the
necessary import for java.net.URLEncoder (or java.nio.charset.StandardCharsets)
so encoding is applied consistently.

In `@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt`:
- Around line 207-224: Replace the boolean resumeSyncSheet with a typed resume
target (sealed class or enum, e.g., ResumeTarget { ImportPostSync, OtherOrigin
}) and thread it through the UI state and viewModel so the origin of the
SyncOptionsSheet is explicit; update the code paths that currently toggle
resumeSyncSheet and call showSyncOptions() to instead set the resume target
(setResumeTarget(ResumeTarget.ImportPostSync) or similar) and call
showSyncOptions(target), change viewModel.hidePostImportSyncDialog() usages to
clear the resume target via a dedicated viewModel method, and in the
SyncOptionsSheet callbacks (onDismiss/onSelectMode) use the resume target to
decide the resume action (e.g., call viewModel.changeSyncMode(mode, customBlock,
target) or route back to the specific origin) so resume behavior is explicit and
future-proof for multiple picker origins (refer to SyncOptionsSheet,
showSyncOptions(), viewModel.hidePostImportSyncDialog(),
viewModel.changeSyncMode()).

In
`@android/app/src/test/java/com/rjnr/pocketnode/ui/screens/help/FaqViewModelTest.kt`:
- Around line 10-43: Add tests that directly exercise
FaqViewModel.initialScrollIndex(): create VMs with a valid anchor (e.g.,
SavedStateHandle(mapOf("anchor" to "sync_mode"))), a blank anchor, and no
anchor, then call initialScrollIndex() and assert that a valid anchor yields a
non-negative index while blank or missing anchors yield a negative index (or the
sentinel your implementation uses); update FaqViewModelTest to include these
assertions alongside the existing expansion-state tests to cover both valid and
invalid anchor cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0ddffd1-e680-4ab6-b2e2-e37580c43530

📥 Commits

Reviewing files that changed from the base of the PR and between 06413c0 and e4f1316.

📒 Files selected for processing (25)
  • android/app/src/main/java/com/rjnr/pocketnode/data/gateway/GatewayRepository.kt
  • android/app/src/main/java/com/rjnr/pocketnode/data/wallet/WalletPreferences.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/MainScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsDialog.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/education/EducationSheet.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/education/EducationTopic.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/education/coachmark/CoachmarkAnchor.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/education/coachmark/SyncCoachmark.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/navigation/NavGraph.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/help/FaqEntry.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/help/FaqScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/help/FaqViewModel.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeViewModel.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/onboarding/MnemonicImportScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/settings/SettingsScreen.kt
  • android/app/src/main/res/values/strings.xml
  • android/app/src/test/java/com/rjnr/pocketnode/data/gateway/SyncProgressFirstCatchingUpTest.kt
  • android/app/src/test/java/com/rjnr/pocketnode/data/wallet/WalletPreferencesCoachmarkTest.kt
  • android/app/src/test/java/com/rjnr/pocketnode/ui/education/EducationTopicFaqLinkTest.kt
  • android/app/src/test/java/com/rjnr/pocketnode/ui/education/EducationTopicTest.kt
  • android/app/src/test/java/com/rjnr/pocketnode/ui/navigation/FaqRouteTest.kt
  • android/app/src/test/java/com/rjnr/pocketnode/ui/screens/help/FaqViewModelTest.kt
  • android/app/src/test/java/com/rjnr/pocketnode/ui/screens/home/HomeViewModelCoachmarkTest.kt
💤 Files with no reviewable changes (1)
  • android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsDialog.kt

Comment on lines +253 to +260
private val _hasSeenSyncCoachmark =
MutableStateFlow(prefs.getBoolean(KEY_SYNC_COACHMARK_SEEN, false))
val hasSeenSyncCoachmarkFlow: StateFlow<Boolean> = _hasSeenSyncCoachmark.asStateFlow()

fun markSyncCoachmarkSeen() {
prefs.edit().putBoolean(KEY_SYNC_COACHMARK_SEEN, true).apply()
_hasSeenSyncCoachmark.value = true
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Coachmark flow can become stale after preference resets.

hasSeenSyncCoachmarkFlow is a one-time snapshot + manual setter path. If preferences are cleared/reset (clear()), this flow is not synchronized back to false, so UI can read stale state until process restart.

Suggested fix
 fun clear() {
     prefs.edit().clear().apply()
+    _hasSeenSyncCoachmark.value = false
+    _themeMode.value = readThemeMode()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/rjnr/pocketnode/data/wallet/WalletPreferences.kt`
around lines 253 - 260, The flow can become stale after prefs are cleared
because _hasSeenSyncCoachmark is only initialized once and updated only by
markSyncCoachmarkSeen; register a
SharedPreferences.OnSharedPreferenceChangeListener in this class to keep the
StateFlow in sync: in an init block register a listener that, when
KEY_SYNC_COACHMARK_SEEN changes (or is removed), reads
prefs.getBoolean(KEY_SYNC_COACHMARK_SEEN, false) and sets
_hasSeenSyncCoachmark.value accordingly; ensure the listener references the same
prefs instance and consider unregistering it when the owning object is disposed
(if applicable).

@RaheemJnr
Copy link
Copy Markdown
Owner Author

Code review

Found 2 issues:

  1. stopSyncPolling() does not reset firstCatchingUpAtMs, breaking the documented "Null whenever we are not catching up" invariant. The sibling reset path in onActiveWalletChanged at line 316 clears it explicitly; stopSyncPolling (called from HomeViewModel.changeSyncMode() and confirmNetworkSwitch()) clears syncProgressTracker and wasSyncing but leaves the timestamp set. After polling resumes, the stale timestamp can immediately satisfy the 2s grace window in HomeViewModel.shouldShowSyncCoachmark, re-triggering the coachmark for users who haven't yet seen it.

    Field declaration + invariant:

    // Process-lifetime edge-tracker for the first time the wallet entered
    // "catching up" (actively downloading blocks). Used by the HomeViewModel
    // coachmark grace timer (#90). Null whenever we are not catching up.
    private var firstCatchingUpAtMs: Long? = null
    private val _syncProgress = MutableStateFlow(SyncProgress())

    Reset gap:

    */
    fun stopSyncPolling() {
    syncPollingJob?.cancel()
    syncPollingJob = null
    syncProgressTracker.reset()
    wasSyncing = false
    Log.d(TAG, "Stopped centralized sync polling")
    }

  2. SyncingChip still renders block height in the TopAppBar while syncing, contradicting both the PR's stated goal ("Block height removed from Home") and the in-file comment immediately above SyncProgressBar ("Plain-language status — no block height (User education  #90). Power users can find block height on Node Status."). SyncProgressBar correctly hides it; SyncingChip does not, and is composed in the TopAppBar actions slot during every active sync.

    In-file comment:

    )
    // Plain-language status — no block height (#90).
    // Power users can find block height on Node Status.
    Text(
    text = if (isSyncing) {

    Block-height literal in SyncingChip:

    )
    Text(
    text = "Block ${syncedToBlock ?: ""}",
    style = MaterialTheme.typography.labelSmall,
    color = MaterialTheme.colorScheme.primary

    Call site in TopAppBar:

    actions = {
    if (uiState.isSyncing) {
    SyncingChip(syncedToBlock = uiState.syncedToBlock)
    } else {
    SyncedChip()

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

stopSyncPolling cancels the polling job that drives the
computeFirstCatchingUpAtMs edge-tracker, so the field had no
natural way to return to null after sync mode change or network
switch. Stale timestamp could immediately satisfy the 2s
coachmark grace window on next polling start.

Match the reset pattern already used in onActiveWalletChanged:
clear the field plus strip it from the last-emitted SyncProgress
so HomeViewModel's combine doesn't see stale state during the gap.
Issue #90 specifies block height is hidden on Home and only
visible on Node Status. SyncProgressBar honored that, but
SyncingChip in the TopAppBar still rendered Block N during
every active sync cycle, contradicting the design and the
in-file comment one block above.

Replace the Block N text with a plain Syncing label.
clear() wiped SharedPreferences but left _hasSeenSyncCoachmark
and _themeMode StateFlows holding their pre-clear values, so any
collector reading them between clear() and process restart saw
stale state.

Re-seed both flows to match the post-clear prefs state.
Without DisposableEffect, an anchor that left the composition
left its bounds in CoachmarkRegistry forever — a coachmark for
that key would aim at coordinates that no longer correspond to a
visible component.
Screen readers announced the FAQ back IconButton as unlabeled.
The post-import sync sheet had onTopicHelp wired to a no-op so
the help icons rendered but did nothing. Add showHelpIcons param
(default true) and pass false from MnemonicImportScreen so the
icons are not surfaced where there's no education flow.

Re-opening the sheet from an EducationSheet stack would be risky
in this constrained context, so suppressing the affordance is
preferred over wiring it up.
Copy link
Copy Markdown
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt (1)

209-227: ⚠️ Potential issue | 🟠 Major

Disable help affordances in the post-import sheet or restore the sheet on dismiss.

This path hides showPostImportSyncDialog before opening education, but unlike the settings flow it never sets resumeSyncSheet, so dismissing the help sheet drops the user out of the required post-import sync selection entirely.

Suggested fix
         SyncOptionsSheet(
             currentMode = SyncMode.RECENT,
             title = "Choose Sync Start Point",
             description = "Select how far back to sync your imported wallet's history. If your wallet is older than 30 days, choose Custom.",
             availableModes = listOf(SyncMode.RECENT, SyncMode.CUSTOM),
             onDismiss = { viewModel.hidePostImportSyncDialog() },
             onSelectMode = { mode, customBlock ->
                 viewModel.hidePostImportSyncDialog()
                 if (mode != SyncMode.RECENT) {
                     viewModel.changeSyncMode(mode, customBlock)
                 }
             },
             onTopicHelp = { topic ->
                 viewModel.hidePostImportSyncDialog()
                 educationTopic = topic
             },
             tipBlockNumber = tipBlockNumberLong,
-            onLookupAddressOnExplorer = onLookupBlockHeight
+            onLookupAddressOnExplorer = onLookupBlockHeight,
+            showHelpIcons = false,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt`
around lines 209 - 227, The post-import SyncOptionsSheet currently hides the
sheet then opens education (viewModel.hidePostImportSyncDialog() and
educationTopic = topic) but never restores the sheet on dismiss, causing the
flow to drop out; fix by either disabling the help affordance (remove or no-op
onTopicHelp) or—preferably—mirror the settings flow by setting the resume flag
before opening education (e.g., set resumeSyncSheet = true or call
viewModel.setResumeSyncSheet(true)) and then hide the sheet, so when the
education UI is dismissed the sheet is restored; ensure onDismiss still calls
viewModel.hidePostImportSyncDialog() and that the resume flag is consumed when
re-showing SyncOptionsSheet.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/com/rjnr/pocketnode/data/gateway/GatewayRepository.kt`:
- Around line 2477-2495: The race occurs because an in-flight
getAccountStatus().onSuccess lambda can write firstCatchingUpAtMs after
stopSyncPolling() cancels; fix by introducing a polling generation/token: add a
mutable pollingGeneration counter incremented when starting/stopping polling (in
start/stopSyncPolling), capture the current generation in the onSuccess lambda
before calling computeFirstCatchingUpAtMs and _syncProgress.value, and only
update firstCatchingUpAtMs/_syncProgress when the captured generation matches
the repository's current pollingGeneration; reference
computeFirstCatchingUpAtMs, firstCatchingUpAtMs, getAccountStatus().onSuccess,
stopSyncPolling()/cancel(), and the _syncProgress.value assignment so the
in-flight lambda ignores stale emissions.

---

Outside diff comments:
In `@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt`:
- Around line 209-227: The post-import SyncOptionsSheet currently hides the
sheet then opens education (viewModel.hidePostImportSyncDialog() and
educationTopic = topic) but never restores the sheet on dismiss, causing the
flow to drop out; fix by either disabling the help affordance (remove or no-op
onTopicHelp) or—preferably—mirror the settings flow by setting the resume flag
before opening education (e.g., set resumeSyncSheet = true or call
viewModel.setResumeSyncSheet(true)) and then hide the sheet, so when the
education UI is dismissed the sheet is restored; ensure onDismiss still calls
viewModel.hidePostImportSyncDialog() and that the resume flag is consumed when
re-showing SyncOptionsSheet.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a206192-ae97-41ef-ba31-62ec584d7a9e

📥 Commits

Reviewing files that changed from the base of the PR and between e4f1316 and 5408b75.

📒 Files selected for processing (8)
  • android/app/src/main/java/com/rjnr/pocketnode/data/gateway/GatewayRepository.kt
  • android/app/src/main/java/com/rjnr/pocketnode/data/wallet/WalletPreferences.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/education/coachmark/CoachmarkAnchor.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/help/FaqScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/onboarding/MnemonicImportScreen.kt
  • android/app/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (2)
  • android/app/src/main/java/com/rjnr/pocketnode/data/wallet/WalletPreferences.kt
  • android/app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (4)
  • android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/help/FaqScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/onboarding/MnemonicImportScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/education/coachmark/CoachmarkAnchor.kt

Coroutine cancellation is cooperative — getAccountStatus() can
have already returned a successful Result before stopSyncPolling()
calls cancel(), in which case the .onSuccess lambda still runs to
completion and resurrects firstCatchingUpAtMs / _syncProgress
after stopSyncPolling cleared them.

Add @volatile syncPollingGeneration. Each start captures the
current generation; stopSyncPolling bumps the counter before
cancel(); the onSuccess lambda's first action is a generation
check that returns early on mismatch. Closes the race window
flagged by code review on the prior fix.
Replace the 4-rect Canvas scrim with a single Path using
Difference op so the spotlight has rounded corners matching the
sync card. Apply explicit ButtonDefaults colors and a fontWeight
on Got it so it renders visibly against the card background.

Reported in manual smoke.
Manual smoke wanted a sense of progress on the sync card.
Replace plain Catching up... with the dynamic
"Catching up from <current> to <tip>", with the values rendered
in SemiBold via AnnotatedString. Block numbers formatted with
locale-aware thousands separators.
…nge (#90)

The outer try/catch in updateStatus swallowed JNI exceptions
(common during sync-mode restart) and short-circuited before
_uiState.update. Old tipHeader / peers / scripts stuck on stale
values until the user backed out and reopened Node Status.

Wrap each repository call individually in runCatching with safe
defaults so the state update always runs and reflects the current
JNI state — including a deliberate null tipHeader during the
brief restart window.
When the user tapped "Look up on explorer" from the CUSTOM
block-height field, Chrome opened and the OEM memory manager +
auth gate killed the process. On return, PIN unlock dropped them
on Home with the dialog gone — they had to reopen and retype.

Persist showSyncOptionsDialog into HomeViewModel.SavedStateHandle
so the dialog re-opens on ViewModel re-creation. Switch the
sheet's typed customBlockHeight / selectedMode / showCustomInput
to rememberSaveable so partial input survives process death too.
Copy link
Copy Markdown
Contributor

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt`:
- Around line 67-68: SyncOptionsSheet's parameter tipBlockNumber was changed
from String/Int to Long; update every call site (notably the SettingsScreen call
that currently passes uiState.tipBlockNumber) to supply a Long. In
SettingsScreen (and any other callers), convert the string/nullable value the
same way HomeScreen does: use toLongOrNull() ?: 0L (or otherwise coerce/parse to
a Long) and pass that result into SyncOptionsSheet; ensure the parameter name
tipBlockNumber is used so the correct overload is selected.

In `@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt`:
- Around line 214-215: The two hardcoded UI strings in HomeScreen.kt (the
sheet's title and description currently set to "Choose Sync Start Point" and the
longer sentence) should be moved into strings.xml and referenced from the
Compose UI using the resource API; add two entries like
choose_sync_start_point_title and choose_sync_start_point_description to
strings.xml, then replace the literal title and description in the HomeScreen's
sheet call with stringResource(R.string.choose_sync_start_point_title) and
stringResource(R.string.choose_sync_start_point_description) (or
context.getString(...) if not using Compose's stringResource) so the sheet copy
is localizable and consistent with other resource-backed text.
- Around line 224-227: The onTopicHelp handler currently closes the post-import
flow (calls viewModel.hidePostImportSyncDialog()) and sets educationTopic, but
there is no logic to reopen the post-import sheet (showPostImportSyncDialog) so
users cannot return to choose CUSTOM; update the handler in HomeScreen.kt (the
onTopicHelp lambda) to either: (A) record a resume flag (e.g., set a boolean
like resumePostImportAfterHelp in the ViewModel) and reopen the post-import
sheet when the help dialog is dismissed (or when educationTopic is cleared), or
(B) suppress/disable the help icon in the post-import/ CUSTOM variant so
onTopicHelp cannot be triggered there; implement the chosen path by modifying
viewModel.hidePostImportSyncDialog() calls and the ViewModel state to support
resumePostImportAfterHelp and reopening showPostImportSyncDialog, or by gating
the help UI when the post-import variant is active.

In
`@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeViewModel.kt`:
- Around line 76-97: The code only persists showSyncOptionsDialog across process
death; add persistent save/restore for showPostImportSyncDialog as well by
introducing a new key (e.g. SAVED_KEY_SHOW_POST_IMPORT_SYNC), initialize
HomeUiState's showPostImportSyncDialog from
savedStateHandle.get<Boolean>(SAVED_KEY_SHOW_POST_IMPORT_SYNC) ?: false, and
update any places that toggle or set showPostImportSyncDialog to also write the
boolean into savedStateHandle.put(SAVED_KEY_SHOW_POST_IMPORT_SYNC, value) so the
post-import SyncOptionsSheet is restored the same way showSyncOptionsDialog is.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4345e0a0-4c44-438f-9f6e-b11977372287

📥 Commits

Reviewing files that changed from the base of the PR and between 5408b75 and 74ac753.

📒 Files selected for processing (7)
  • android/app/src/main/java/com/rjnr/pocketnode/data/gateway/GatewayRepository.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/education/coachmark/SyncCoachmark.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeViewModel.kt
  • android/app/src/main/java/com/rjnr/pocketnode/ui/screens/status/NodeStatusViewModel.kt
  • android/app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (3)
  • android/app/src/main/res/values/strings.xml
  • android/app/src/main/java/com/rjnr/pocketnode/ui/education/coachmark/SyncCoachmark.kt
  • android/app/src/main/java/com/rjnr/pocketnode/data/gateway/GatewayRepository.kt

Comment on lines +67 to +68
savedCustomBlockHeight: Long? = null,
tipBlockNumber: Long = 0L,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Update all SyncOptionsSheet call sites to pass a Long.

tipBlockNumber is now a Long, but android/app/src/main/java/com/rjnr/pocketnode/ui/screens/settings/SettingsScreen.kt:191-205 still passes uiState.tipBlockNumber directly. With the current API, that screen will stop compiling unless it does the same toLongOrNull() ?: 0L conversion as HomeScreen.

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

In
`@android/app/src/main/java/com/rjnr/pocketnode/ui/components/SyncOptionsSheet.kt`
around lines 67 - 68, SyncOptionsSheet's parameter tipBlockNumber was changed
from String/Int to Long; update every call site (notably the SettingsScreen call
that currently passes uiState.tipBlockNumber) to supply a Long. In
SettingsScreen (and any other callers), convert the string/nullable value the
same way HomeScreen does: use toLongOrNull() ?: 0L (or otherwise coerce/parse to
a Long) and pass that result into SyncOptionsSheet; ensure the parameter name
tipBlockNumber is used so the correct overload is selected.

Comment on lines 214 to 215
title = "Choose Sync Start Point",
description = "Select how far back to sync your imported wallet's history. If your wallet is older than 30 days, choose Custom.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Move the new post-import sheet copy into strings.xml.

These are new user-facing strings in the education flow, but they're still hardcoded here. That leaves this path non-localizable and inconsistent with the rest of the sheet copy, which is resource-backed.

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

In `@android/app/src/main/java/com/rjnr/pocketnode/ui/screens/home/HomeScreen.kt`
around lines 214 - 215, The two hardcoded UI strings in HomeScreen.kt (the
sheet's title and description currently set to "Choose Sync Start Point" and the
longer sentence) should be moved into strings.xml and referenced from the
Compose UI using the resource API; add two entries like
choose_sync_start_point_title and choose_sync_start_point_description to
strings.xml, then replace the literal title and description in the HomeScreen's
sheet call with stringResource(R.string.choose_sync_start_point_title) and
stringResource(R.string.choose_sync_start_point_description) (or
context.getString(...) if not using Compose's stringResource) so the sheet copy
is localizable and consistent with other resource-backed text.

The smoke fix landed save/restore for showSyncOptionsDialog, but
the user's actual flow was the post-import sheet
(showPostImportSyncDialog) — it shows right after wallet import
to ask which start point to sync from. That sheet ALSO surfaces
the explorer-lookup link, so it has the same Chrome → process
death → return failure mode, and the previous fix didn't cover it.

Adds:
- SAVED_KEY_SHOW_POST_IMPORT_SYNC + restore on init.
- savedStateHandle write on every showPostImportSyncDialog mutation
  (hidePostImportSyncDialog, showPostImportSyncDialog, importWallet
  success path).
- resumePostImportSheet flag in HomeScreen so EducationSheet
  dismiss reopens the right sheet (post-import vs. settings).
Adds home_post_import_sync_title and home_post_import_sync_description
so the post-import flow is localizable alongside the rest of the
sync sheet copy. Per CodeRabbit review on PR #135.
@RaheemJnr RaheemJnr merged commit 8d6c0cd into main Apr 29, 2026
5 checks passed
@RaheemJnr RaheemJnr deleted the feat/issue-90-education-feature branch April 29, 2026 17:47
@RaheemJnr RaheemJnr mentioned this pull request Apr 29, 2026
3 tasks
RaheemJnr added a commit that referenced this pull request Apr 29, 2026
* chore(release): bump to 1.6.0 (versionCode 7)

Bumps the app version for the user-education release covering
PRs #134 and #135, which closed #90.

Adds the F-Droid changelog entry for versionCode 7.

* chore(release): retarget version bump to 1.5.1

More changes are queued before 1.6.0; this release ships the
user-education work as a patch instead.
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.

User education

1 participant