Skip to content

refactor(ui): migrate protected sheets to DashUIKit - #1075

Merged
romchornyi merged 2 commits into
developfrom
refactor/dashuikit-protected-sheets
Aug 28, 2026
Merged

refactor(ui): migrate protected sheets to DashUIKit#1075
romchornyi merged 2 commits into
developfrom
refactor/dashuikit-protected-sheets

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 26, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Follow-up to #1073 and #1074, using the dismissal controls introduced by
dashpay/DashUIKit#14. Protected signing, locking, proving, and broadcast flows still owned
their grabber/title shell and used a content-level interactiveDismissDisabled workaround.
That prevented swipe dismissal but could not coordinate DashUIKit's close affordance for
sheets already using the shared component.

This pull request is intentionally stacked on refactor/dashuikit-sheet-content.

What was done?

  • Pinned DashUIKit to the exact API revision from feat(bottom-sheet): add dismissal controls DashUIKit#14 while that dependency
    is under review.
  • Migrated internal-transfer confirmation, shielded recovery, send confirmation, CoinJoin
    move-funds, and evonode withdrawal confirmation to DashUIKit.BottomSheet.
  • Preserved each host's cancellation/completion callbacks and existing detents.
  • Routed protected state through isDismissalEnabled, disabling swipe and the shared close
    control together during authorization, signing, locking, proving, submitting, and
    broadcasting as appropriate.
  • Removed the migrated sheets' duplicate grabbers, titles, backgrounds, and local interactive
    dismissal modifiers.
  • Updated username registration sheets to use DashUIKit dismissal controls, disabling swipe
    and the shared close control together during an active operation.
  • Kept marketplace name detail, set-price, and transfer as native detented sheets with native
    interactive-dismissal protection.
  • Left SDKIdentityProfileSheet and WalletsScreen on native SwiftUI sheets; their remaining
    interactiveDismissDisabled modifiers are intentional.

How Has This Been Tested?

  • xcodebuild -resolvePackageDependencies -workspace DashWallet.xcworkspace -scheme dashpay
  • Full dashpay Debug builds for iPhone 17 Pro / iOS 26.5 Simulator at exact base
    bb34895b65769d5849b97aea56adb39dcf3c6982 and exact head
    9198db97fe6f58f69efcfa5d7062d59530800463, using separate worktrees, separate DerivedData,
    normal Simulator signing, and the required local-only service plists.
  • Both builds installed, launched, and remained alive as org.dashfoundation.dash on clones
    of the same shut-down wallet fixture.
  • Exercised the internal transparent-to-shielded confirmation at idle and during the locking
    phase. Accessibility reported the head's close control as disabled while protected.
  • Validated the package lock as JSON, confirmed DashUIKit resolved to
    e8d92434bfc28fbf933b896cd40a01dd61835b5f, and ran git diff --check.
  • Simplification review found no safe consolidation with positive value; independent
    correctness, quality, architecture, and reliability passes approved the migration with no
    findings.
  • The dependency PR separately passes 7 focused Swift tests and an iOS 14 Simulator build for
    the dismissal API.

Exact provenance, device identifiers, full-resolution originals, dimensions, and SHA-256
hashes: visual evidence tree

Idle confirmation

Before — exact base After — full head
Before idle confirmation After idle confirmation

Protected locking phase

Before — exact base After — full head
Before protected transfer After protected transfer

Breaking Changes

None. This wallet change depends on dashpay/DashUIKit#14 landing first.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests (host composition
    is validated in Simulator; the shared dismissal behavior has focused tests in DashUIKit)
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

This pull request was created by Codex.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 176ba56a-fa3c-412e-a296-89c190ee1f7c

📥 Commits

Reviewing files that changed from the base of the PR and between b5273c4 and a0e4c38.

⛔ Files ignored due to path filters (1)
  • DashWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved is excluded by !**/Package.resolved
📒 Files selected for processing (6)
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
  • DashWallet/Sources/UI/Home/Views/CoinJoinMoveFundsSheet.swift
  • DashWallet/Sources/UI/Menu/Tools/Masternode Withdrawal/EvonodeWithdrawalScreen.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferConfirmSheet.swift
  • DashWallet/Sources/UI/Payments/Pay/SendScreen.swift

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

DashUIKit is pinned to a fixed revision. Marketplace, CoinJoin, Evonode withdrawal, internal transfer, and send confirmation sheets now use BottomSheet for presentation and dismissal control.

Changes

BottomSheet migration

