feat(dashpay): prompt for a temporary username alongside a contested submission - #989
Conversation
…submission
A contested DPNS name is only preregistered — it belongs to nobody until
masternode voting resolves (~90 min testnet, ~2 weeks mainnet), so a
first-time DashPay signup that picks one has no reachable username for
the whole wait. The contested confirmation alert is now a sheet that
prompts the user to register a non-contested companion ("temporary")
username to the same identity in the same authorized flow: one PIN
prompt, same signer, registered right after the contested submission.
The sheet seeds a guaranteed non-contested suggestion (any digit 2-9
takes a label out of the contested character set), validates it with
the same local rules plus a debounced DPNS availability check, and
makes skipping an explicit secondary action. The copy states the name
stays theirs permanently and that a vote win makes them reachable at
both usernames.
The companion registers via the coordinator's new optional
temporaryUsername parameter (threaded through the bridge as
pendingTemporaryUsername with the preferredFundingSource lifecycle, so
a retry keeps it; the invitation path passes it directly). Its failure
is deliberately non-fatal — the contested submission has already
succeeded — and surfaces as a partial-outcome note in the voting
alert. On completion the companion becomes the DWGlobalOptions mirror
username while the contested label stays deferred; finalizeWon only
backfills an empty mirror, so a later vote win adds the second name
instead of displacing the first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe username setup flow now supports an optional temporary username for contested registrations. The UI validates and submits both names. The bridge and coordinator preserve state across retries and report temporary registration outcomes. ChangesTemporary Username Registration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CreateUsernameViewController
participant CreateUsernameViewModel
participant DWIdentityRegistrationBridge
participant DWIdentityRegistrationCoordinator
participant IdentitySigner
CreateUsernameViewController->>CreateUsernameViewModel: Submit primary and temporary usernames
CreateUsernameViewModel->>DWIdentityRegistrationBridge: Store temporary username
CreateUsernameViewModel->>DWIdentityRegistrationBridge: Start registration
DWIdentityRegistrationBridge->>DWIdentityRegistrationCoordinator: Forward temporary username
DWIdentityRegistrationCoordinator->>IdentitySigner: Register contested primary username
DWIdentityRegistrationCoordinator->>IdentitySigner: Register temporary username
IdentitySigner-->>DWIdentityRegistrationCoordinator: Return registration outcome
DWIdentityRegistrationCoordinator-->>CreateUsernameViewController: Report temporary registration result
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swift (1)
514-525: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
registrationOutcomeuses the single-slotpendingLabeland can drop the contested outcome.
DWContestedNameStatusService.pendingLabelreturns only the OLDEST in-flight contested label for the active network;pendingLabelsis the multi-label store. If the wallet already holds an older unresolved contested submission,pendingLabelreturns that older label,labelsMatchfails, and this function returns.success.The consequence is now larger than before: the form shows the "Username registered" alert for a name that is only submitted for voting, and both
registeredTemporaryUsernameandtemporaryUsernameErrorare discarded, so the user never learns whether the companion name landed.
DWIdentityRegistrationCoordinator.handlePhaseChangedecides the same question withDWContestedNameStatusService.shared.isPendingLabel(username)(Line 1328). Use the same predicate here so the two consumers agree.🐛 Proposed fix
private func registrationOutcome(for username: String) -> UsernameRegistrationOutcome { - guard let pending = DWContestedNameStatusService.shared.pendingLabel, - DWContestedNameStatusService.labelsMatch(pending, username) else { + guard DWContestedNameStatusService.shared.isPendingLabel(username) else { return .success }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swift` around lines 514 - 525, Update registrationOutcome(for:) to determine pending status with DWContestedNameStatusService.shared.isPendingLabel(username), matching the predicate used by DWIdentityRegistrationCoordinator.handlePhaseChange. Preserve the existing .success and .submittedForVoting outcomes, including the coordinator’s temporary username fields.
🧹 Nitpick comments (6)
DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationBridge.swift (2)
189-197: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLog the temporary username in
retryas well.
retrycapturestemporaryUsernameand forwards it to the coordinator, but the log line omits it.startCreateUsernamelogs it on Line 168. The retry path is the one where the preserved value matters most, so the asymmetry hides the state that is actually used.♻️ Proposed fix
- Self.logger.info("🪪 IDENT-BRIDGE :: retry username=\(username, privacy: .public) funding=\(source.logLabel, privacy: .public)") + Self.logger.info("🪪 IDENT-BRIDGE :: retry username=\(username, privacy: .public) funding=\(source.logLabel, privacy: .public) temporary=\(temporaryUsername ?? "none", privacy: .public)")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationBridge.swift` around lines 189 - 197, Update the retry log in the identity registration bridge to include the captured temporaryUsername value, matching the diagnostic information logged by startCreateUsername while preserving the existing username and funding fields.
136-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the redundant
@objcattribute.The class is annotated
@objcMembers, so@objcon this stored property is redundant. SwiftLint reportsredundant_objc_attributeon Line 143.♻️ Proposed fix
- `@objc` public var pendingTemporaryUsername: String? + public var pendingTemporaryUsername: String?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationBridge.swift` around lines 136 - 143, Remove the redundant `@objc` attribute from the pendingTemporaryUsername property; the enclosing `@objcMembers` class already exposes it to Objective-C.Source: Linters/SAST tools
DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift (2)
848-865: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe
isContestedSubmissioncondition is unreachable as a false branch.The guard at Lines 461-468 already rejects a non-nil
temporaryUsernamewhenusernameis not contested, sotemporaryUsername != nilimpliesisContestedSubmission == trueat Line 848. The extra term is harmless, but it suggests a second, independent rule that does not exist. Keep it only if you intend it as a defensive assertion, and say so in the comment.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift` around lines 848 - 865, Simplify the conditional in the temporary DPNS registration block around temporaryUsernameError so it checks only for a non-nil temporaryUsername; the earlier validation already guarantees contested status. Remove the redundant isContestedSubmission term, or explicitly document it as an intentional defensive assertion if retaining it.
448-455: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftSwiftLint reports a
cyclomatic_complexityerror onstartCreateUsername.The rule is violated at Line 449 with a complexity of 36 against a limit of 10, and the tool reports it at error severity. The new temporary-username guard and Step 3.6 branch add to an already large function. If the lint run gates the build, this fails it.
Extract the funding-source branch (Lines 657-722) and the contested post-registration work (Lines 812-865) into private methods to bring the count down.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift` around lines 448 - 455, Reduce cyclomatic complexity in startCreateUsername by extracting the funding-source branch into a private helper and the contested post-registration work into another private helper. Preserve the existing control flow, parameters, return behavior, and error handling while updating startCreateUsername to delegate to those helpers; keep the temporary-username guard and Step 3.6 behavior unchanged.Source: Linters/SAST tools
DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swift (2)
527-543: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe truncation branch is unreachable for a contested label.
DWContestedNameStatusService.isContestedLabelonly returns true for labels of 19 characters or fewer, andprepareTemporaryUsernameSuggestionruns from the contested confirmation sheet.DW_MAX_USERNAME_LENGTHis 23, sosuggestion.count >= DW_MAX_USERNAME_LENGTHat Line 539 cannot be true on that path. Keep the guard as defensive code, but state that intent in the comment so a reader does not look for the case that triggers it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swift` around lines 527 - 543, Clarify the inline comment on the truncation branch in prepareTemporaryUsernameSuggestion to state that it is defensive/unreachable for contested labels because the contested-name flow limits their length, while retaining the existing truncation behavior.
545-586: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the shared local username rules.
Lines 555-563 repeat the length, illegal-character, and hyphen-placement checks from
validateUsername(Lines 646-648). Lines 569-586 repeat the debounce, availability call, and stale-result guard fromcheckIfBlocked(Lines 744-746 and 841-843). Two copies of the same rules will drift when one side changes, and the two fields must stay consistent because both feed the same DPNS registration.Extract one private helper that returns the local verdict for a trimmed label, and call it from both validators.
♻️ Sketch of the shared helper
+ /// Local-only DPNS label rules shared by the main field and the + /// companion field. Returns nil when the label passes every rule. + private func localRuleFailure(for label: String) -> TemporaryUsernameCheck? { + guard label.count >= DW_MIN_USERNAME_LENGTH && label.count <= DW_MAX_USERNAME_LENGTH else { + return .invalidLength + } + guard label.rangeOfCharacter(from: illegalChars) == nil, + label.first != "-", label.last != "-" else { + return .invalidCharacters + } + return nil + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swift` around lines 545 - 586, Extract the duplicated local validation rules from validateTemporaryUsername and validateUsername into one private helper that accepts a trimmed label and returns the shared local verdict, including empty, length, character, hyphen, and contested-name outcomes. Update both validators to use this helper and preserve their existing debounce, availability, and stale-result handling for labels that pass local validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationBridge.swift`:
- Around line 296-299: Update the phase-handling logic around the registration
flow so pendingTemporaryUsername is cleared when registration is cancelled and
when processing legacy plain submissions that did not intentionally set it.
Preserve the property only through an intentional retry, and retain the existing
completed-phase cleanup in the relevant identity registration method.
In
`@DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewController.swift`:
- Around line 961-982: Update the temporary-username initialization flow around
prepareTemporaryUsernameSuggestion and validateTemporaryUsername so the seeded
value is passed through local validation synchronously when the sheet appears,
immediately setting temporaryUsernameCheck to .checking and showing the
validation row. Preserve the existing throttled/debounced availability request
for the eventual server result.
- Around line 1004-1007: Update the temporary username suggestion flow around
prepareTemporaryUsernameSuggestion so it re-seeds when the current main username
differs from the username used for the last seeding, rather than returning
solely because temporaryUsername is non-empty. Track the last-seeded main
username in CreateUsernameViewModel and preserve the existing cached suggestion
only when it was generated for the current main username.
---
Outside diff comments:
In
`@DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swift`:
- Around line 514-525: Update registrationOutcome(for:) to determine pending
status with DWContestedNameStatusService.shared.isPendingLabel(username),
matching the predicate used by
DWIdentityRegistrationCoordinator.handlePhaseChange. Preserve the existing
.success and .submittedForVoting outcomes, including the coordinator’s temporary
username fields.
---
Nitpick comments:
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationBridge.swift`:
- Around line 189-197: Update the retry log in the identity registration bridge
to include the captured temporaryUsername value, matching the diagnostic
information logged by startCreateUsername while preserving the existing username
and funding fields.
- Around line 136-143: Remove the redundant `@objc` attribute from the
pendingTemporaryUsername property; the enclosing `@objcMembers` class already
exposes it to Objective-C.
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swift`:
- Around line 848-865: Simplify the conditional in the temporary DPNS
registration block around temporaryUsernameError so it checks only for a non-nil
temporaryUsername; the earlier validation already guarantees contested status.
Remove the redundant isContestedSubmission term, or explicitly document it as an
intentional defensive assertion if retaining it.
- Around line 448-455: Reduce cyclomatic complexity in startCreateUsername by
extracting the funding-source branch into a private helper and the contested
post-registration work into another private helper. Preserve the existing
control flow, parameters, return behavior, and error handling while updating
startCreateUsername to delegate to those helpers; keep the temporary-username
guard and Step 3.6 behavior unchanged.
In
`@DashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swift`:
- Around line 527-543: Clarify the inline comment on the truncation branch in
prepareTemporaryUsernameSuggestion to state that it is defensive/unreachable for
contested labels because the contested-name flow limits their length, while
retaining the existing truncation behavior.
- Around line 545-586: Extract the duplicated local validation rules from
validateTemporaryUsername and validateUsername into one private helper that
accepts a trimmed label and returns the shared local verdict, including empty,
length, character, hyphen, and contested-name outcomes. Update both validators
to use this helper and preserve their existing debounce, availability, and
stale-result handling for labels that pass local validation.
🪄 Autofix
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 Plus
Run ID: 5eb57659-8db8-4971-aea1-4df471aadfe1
📒 Files selected for processing (5)
DashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationBridge.swiftDashWallet/Sources/Infrastructure/SwiftDashSDK/Identity/DWIdentityRegistrationCoordinator.swiftDashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewController.swiftDashWallet/Sources/UI/DashPay/Setup/CreateUsername/CreateUsernameViewModel.swiftDashWallet/en.lproj/Localizable.strings
- Sanitize the bridge's pendingTemporaryUsername at read time: a PIN-cancelled attempt never reaches the .completed cleanup and the legacy DWDashPayModel.createUsername: entry point never writes the property, so a stale companion could fail an unrelated submission at the coordinator's pre-flight pairing guard. Values that don't pair validly with the submitted label are dropped and cleared (logged); an intentional retry of the same contested attempt still keeps it. - Validate the seeded companion suggestion synchronously so the sheet's rule row and primary button don't sit blank/disabled through the pipeline's 500 ms throttle; a temporaryCheckLabel cache (mirroring availabilityCheckLabel) keeps the later same-label emission from restarting the availability check. - Re-seed the companion suggestion when the main username changed since the last seeding, instead of carrying a name derived from an abandoned contested label into the next confirmation sheet. - registrationOutcome now checks membership across all pending contested entries (isPendingLabel) instead of the single-slot oldest pendingLabel, so an older unresolved contest can't make a new contested submission read as a completed registration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sion; correct the fee copy Two gaps from testing the flow against a wallet already waiting on a vote: - The temporary-username prompt only existed inside the signup flow. An identity whose FIRST username went to a vote — submitted on an older build, or with the prompt skipped — had no surface offering it a reachable name. UsernameRequestStatusScreen now carries an "Add a temporary username" section (DASHPAY-gated; the screen also compiles in the dashwallet target): explainer, validated field with the seeded suggestion, and a Register action through UsernameMarketplaceService.register(label:) against the existing identity (own PIN gate; PIN cancel is a silent no-op; failures alert). Shown only while the vote is unresolved and the identity owns no other username; flips to a confirmation on success. - The contested confirmation claimed "Your Dash will be locked until voting completes." That is false: the contest fee prefunds the vote poll's specialized balance and its leftover is swept into the network's processing pools at poll end (rs-drive-abci clean_up_after_contested_resources_vote_polls_end) — nothing is returned to the contender, win or lose. Both message variants now say the fee is spent at submission and not returned. To keep the two surfaces from drifting, the companion-name validation moved out of CreateUsernameViewModel into a shared TemporaryUsernameFieldModel (input pipeline, local rules, non-contested gate, debounced availability, suggestion seeding) with a shared TemporaryUsernameField view; the confirmation sheet and the status screen both bind to it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Issue being fixed or feature implemented
A contested DPNS name is only preregistered — it belongs to nobody until masternode voting resolves (~90 min testnet, ~2 weeks mainnet), so a first-time DashPay signup that picks one leaves the user with no reachable username for the entire wait. This PR prompts the user, at the moment they confirm a contested submission, to also register a non-contested temporary username they keep permanently — making it clear that when the contest closes in their favor they will be reachable at both usernames.
What was done?
Contested confirmation alert → sheet (
CreateUsernameViewController.swift): the "Submit anyway" alert is nowContestedNameConfirmationSheet. It keeps the existing vote-wait / locked-Dash warning (both wording variants, and the last-moment join-window re-check), and adds a temporary-username field seeded with a guaranteed non-contested suggestion (<name>2— any digit 2–9 takes a label out of the contested character set). Primary action "Submit both usernames"; skipping is an explicit secondary action; swipe-down cancels.Live validation for the companion label (
CreateUsernameViewModel.swift): same length/character rules as the main field, a must-not-be-contested gate (a contested companion would silently open a second vote), and a debounced DPNS availability check, surfaced through the existingValidationCheckrow styling.One authorized flow (
DWIdentityRegistrationCoordinator.swift):startCreateUsernamegains an optionaltemporaryUsername. The companion registers right after the contested submission's bookkeeping — same identity, same signer, no second PIN prompt — with a pre-flight guard rejecting invalid pairings before any money moves. Companion failure is deliberately non-fatal (the contested submission already succeeded) and surfaces as a partial-outcome note in the "Username submitted" alert.Mirror semantics: on completion the temporary name becomes the
DWGlobalOptionsactive username while the contested label stays deferred as before.finalizeWononly backfills an empty mirror, so a vote win adds the contested name to the identity without displacing the temporary one — both end up live. The voting banner keeps working since it keys off the pending-contest bookmark, not the mirror.Bridge threading (
DWIdentityRegistrationBridge.swift):pendingTemporaryUsernamefollows thepreferredFundingSourcelifecycle (written by the form before submit, preserved across.failedso retry keeps it, cleared on.completed). The invitation-claim path passes the parameter directly.Nine new user-facing strings added to
en.lproj/Localizable.strings.Post-submission surface (
UsernameRequestStatusScreen.swift,d41f38ac1): the prompt originally existed only inside the signup flow, so an identity whose first username was already in a vote (submitted on an older build, or with the prompt skipped) was never offered a reachable name. The request-status screen now carries an "Add a temporary username" section — same shared validated field (TemporaryUsernameFieldModel, extracted so the two surfaces can't drift), registering to the existing identity viaUsernameMarketplaceService.register(label:). Shown only while the vote is unresolved and the identity owns no other username;#if DASHPAY-gated because the screen also compiles in the dashwallet target.Copy correction: the confirmation previously claimed "Your Dash will be locked until voting completes" — false: the contest fee prefunds the vote poll's specialized balance, whose leftover is swept into the network's processing pools at poll end (
rs-drive-abciclean_up_after_contested_resources_vote_polls_end); nothing returns to the contender. Both variants now say the fee is spent at submission and not returned.How Has This Been Tested?
dashpayscheme (arm64 iOS Simulator), no new warnings in touched files.Breaking Changes
None.
Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code
Summary by CodeRabbit