Order action state machine — pure logic (steps 1 + 2) - #4
Merged
Merged
Conversation
added 7 commits
May 15, 2026 08:51
Pure state machine that drives the smart per-row layout for
<PaymentHistoryWithSupport>. Maps (Order, now) → three independent
outputs:
- statusText : informational line always rendered
- action : action button variant (none | resume | raise-dispute)
with remaining ms for countdown rendering
- disputeState : chain's view of the dispute lifecycle
Dispute windows match the on-chain enforcement in
contracts-v4/contracts/facets/OrderProcessorFacet.sol#raiseDispute:
BUY/PAID is disputable [15m, 24h] after placedAt; SELL or PAY/COMPLETED
is disputable [30m, 7d] after placedAt. Dispute lifecycle short-circuits
status flow — a raised or settled dispute is always the most relevant
state.
`now` is injected for deterministic tests and to support a one-time
clock-skew correction at hook init (chain block.timestamp vs browser
Date.now()), wired in step 3.
Inline countdown text in statusText (eg "Paid · dispute opens in 8m")
reuses formatRemaining which lives in the same module — compact
representation optimised for narrow action buttons: <60s as "<n>s",
<60m as "<n>m", <24h as "<h>h <m>m", else "<d>d <h>h". Clamps to "0s"
on non-positive / non-finite input.
No UI in this commit. Steps 3+ build on top.
Tests (node:test): 17 cases on order-action covering BUY/SELL/PAY ×
status × dispute states × elapsed boundaries; 10 cases on
format-remaining covering each format boundary + clamp behaviour. Full
widget test suite: 99/99 (70 pure + 29 vitest).
Steps 3-6 of the smart action stack. Three layers per row:
A — Status text (always rendered)
B — Action button: Resume order / Raise dispute · countdown
(suppressed when not actionable now)
C — Support button: dispute-open / dispute-resolved / chat-active /
chat-new
PaymentHistoryWithSupport gains actionMode: "chat" | "smart" defaulting
to "smart". Legacy "chat" preserves the prior single-button layout.
New artifacts:
src/widgets/OrderAction.tsx composes the three layers per row,
1s tick for countdowns, optimistic
flip on dispute broadcast
src/widgets/RaiseDisputeStep.tsx confirm → form → submitting → done /
error state machine, encodes the
raiseDispute(orderId, redactTransId)
payload, fires onSubmitted on tx
broadcast (hash known, receipt
pending) for snappy optimistic UI
src/hooks/useOrderStates.ts multicall3-batched on-chain reads +
1s sub-tick when any row's
countdown is under 60s; shipped as
a building block for embedders
without a PaymentHistory feed
src/core/order-action.ts pure (Order, now) → state predicate
+ formatRemaining(ms); merged into
one file so node:test resolves
without an experimental TS loader
for cross-file imports
Support widget gains chatState: "active" | "new" prop. With
disputeStatus="none" the launcher label adapts:
- chatState="active" → "Continue support" + green pip
- chatState="new" → "Get help"
Dispute variants are unchanged: "View dispute" / "View resolution".
DIAMOND_ABI extended with raiseDispute(uint256,uint256) so
RaiseDisputeStep can encode the tx in the browser.
Tests:
node:test 69 cases (computeOrderAction + formatRemaining)
vitest 57 cases (Support / RaiseDisputeStep / OrderAction /
useOrderStates / PaymentHistoryWithSupport / Support
launcher chat-state matrix)
Backward compat:
- actionMode default flips to "smart". Existing tests opt back into
"chat" to keep asserting legacy visuals.
- SupportProps.chatState is optional; absent → "new" → "Get help".
V1 decisions (locked):
- smart default; existing embedders see the new layout next update
- Resume button suppressed when onResumeOrder callback is absent
- Optimistic flip on tx broadcast; snap back on next poll if revert
- Multicall3 hook ships but is unused by PaymentHistoryWithSupport's
smart mode (PaymentHistory's subgraph fetch suffices; multicall is
a building block for embedders without it). Multicall address
pinned to 0xcA11bde05977b3631167028862bE2a173976CA11.
When `actionMode="smart"` and `support.onResumeOrder` is not explicitly
set, reuse the embedder's existing `onResume` prop (already passed for
the legacy chat-mode Resume button). Makes the smart-mode flip a
zero-config upgrade for embedders like p2p-checkout/merchant-app that
already wire `onResume={handleResume}` against PaymentHistory.
`support.onResumeOrder` still wins when present — explicit override for
embedders that want different resume semantics in smart mode.
Vocabulary + visual rethink for the smart per-row layout.
Per-row presentation is now ONE Contact Support affordance with chain
state driving the rendering:
inside the review window, no dispute filed:
outline chip + draining doughnut + countdown
click → ReportProblemStep modal
dispute raised:
plain button with red dot
click → chat
dispute resolved:
plain button with green dot
click → chat (read-only thread)
otherwise (pre-window / post-window / non-disputable):
not rendered. Chat is gated on the dispute being on chain per the
protocol design.
User-facing copy avoids "dispute" everywhere: "Contact Support",
"review opens in", "review window closed", "Under review",
"Resolved", "Report submitted", "Could not submit report".
Internal renames:
- RaiseDisputeStep → ReportProblemStep (file + component + props)
- action.kind "raise-dispute" → "report-problem"
- support.onDisputeRaised → support.onReportSubmitted
- new export: ContactSupport (the row affordance wrapper)
Action variant gains a `filled` field (0.0..1.0) for the doughnut, set
on entry to the disputable window at 1.0 and draining to 0.0 at close.
The 1s tick inside OrderAction recomputes both `remainingMs` and
`filled` so the ring updates without an extra timer.
Bridge typing trade-off: the dispute-raised dot ramp lives in the
plain-button variant; the in-window chip relies solely on the ring
colour + countdown — no dot, the ring is the indicator.
Bridge tests still pass (52 vitest + 69 node:test). Two paths were
renamed:
test/RaiseDisputeStep.test.tsx → test/ReportProblemStep.test.tsx
Removed mock of <Support> from OrderAction tests, replaced with
mock of <ContactSupport>.
V1 production-ready. PR #4 superseded prior commit's design.
Three V1-blocking bugs uncovered during live testing.
(1) State machine wrong for BUY orders.
OrderProcessorFacet#raiseDispute rejects BUY/PAID — the contract gates
the dispute path on status=CANCELLED AND paidTimestamp != 0 (the
user paid but the order auto-cancelled before the merchant completed).
My state machine was showing the Contact Support chip on BUY/PAID
rows, which let users submit a tx the chain would always revert.
- BUY/paid → no action, status "Paid · awaiting merchant completion"
- BUY/cancelled + paidAt > 0 → review window [15m, 24h] after place,
status "Cancelled · contact support to recover funds"
- BUY/cancelled + paidAt == 0 → no action, status "Cancelled"
- SELL/PAY paths unchanged (status=COMPLETED + [30m, 7d])
(2) White text on white modal in dark-theme hosts.
The Modal portal renders outside the widget root, so theme CSS vars
applied on an ancestor don't reach inside. ReportProblemStep +
ContactSupport's chat-loading / chat-error views now set
background + colour + padding directly from `color.surface` /
`color.text` so the dark-theme merchant-app's white-on-white is fixed.
(3) Contract reverts surfaced as "Execution reverted for an unknown
reason."
ReportProblemStep now pre-simulates the on-chain raiseDispute via
viem.simulateContract using the diamond ABI (extended with the seven
custom errors thrown by raiseDispute: NotAuthorized, DisputeTimeNotReached,
DisputeTimeExpired, InvalidOrderType, InvalidOrderStatusToRaiseDispute,
CannotRaiseDisputeTwice, DisputeAlreadySettled). Reverts get decoded
into human-readable messages — no gas spent on a doomed tx. New props
`rpcUrl` + `chainId` flow PaymentHistoryWithSupport → OrderAction →
ContactSupport → ReportProblemStep.
Tests:
node:test 70 cases (BUY paid no-action; cancelled BUY paths with
paidAt>0 inside/before/after window; cancelled BUY
paidAt==0 no-action)
vitest 53 cases (new: pre-flight revert decodes
DisputeTimeNotReached without invoking the wallet)
Backwards-compat: kind-name "report-problem" unchanged; public exports
unchanged. SDK Order.paidAt fed verbatim from the subgraph layer is
the source of truth for the BUY/cancelled gating.
Per user feedback memory: in widget user-facing surfaces, drop
protocol-internal terms ("merchant", "dispute", "circle admin"). The
widget embeds in third-party apps where end users don't know or care
about the protocol's role names.
Replacements:
"Placed · awaiting merchant" → "Placed · matching"
"Accepted · awaiting merchant payment" → "Accepted · processing payment"
"Paid · awaiting merchant completion" → "Paid · processing payment"
"Merchant paid · awaiting your confirmation"
→ "Payment received · confirm to complete"
Confirm-step copy in ReportProblemStep also de-merchantified:
"the merchant has not completed the order" → "your paid order has not been completed"
Internal symbol names in code (`acceptedMerchant`, `disputeStatus`,
`raiseDispute`) unchanged — the rule applies only to text the end
user reads.
Tests updated; 70 node:test + 53 vitest green.
…ollback External code-review pass surfaced five concerns. All addressed. 1. **Mainnet footgun: testnet-only default diamond.** Old: `DEFAULT_DIAMOND_ADDRESS` hardcoded Base Sepolia. A Base mainnet host that forgets the `diamondAddress` prop silently calls a non-existent contract. New: `resolveDiamondAddress(chainId, override)` looks up by chainId and throws with a clear message if the chain isn't registered. Legacy export kept as `@deprecated` for back-compat. Used by `ReportProblemStep`. 2. **`Promise.all` aborts both reads on a transient `getBlock` failure.** Multicall result was being thrown away when only the clock-skew refresh failed. Split: multicall is awaited unconditionally; `getBlock` is wrapped in its own try/catch and leaves `clockSkewMs` at the prior value on error. 3. **`useOrderStates` hardcoded `paidAt: 0n`.** Structurally killed the BUY/CANCELLED dispute eligibility predicate (which gates on `paidAt > 0n`). Now multicalls `getAdditionalOrderDetails` in parallel with `getOrdersById` and populates `paidAt`, `acceptedAt`, `fixedFeePaid`, `tipsPaid`, `actualUsdtAmount`, `actualFiatAmount` from it. Doubles the contracts per row (2 per orderId) but stays one multicall round-trip per refresh. 4. **Optimistic dispute flip never rolled back on revert.** Once `setOptimisticDispute(true)` fired we never cleared it. If the tx reverted at confirmation the chip permanently showed `View report` pointing at chat that didn't exist. New: spawn a `waitForTransactionReceipt` against the read RPC; if the receipt reports `status !== "success"`, clear the local flag and let the chain poll surface the real state. Timeout 60s; on timeout / not found also clear (the chain poll will reconcile either way). 5. **`extractRevert` didn't walk viem's `BaseError` chain.** Wallet- side reverts surface as `TransactionExecutionError` whose decoded `errorName` lives one level deeper than the old probe walked. Use viem's `BaseError.walk((e) => e instanceof ContractFunctionRevertedError)` first; fall back to the manual `.cause` probe for non-viem shapes (tests, raw wallet errors). Tests added: - useOrderStates: getBlock failure no longer erases multicall result. - useOrderStates: `paidAt` populated from getAdditionalOrderDetails. - useOrderStates partial-failure mock updated for the new 2-call-per-row shape. 70 node:test + 55 vitest still green. Typecheck + build clean. Pending nits from the review (logged for follow-up, not V1 blockers): - N intervals for N rows in `useNowTick` (cosmetic perf). - Modal aria-modal duplicated landmarks + no focus trap. - `useActiveSupportTickets` deps array uses signer object identity. - dead `originApp` prop in ContactSupport.
Software-Artist-Aash
added a commit
that referenced
this pull request
May 15, 2026
Ships the smart per-row action layout for <PaymentHistoryWithSupport> (PR #4): pure (Order, now) → state machine, OrderAction + RaiseDisputeStep + useOrderStates components, and new chatState / actionMode / txSigner props. All additions are non-breaking; the prior single-launcher layout is preserved verbatim under actionMode="chat". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gitchadd
added a commit
that referenced
this pull request
Aug 21, 2026
Vocabulary + visual rethink for the smart per-row layout.
Per-row presentation is now ONE Contact Support affordance with chain
state driving the rendering:
inside the review window, no dispute filed:
outline chip + draining doughnut + countdown
click → ReportProblemStep modal
dispute raised:
plain button with red dot
click → chat
dispute resolved:
plain button with green dot
click → chat (read-only thread)
otherwise (pre-window / post-window / non-disputable):
not rendered. Chat is gated on the dispute being on chain per the
protocol design.
User-facing copy avoids "dispute" everywhere: "Contact Support",
"review opens in", "review window closed", "Under review",
"Resolved", "Report submitted", "Could not submit report".
Internal renames:
- RaiseDisputeStep → ReportProblemStep (file + component + props)
- action.kind "raise-dispute" → "report-problem"
- support.onDisputeRaised → support.onReportSubmitted
- new export: ContactSupport (the row affordance wrapper)
Action variant gains a `filled` field (0.0..1.0) for the doughnut, set
on entry to the disputable window at 1.0 and draining to 0.0 at close.
The 1s tick inside OrderAction recomputes both `remainingMs` and
`filled` so the ring updates without an extra timer.
Bridge typing trade-off: the dispute-raised dot ramp lives in the
plain-button variant; the in-window chip relies solely on the ring
colour + countdown — no dot, the ring is the indicator.
Bridge tests still pass (52 vitest + 69 node:test). Two paths were
renamed:
test/RaiseDisputeStep.test.tsx → test/ReportProblemStep.test.tsx
Removed mock of <Support> from OrderAction tests, replaced with
mock of <ContactSupport>.
V1 production-ready. PR #4 superseded prior commit's design.
vvictor-dev
pushed a commit
to vvictor-dev/widgets
that referenced
this pull request
Aug 22, 2026
* core: computeOrderAction + formatRemaining (pure logic, no UI)
Pure state machine that drives the smart per-row layout for
<PaymentHistoryWithSupport>. Maps (Order, now) → three independent
outputs:
- statusText : informational line always rendered
- action : action button variant (none | resume | raise-dispute)
with remaining ms for countdown rendering
- disputeState : chain's view of the dispute lifecycle
Dispute windows match the on-chain enforcement in
contracts-v4/contracts/facets/OrderProcessorFacet.sol#raiseDispute:
BUY/PAID is disputable [15m, 24h] after placedAt; SELL or PAY/COMPLETED
is disputable [30m, 7d] after placedAt. Dispute lifecycle short-circuits
status flow — a raised or settled dispute is always the most relevant
state.
`now` is injected for deterministic tests and to support a one-time
clock-skew correction at hook init (chain block.timestamp vs browser
Date.now()), wired in step 3.
Inline countdown text in statusText (eg "Paid · dispute opens in 8m")
reuses formatRemaining which lives in the same module — compact
representation optimised for narrow action buttons: <60s as "<n>s",
<60m as "<n>m", <24h as "<h>h <m>m", else "<d>d <h>h". Clamps to "0s"
on non-positive / non-finite input.
No UI in this commit. Steps 3+ build on top.
Tests (node:test): 17 cases on order-action covering BUY/SELL/PAY ×
status × dispute states × elapsed boundaries; 10 cases on
format-remaining covering each format boundary + clamp behaviour. Full
widget test suite: 99/99 (70 pure + 29 vitest).
* Smart per-row layout: useOrderStates + OrderAction + RaiseDisputeStep
Steps 3-6 of the smart action stack. Three layers per row:
A — Status text (always rendered)
B — Action button: Resume order / Raise dispute · countdown
(suppressed when not actionable now)
C — Support button: dispute-open / dispute-resolved / chat-active /
chat-new
PaymentHistoryWithSupport gains actionMode: "chat" | "smart" defaulting
to "smart". Legacy "chat" preserves the prior single-button layout.
New artifacts:
src/widgets/OrderAction.tsx composes the three layers per row,
1s tick for countdowns, optimistic
flip on dispute broadcast
src/widgets/RaiseDisputeStep.tsx confirm → form → submitting → done /
error state machine, encodes the
raiseDispute(orderId, redactTransId)
payload, fires onSubmitted on tx
broadcast (hash known, receipt
pending) for snappy optimistic UI
src/hooks/useOrderStates.ts multicall3-batched on-chain reads +
1s sub-tick when any row's
countdown is under 60s; shipped as
a building block for embedders
without a PaymentHistory feed
src/core/order-action.ts pure (Order, now) → state predicate
+ formatRemaining(ms); merged into
one file so node:test resolves
without an experimental TS loader
for cross-file imports
Support widget gains chatState: "active" | "new" prop. With
disputeStatus="none" the launcher label adapts:
- chatState="active" → "Continue support" + green pip
- chatState="new" → "Get help"
Dispute variants are unchanged: "View dispute" / "View resolution".
DIAMOND_ABI extended with raiseDispute(uint256,uint256) so
RaiseDisputeStep can encode the tx in the browser.
Tests:
node:test 69 cases (computeOrderAction + formatRemaining)
vitest 57 cases (Support / RaiseDisputeStep / OrderAction /
useOrderStates / PaymentHistoryWithSupport / Support
launcher chat-state matrix)
Backward compat:
- actionMode default flips to "smart". Existing tests opt back into
"chat" to keep asserting legacy visuals.
- SupportProps.chatState is optional; absent → "new" → "Get help".
V1 decisions (locked):
- smart default; existing embedders see the new layout next update
- Resume button suppressed when onResumeOrder callback is absent
- Optimistic flip on tx broadcast; snap back on next poll if revert
- Multicall3 hook ships but is unused by PaymentHistoryWithSupport's
smart mode (PaymentHistory's subgraph fetch suffices; multicall is
a building block for embedders without it). Multicall address
pinned to 0xcA11bde05977b3631167028862bE2a173976CA11.
* PaymentHistoryWithSupport: smart mode falls back to embedder's onResume
When `actionMode="smart"` and `support.onResumeOrder` is not explicitly
set, reuse the embedder's existing `onResume` prop (already passed for
the legacy chat-mode Resume button). Makes the smart-mode flip a
zero-config upgrade for embedders like p2p-checkout/merchant-app that
already wire `onResume={handleResume}` against PaymentHistory.
`support.onResumeOrder` still wins when present — explicit override for
embedders that want different resume semantics in smart mode.
* Replace separate Raise + Support buttons with one Contact Support chip
Vocabulary + visual rethink for the smart per-row layout.
Per-row presentation is now ONE Contact Support affordance with chain
state driving the rendering:
inside the review window, no dispute filed:
outline chip + draining doughnut + countdown
click → ReportProblemStep modal
dispute raised:
plain button with red dot
click → chat
dispute resolved:
plain button with green dot
click → chat (read-only thread)
otherwise (pre-window / post-window / non-disputable):
not rendered. Chat is gated on the dispute being on chain per the
protocol design.
User-facing copy avoids "dispute" everywhere: "Contact Support",
"review opens in", "review window closed", "Under review",
"Resolved", "Report submitted", "Could not submit report".
Internal renames:
- RaiseDisputeStep → ReportProblemStep (file + component + props)
- action.kind "raise-dispute" → "report-problem"
- support.onDisputeRaised → support.onReportSubmitted
- new export: ContactSupport (the row affordance wrapper)
Action variant gains a `filled` field (0.0..1.0) for the doughnut, set
on entry to the disputable window at 1.0 and draining to 0.0 at close.
The 1s tick inside OrderAction recomputes both `remainingMs` and
`filled` so the ring updates without an extra timer.
Bridge typing trade-off: the dispute-raised dot ramp lives in the
plain-button variant; the in-window chip relies solely on the ring
colour + countdown — no dot, the ring is the indicator.
Bridge tests still pass (52 vitest + 69 node:test). Two paths were
renamed:
test/RaiseDisputeStep.test.tsx → test/ReportProblemStep.test.tsx
Removed mock of <Support> from OrderAction tests, replaced with
mock of <ContactSupport>.
V1 production-ready. PR p2pdotme#4 superseded prior commit's design.
* Fix BUY dispute eligibility, modal styling, pre-flight revert decode
Three V1-blocking bugs uncovered during live testing.
(1) State machine wrong for BUY orders.
OrderProcessorFacet#raiseDispute rejects BUY/PAID — the contract gates
the dispute path on status=CANCELLED AND paidTimestamp != 0 (the
user paid but the order auto-cancelled before the merchant completed).
My state machine was showing the Contact Support chip on BUY/PAID
rows, which let users submit a tx the chain would always revert.
- BUY/paid → no action, status "Paid · awaiting merchant completion"
- BUY/cancelled + paidAt > 0 → review window [15m, 24h] after place,
status "Cancelled · contact support to recover funds"
- BUY/cancelled + paidAt == 0 → no action, status "Cancelled"
- SELL/PAY paths unchanged (status=COMPLETED + [30m, 7d])
(2) White text on white modal in dark-theme hosts.
The Modal portal renders outside the widget root, so theme CSS vars
applied on an ancestor don't reach inside. ReportProblemStep +
ContactSupport's chat-loading / chat-error views now set
background + colour + padding directly from `color.surface` /
`color.text` so the dark-theme merchant-app's white-on-white is fixed.
(3) Contract reverts surfaced as "Execution reverted for an unknown
reason."
ReportProblemStep now pre-simulates the on-chain raiseDispute via
viem.simulateContract using the diamond ABI (extended with the seven
custom errors thrown by raiseDispute: NotAuthorized, DisputeTimeNotReached,
DisputeTimeExpired, InvalidOrderType, InvalidOrderStatusToRaiseDispute,
CannotRaiseDisputeTwice, DisputeAlreadySettled). Reverts get decoded
into human-readable messages — no gas spent on a doomed tx. New props
`rpcUrl` + `chainId` flow PaymentHistoryWithSupport → OrderAction →
ContactSupport → ReportProblemStep.
Tests:
node:test 70 cases (BUY paid no-action; cancelled BUY paths with
paidAt>0 inside/before/after window; cancelled BUY
paidAt==0 no-action)
vitest 53 cases (new: pre-flight revert decodes
DisputeTimeNotReached without invoking the wallet)
Backwards-compat: kind-name "report-problem" unchanged; public exports
unchanged. SDK Order.paidAt fed verbatim from the subgraph layer is
the source of truth for the BUY/cancelled gating.
* copy: drop internal vocab ("merchant") from user-facing status strings
Per user feedback memory: in widget user-facing surfaces, drop
protocol-internal terms ("merchant", "dispute", "circle admin"). The
widget embeds in third-party apps where end users don't know or care
about the protocol's role names.
Replacements:
"Placed · awaiting merchant" → "Placed · matching"
"Accepted · awaiting merchant payment" → "Accepted · processing payment"
"Paid · awaiting merchant completion" → "Paid · processing payment"
"Merchant paid · awaiting your confirmation"
→ "Payment received · confirm to complete"
Confirm-step copy in ReportProblemStep also de-merchantified:
"the merchant has not completed the order" → "your paid order has not been completed"
Internal symbol names in code (`acceptedMerchant`, `disputeStatus`,
`raiseDispute`) unchanged — the rule applies only to text the end
user reads.
Tests updated; 70 node:test + 53 vitest green.
* Fix V1 review blockers — diamond resolver, hook details, optimistic rollback
External code-review pass surfaced five concerns. All addressed.
1. **Mainnet footgun: testnet-only default diamond.**
Old: `DEFAULT_DIAMOND_ADDRESS` hardcoded Base Sepolia. A Base
mainnet host that forgets the `diamondAddress` prop silently calls
a non-existent contract.
New: `resolveDiamondAddress(chainId, override)` looks up by chainId
and throws with a clear message if the chain isn't registered.
Legacy export kept as `@deprecated` for back-compat. Used by
`ReportProblemStep`.
2. **`Promise.all` aborts both reads on a transient `getBlock`
failure.** Multicall result was being thrown away when only the
clock-skew refresh failed. Split: multicall is awaited
unconditionally; `getBlock` is wrapped in its own try/catch and
leaves `clockSkewMs` at the prior value on error.
3. **`useOrderStates` hardcoded `paidAt: 0n`.** Structurally killed
the BUY/CANCELLED dispute eligibility predicate (which gates on
`paidAt > 0n`). Now multicalls `getAdditionalOrderDetails` in
parallel with `getOrdersById` and populates `paidAt`, `acceptedAt`,
`fixedFeePaid`, `tipsPaid`, `actualUsdtAmount`, `actualFiatAmount`
from it. Doubles the contracts per row (2 per orderId) but stays
one multicall round-trip per refresh.
4. **Optimistic dispute flip never rolled back on revert.** Once
`setOptimisticDispute(true)` fired we never cleared it. If the tx
reverted at confirmation the chip permanently showed `View report`
pointing at chat that didn't exist. New: spawn a
`waitForTransactionReceipt` against the read RPC; if the receipt
reports `status !== "success"`, clear the local flag and let the
chain poll surface the real state. Timeout 60s; on timeout / not
found also clear (the chain poll will reconcile either way).
5. **`extractRevert` didn't walk viem's `BaseError` chain.** Wallet-
side reverts surface as `TransactionExecutionError` whose decoded
`errorName` lives one level deeper than the old probe walked. Use
viem's `BaseError.walk((e) => e instanceof
ContractFunctionRevertedError)` first; fall back to the manual
`.cause` probe for non-viem shapes (tests, raw wallet errors).
Tests added:
- useOrderStates: getBlock failure no longer erases multicall result.
- useOrderStates: `paidAt` populated from getAdditionalOrderDetails.
- useOrderStates partial-failure mock updated for the new 2-call-per-row shape.
70 node:test + 55 vitest still green. Typecheck + build clean.
Pending nits from the review (logged for follow-up, not V1 blockers):
- N intervals for N rows in `useNowTick` (cosmetic perf).
- Modal aria-modal duplicated landmarks + no focus trap.
- `useActiveSupportTickets` deps array uses signer object identity.
- dead `originApp` prop in ContactSupport.
---------
Co-authored-by: gitchadd <gitchad@icloud.com>
vvictor-dev
pushed a commit
to vvictor-dev/widgets
that referenced
this pull request
Aug 22, 2026
Ships the smart per-row action layout for <PaymentHistoryWithSupport> (PR p2pdotme#4): pure (Order, now) → state machine, OrderAction + RaiseDisputeStep + useOrderStates components, and new chatState / actionMode / txSigner props. All additions are non-breaking; the prior single-launcher layout is preserved verbatim under actionMode="chat". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Smart per-row layout for `` — status, conditional action, support button with chat-active vs chat-new differentiation. Default behavior changes; existing visual layout is preserved via opt-in.
What's in
Three layers per row:
`` gains a new `actionMode: "chat" | "smart"` prop. Default flips to `"smart"` — existing embedders get the new layout next time they update. Old behavior is preserved via `actionMode="chat"`.
Decisions locked
New exports on `@p2pdotme/widgets/support`
Surface changes
Test plan
What's NOT in this PR