Layer / File(s) Summary
Marketplace dismissal wiring
DashWallet.xcodeproj/project.pbxproj, DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
DashUIKit is pinned to a fixed revision. Marketplace sheets configure dismissal through BottomSheet while actions are running.
Action sheet container migration
DashWallet/Sources/UI/Home/Views/CoinJoinMoveFundsSheet.swift, DashWallet/Sources/UI/Menu/Tools/Masternode Withdrawal/EvonodeWithdrawalScreen.swift
CoinJoin and Evonode withdrawal sheets replace custom headers and containers with BottomSheet. Dismissal remains restricted during active operations.
Payment sheet container migration
DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferConfirmSheet.swift, DashWallet/Sources/UI/Payments/Pay/SendScreen.swift
Transfer and send confirmation sheets use BottomSheet titles, close handling, and in-flight dismissal settings.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to a0e4c

This change centralizes protected sheet dismissal behavior while preserving callbacks and detents; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: quantumexplorer, jeanpierreroma, llbartekll

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 5…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: migrating protected UI sheets to DashUIKit.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 5 files. (1 skipped: 1 unsupported.)

✨ 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 refactor/dashuikit-protected-sheets

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.

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

@coderabbitai review


🤖 Posted autonomously by Codex on behalf of pasta.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

@PastaPastaPasta I will review pull request #1075.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@PastaPastaPasta
PastaPastaPasta force-pushed the refactor/dashuikit-sheet-content branch from b5273c4 to bb34895 Compare August 27, 2026 00:11
@PastaPastaPasta
PastaPastaPasta force-pushed the refactor/dashuikit-protected-sheets branch from a0e4c38 to 9198db9 Compare August 27, 2026 00:12
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

@coderabbitai review\n\n---\n🤖 Posted autonomously by Codex on behalf of pasta.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

@PastaPastaPasta I will review pull request #1075.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@romchornyi romchornyi left a comment

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.

The migration itself is wired correctly — I traced all five onClose: handlers and each one really does dismiss (confirmation = nil, showConfirm = false, showConfirmation = false, pendingShieldedRecovery = nil, showCoinJoinMoveFundsSheet = false), so there is no dead X. The problems are the pinned revision, and the fact that the newly-added X is wired to one handler for every phase.

The pin is wrong in both directions

e8d9243 is the first commit of DashUIKit#14's branch, not its merged result. Three commits from that same branch are excluded — b8f3159 (separate the close button from interactive dismissal), ab9eac1 (block the swipe on iOS 14), and 8097d16 (fix: keep the sheet's identity when dismissal flips).

So the PR is written against semantics that no longer exist upstream, and both states are broken:

Staying on e8d9243 means shipping without 8097d16. That commit exists because BottomSheetDismissalModifier branched on isEnabled inside a @ViewBuilder, producing _ConditionalContent; flipping isDismissalEnabled at runtime swaps branches and SwiftUI tears the subtree down and rebuilds it. Flipping that flag at runtime is exactly what this PR does, on five money-moving sheets, at the moment signing starts.

Moving the pin to master silently defeats the protection this PR is built to add. The merge changed the contract:

// e8d9243 — the pinned revision      // master (0fe7fcd and later)
guard isEnabled else { return }        if let onClose { onClose() }
if let onClose { onClose() }           else if isDismissalEnabled { dismiss() }
else { dismiss() }

and added isCloseButtonEnabled, with the button active when isCloseButtonEnabled && (isDismissalEnabled || hasCustomCloseAction). Every call site here passes onClose:, so hasCustomCloseAction is true and isCloseButtonEnabled defaults to true — the X stays tappable during signing, locking, proving and broadcasting, and fires onCancel. The PR body describes the pin as temporary ("while that dependency is under review"), but #14 is already merged, so lifting it is a one-line change that is unlikely to prompt a re-review of all five sites.

The fix is both halves together: track master, and pass isCloseButtonEnabled: !isInFlight at each site. Note master has moved again since — DashUIKit#16 merged as 83cf65a.

The X ignores the sheet's phase

onClose: takes a single handler, but these sheets have phases whose own buttons call something different. In .success and .submittedUnconfirmed the button calls onCompleted; the X calls onCancel. Verified concretely in SendScreen and EvonodeWithdrawalScreen — details inline.

Smaller

Package.resolved changes the requirement kind from branch to revision but leaves originHash at its previous value. If SwiftPM treats it as stale it re-resolves and rewrites the lock, so the intended pin can drift; a CI job resolving with automatic resolution disabled would fail on the mismatch instead.

