Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ struct DocumentWithPriceView: View {
.foregroundColor(.secondary)
}
}
.onAppear {
// `onChange(of: documentId)` does NOT fire when the id is
// established as the binding mounts — e.g. PurchaseDocumentView
// seeds `documentIdField` in its own `onAppear` and disables this
// view, so relying on onChange alone the price probe would never
// run and Purchase would stay gated off forever. Kick the fetch
// here so a pre-seeded id loads its price without a user edit.
// `.disabled(true)` only blocks hit-testing, not lifecycle events,
// so this still fires in the Purchase flow. When the field starts
// empty (the editable transition flow) this is a no-op and the
// existing onChange path continues to handle typing.
if !documentId.isEmpty {
handleDocumentIdChange(documentId)
}
}
Comment on lines +120 to +134

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Nitpick: Guard onAppear fetch to avoid re-probing an already-loaded document

onAppear unconditionally calls handleDocumentIdChange(documentId), which clears documentExists, documentPrice, fetchedDocument, transitionState.canPurchaseDocument, and transitionState.documentPrice before re-arming a 500ms debounce. If the host sheet re-appears (sheet detent change, return from a nested presentation, parent remount), the Purchase button will briefly disable and the price will flash to nil. Guarding with if !documentId.isEmpty && fetchedDocument == nil && !isLoading preserves the pre-seeded fetch fix while avoiding redundant re-probes and transient state stomps.

source: ['claude']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining the fetchedDocument == nil guard: it would regress the submit button. PurchaseDocumentView gates Purchase / Broadcast on the app-wide transitionState.documentPrice, which its own .onAppear clears to nil — the unconditional re-probe here is precisely what re-publishes that value (via fetchDocument's MainActor.run). With the guard, a sheet re-appear would skip the re-probe and leave transitionState.documentPrice stuck at nil → button permanently disabled, the exact bug this PR fixes. The transient flash is the acceptable cost of keeping the gating correct without a larger refactor. The clean way to also kill the flash is to gate submit on local @State instead of the shared transitionState (the approach in the dropped 588435aff4) — left as a follow-up to keep this PR's diff minimal.

🤖 Addressed by Claude Code

}

private func handleDocumentIdChange(_ newValue: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,19 +275,29 @@ struct DocumentDetailView: View {
private var availableActions: [DocumentAction] {
var actions: [DocumentAction] = []
let docType = documentTypeRow
// `tradeMode == 1` is DirectPurchase — the only mode that supports
// listing/buying — matching the marketplace gating in
// TransitionInputView (`$0.tradeMode == 1`).
let tradeable = (docType?.tradeMode ?? 0) == 1

if ownerIsControlled {
actions.append(.replace)
actions.append(.delete)
if docType?.documentsTransferable == true {
actions.append(.transfer)
}
if (docType?.tradeMode ?? 0) > 0 {
if tradeable {
actions.append(.setPrice)
}
} else if (docType?.tradeMode ?? 0) > 0 && !nonOwnerControlledIdentities.isEmpty {
// Not the owner, but the wallet holds another identity that
// could buy a for-sale, tradeable document.
}
// Surface Purchase whenever the doc type is tradeable and the wallet
// holds a controlled identity that isn't the owner (the buyer ≠
// owner, and the buyer signs). This intentionally also covers a doc
// owned by another *controlled* identity — the two-identities-in-one
// -app flow — not just externally-owned docs, matching the doc
// comment above. The Purchase sheet resolves the real on-chain price
// / for-sale state and disables the button when it isn't for sale.
if tradeable && !nonOwnerControlledIdentities.isEmpty {
actions.append(.purchase)
}
return actions
Expand Down
Loading