All five migrated bodies still end in ButtonsGroup/DashButton with .padding(.bottom, 16), calibrated for the old safe-area-respecting VStack. BottomSheet(fillsHeight: true) applies edgesIgnoringSafeArea(.bottom) — which the base commit b8cf2aa9 in this very stack documents — so those buttons now sit in the home-indicator strip. I assessed that from the layout rather than on a device; worth a look on hardware.


🤖 Reviewed with Claude Code

Comment thread DashWallet.xcodeproj/project.pbxproj Outdated
branch = master;
kind = branch;
kind = revision;
revision = e8d92434bfc28fbf933b896cd40a01dd61835b5f;

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.

This is an intermediate commit of DashUIKit#14, not its merged result — b8f3159, ab9eac1 and 8097d16 are all on that branch and all excluded.

The one that matters most is 8097d16, whose message describes the exact failure this PR would ship:

BottomSheetDismissalModifier branched on isEnabled inside a @ViewBuilder […] Flipping isDismissalEnabled at runtime — the use the API is built for, locking the sheet while signing or broadcasting and unlocking it afterwards — swapped branches, and SwiftUI answers that by tearing the subtree down and building the other one from scratch.

And lifting the pin to master is not a safe no-op either: the merge made perform call onClose unconditionally and introduced isCloseButtonEnabled, so the X would become live during the protected phases (see the review body).

Please track master (83cf65a after DashUIKit#16) and add isCloseButtonEnabled: !isInFlight to each of the five call sites.

title: NSLocalizedString("Confirm", comment: ""),
showBackButton: .constant(false),
isDismissalEnabled: .constant(!isInFlight),
onClose: onCancel

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.

onClose is wired to onCancel for every phase, but the sheet's own buttons are not.

In .success the button is action: onCompleted (line 839) and in .submittedUnconfirmed it is onDone: onCompleted (line 739). The host defines them differently:

onCancel:    { showConfirm = false }
onCompleted: { showConfirm = false; onSendCompleted() }

So after a successful send, tapping the newly-added X closes the sheet without onSendCompleted() — the user lands back on the amount screen with the amount still filled in, which is an easy accidental second send. Route close per phase, or make the success phases hide the X.

title: NSLocalizedString("Confirm withdrawal", comment: "Evonode withdrawal"),
showBackButton: .constant(false),
isDismissalEnabled: .constant(!(isInFlight || isUnconfirmed)),
onClose: onCancel

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.

Same phase mismatch, and here the completion handler does more than dismiss:

onCancel:    { showConfirmation = false }
onCompleted: { remaining in showConfirmation = false; onWithdrawn(remaining); dismiss() }

Tapping the X on the success screen runs onCancel, so onWithdrawn(remaining) never fires and the withdrawal screen stays up showing the stale pre-withdrawal claimable balance. onUnconfirmedAcknowledged is skipped the same way.

title: NSLocalizedString("Confirm", comment: ""),
showBackButton: .constant(false),
isDismissalEnabled: .constant(!isInFlight),
onClose: onCancel

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.

Same shape: in .success / .submittedUnconfirmed the X runs onCancel and skips onCompleted(), so the transfer screen is never told the transfer finished.

Lower impact than the other two because onCancel here is { confirmation = nil } and the screen refreshes anyway — but the X makes it the obvious exit, where previously only a swipe could reach this state.

} else {
DashUIKit.BottomSheet.selfSizing(
showBackButton: .constant(false),
isDismissalEnabled: .constant(!viewModel.isPerformingAction),

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.

Concrete consequence of the missing 8097d16 on the pinned revision: tapping Register/Request sets isPerformingAction = true, which flips isDismissalEnabled and — at e8d9243 — swaps _ConditionalContent branches, so SwiftUI rebuilds RegisterNameSheet from scratch.

Its @State goes with it: precheck and voteState reset to nil, the .task re-fires contestPrecheck/contestState, and the contest cards blank back to loading exactly while the action is running — then rebuild again when it finishes.

The four transfer sheets take the same rebuild on every start and end of a transfer; this one is just the easiest to observe.

title: NSLocalizedString("Move your mixed coins", comment: "CoinJoin"),
showBackButton: .constant(false),
isDismissalEnabled: .constant(!viewModel.isInFlight),
onClose: onDismiss

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.

This is the only migrated sheet whose initial detent is .medium (HomeView.swift:502, [.medium, .large]).

The BottomSheet chrome is 82pt (18pt grabber + 64pt navigation bar) against roughly 51pt for the handle and title it replaces, and choiceBody is a plain non-scrolling VStack — amount, explainer, two destination cards, "Later". The extra ~31pt comes out of the bottom, so on a shorter device the "Later" button can clip, and unlike the .large hosts there is no scroll fallback.

Worth either an explicit detent that accounts for the chrome, or making the body scrollable — b8cf2aa9 in this stack took the scroll route for TransferTimingSheet for the same reason.

DashUIKit#14 is merged, so pin back to `branch = master` (resolved at
`83cf65a`, which also carries #16) instead of `e8d9243`. That commit was
the first of #14's branch and predates three of its own follow-ups —
notably `8097d16`, which stops `BottomSheetDismissalModifier` from
branching on `isEnabled` inside a `@ViewBuilder`. Without it, flipping
`isDismissalEnabled` swaps `_ConditionalContent` branches and SwiftUI
rebuilds the sheet's content from scratch — every time a transfer starts
and ends.

Tracking master changes the dismissal contract, so the call sites have to
follow:

- `perform` now calls `onClose` unconditionally, and the button is live
  when `isCloseButtonEnabled && (isDismissalEnabled || hasCustomCloseAction)`.
  Every sheet here passes `onClose`, so without `isCloseButtonEnabled` the
  X would stay tappable through signing, locking, proving and broadcasting.
  Each site now mirrors its own `isDismissalEnabled` condition.
- `onClose` took a single handler while these sheets have phases whose own
  buttons call something else. Route close per phase: on success the send
  and internal-transfer sheets run `onCompleted` (which unwinds the flow)
  and the evonode sheet runs `onCompleted(remaining)` (which reports the
  new balance), rather than `onCancel`, which only closes the sheet.

`UsernameMarketplaceScreen` passes no `onClose`, so its close button is
already gated by `isDismissalEnabled` alone and needs no change.

Also make `CoinJoinMoveFundsSheet`'s choice body scrollable: it is the only
migrated sheet presented at `.medium`, and the 82pt of chrome came out of
the bottom, where "Later" — the only exit that records the deferral — sits.

@romchornyi romchornyi left a comment

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.

All findings from my earlier review are addressed in 4a627bda0, which I pushed to this branch.

  • The pin is back to branch = master, resolved at 83cf65a (master tip, which also carries DashUIKit#16). That brings in 8097d16, so flipping isDismissalEnabled no longer swaps _ConditionalContent branches and rebuilds the sheet content — which also resolves the RegisterNameSheet teardown without any change at that call site.
  • isCloseButtonEnabled added at the five sites that pass onClose, each mirroring its own isDismissalEnabled condition. Tracking master is only safe with this: perform now calls onClose unconditionally, and the button is live when isCloseButtonEnabled && (isDismissalEnabled || hasCustomCloseAction).
  • Close is routed per phase. On success the send and internal-transfer sheets run onCompleted (which unwinds the flow) and the evonode sheet runs onCompleted(remaining) (which reports the new balance), instead of onCancel, which only closed the sheet.
  • CoinJoinMoveFundsSheet's choice body scrolls, so "Later" stays reachable inside the .medium detent.
  • originHash is no longer a concern: restoring branch = master restores the dependency declaration the hash covers, and it now matches #1073 and #1074 again.

UsernameMarketplaceScreen needed no change — it passes no onClose, so its close button is gated by isDismissalEnabled alone.

Verified with a full dashpay build resolving DashUIKit at master, then installed on an iPhone 17 Pro / iOS 26.5 simulator and exercised by the reviewer. The bottom-padding question I raised earlier — whether the migrated sheets' buttons land in the home-indicator strip — was checked on device and is fine, so no padding change was made.

One note for whoever merges: this branch, like #1073 and #1074, is 31 commits behind develop and does not compile as-is — ContestedNamesService.swift:83 and SwiftDashSDKHost.swift:941,958 still call dpnsActiveContests / loadFromPersistor without await, which the current SDK requires. A squash merge applies only this PR's diff, so the merged result is fine; it is local verification that needs develop merged in first. Worth knowing, because CI here runs no build check.


🤖 Reviewed with Claude Code

Base automatically changed from refactor/dashuikit-sheet-content to develop August 28, 2026 13:26
@romchornyi
romchornyi merged commit f6aa8b1 into develop Aug 28, 2026
3 checks passed
@romchornyi
romchornyi deleted the refactor/dashuikit-protected-sheets branch August 28, 2026 13:26
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.

3 participants