feat(concept): live auctions in react native exploration - #13499
Draft
brainbicycle wants to merge 38 commits into
Draft
feat(concept): live auctions in react native exploration#13499brainbicycle wants to merge 38 commits into
brainbicycle wants to merge 38 commits into
Conversation
Implements withSuspense pattern to fetch static auction data including sale info, causality JWT token, and user bidding credentials (paddle number, bidder ID). Includes loading and error states. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Add WebSocket connection management for live auctions with real-time bidding support. Includes connection state management, heartbeat (send "2" every 1s), reconnection logic (500ms retry), and state reducer for handling auction events. Key features: - WebSocket connection with auto-reconnection - Duplicate event prevention with processed event tracking - Derived lot state calculations (reserve status, asking price, bid counts) - Pending bid tracking with UUID-based response matching - Disconnect warning (debounced to 1.1s) - Pass role: PARTICIPANT to causalityJWT to fetch proper credentials Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Use the correct causalityURL environment variable instead of parsing predictionURL. The predictionURL points to live.artsy.net (webview host) while causalityURL points to causality.artsy.net (websocket host). Also fixes TypeScript timeout type errors and removes unused imports. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Add comprehensive tests for the INITIAL_STATE_RECEIVED reducer action to verify: - Single and multiple lot parsing - Event deduplication - Event sorting by timestamp - Derived state calculations - Operator connected status - Sale on hold state All tests passing. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Updates the InitialFullSaleStateMessage to match the actual payload structure from the websocket server: - Use fullLotStateById (object) instead of lots (array) - Rename onHoldMessage to saleOnHoldMessage - Add FullLotStateData and DerivedLotStateData types Also exports liveAuctionReducer from the hook for direct testing instead of duplicating reducer logic in tests. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Adds test coverage for all reducer action types: - Connection state (CONNECTION_OPENED, CONNECTION_CLOSED, warnings) - Lot updates (LOT_UPDATE_RECEIVED with new/existing lots, deduplication) - Current lot changes (CURRENT_LOT_CHANGED) - Bidding flow (BID_PLACED, BID_RESPONSE_RECEIVED with success/error) - Sale state (SALE_ON_HOLD_CHANGED, OPERATOR_STATUS_CHANGED) Test coverage increased from 9 to 26 tests, covering all state transitions and edge cases based on the Swift implementation patterns. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Add a horizontal pageable carousel for browsing auction lots with: - PagerView-based carousel with adjacent card peeking - Spring-based scale/opacity animations (focused: 1.0, unfocused: 0.85) - Lot cards displaying asking price, bidding status, reserve status, online bid count - Smart bid button that changes text based on lot state - Comprehensive unit tests for all components The carousel replaces the simple lot info display in LiveSale.tsx and provides a more engaging way to browse lots. Future work includes adding artwork images, artist names, and estimate ranges when GraphQL data is available. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Add GraphQL query to fetch artwork data for auction lots and display: - Artwork images with aspect ratio handling - Artist names - Lot titles - Estimate ranges - Fallback to placeholder when no image available The GraphQL query matches the iOS implementation (saleArtworksConnection with all: true) and creates a Map of artwork metadata keyed by lotLabel. The carousel card component now displays rich artwork information when focused. All tests updated and passing. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
… IDs
The bug was a mismatch between how we keyed the artwork metadata map and how
the WebSocket identifies lots:
**Problem:**
- GraphQL provides both `internalID` (UUID) and `lotLabel` ("1", "2", "3")
- WebSocket messages use `internalID` (UUID) as the `lotId`
- We were keying the metadata map by `lotLabel`, causing lookups to fail
**Solution:**
- Key the metadata map by `internalID` (UUID) to match WebSocket lot IDs
- Store `lotLabel` as a property within the metadata for display purposes
- Matches the iOS Swift implementation pattern (LiveAuctionLot has both IDs)
**Testing:**
- Added comprehensive test to prevent this regression
- Test documents the expected behavior and validates the mapping logic
- Added debug logging to help diagnose similar issues in the future
Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Cleans up LiveSale.tsx to show only sale name and carousel. All debug information (connection status, credentials, stats, pending bids) moved to new LiveSaleDebugView modal accessed via header button. Adds DTShowLiveSaleDebugButton dev toggle to enable debug modal in production builds (TestFlight/live apps) for troubleshooting live auction issues. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
The lot order was incorrect because we were iterating through Object.entries(fullLotStateById), which doesn't preserve order. Now iterate through artworkMetadata.keys() (which preserves GraphQL order) and look up lots in fullLotStateById. This matches the Swift implementation which iterates through saleArtworks array. Also removed incorrect sorting by UUID extraction in LiveLotCarousel since lotId is a UUID, not a lot number. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Fixed image scaling issue where images were zoomed in incorrectly. Now calculates explicit width based on screen dimensions and aspect ratio, then uses resizeMode="contain" to match Swift's scaleAspectFit behavior. Container has fixed height for predictable carousel sizing. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
brainbicycle
marked this pull request as draft
April 20, 2026 17:57
…ot detection Implements the React Native equivalent of the Swift LiveAuctionBidButton: - Discriminated union type covering all 13 bidding progress states - deriveBidButtonState hook derives button state from LiveAuctionState - Outbid flash animation (bidBecameMaxBidder → biddable shows brief "Outbid") with state queuing during animation (two tests skipped pending RNTL fix) - Registration status derived from me.bidders qualifiedForBidding (matching native) - LiveLotCarouselCard now delegates to LiveAuctionBidButton via useLiveAuction context Fixes a discrepancy vs Swift where sold lots showed "Register to Bid" instead of "Sold": the reducer was ignoring the server's derivedLotState.biddingStatus and recalculating purely from eventHistory, which may be incomplete for already-sold lots. Now trusts the server when it reports biddingStatus: "Complete". Also stops skipping lots absent from fullLotStateById so they appear in state.lots. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Add LiveAuctionEventFeed component and useLiveAuctionEventFeed hook, wired into the lot carousel card - Add deriveLotEventFeed utility that builds display-ready feed events from raw lot event history, with full test coverage - Fix LiveOperatorEventUndone parsing: wire sends the cancelled event ID as a nested object `event.eventId`, not a top-level `hostedEventId` (mirrors Obj-C Mantle mapping `hostedEventID -> "event.eventId"`) - Fix sellingToBidder/floorWinningBidder to read from nested wire objects, falling back to server values when event history is incomplete Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Move cancelled/pending gray out of the top-level check into the bid case only — Swift only grays bid rows when cancelled, non-bid events (warning, finalCall, lotOpen, closed) keep their fixed colors - My winning bid (isMine && isTopBid) now renders gray (mono60) to match Swift's artsyGrayMedium, not black - Replace invalid purple100 token with blue100 for lotOpen rows Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
OfflineBidder bids are placed by the operator and don't go through the online confirmation flow (CompositeOnlineBidConfirmed), so they were always resolving isPending=true and rendering gray instead of black. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Mirrors native iOS behaviour in LiveAuctionLotViewController where bidHistoryViewController.view.isHidden is true whenever the lot's ID does not match the auction's currentLotId. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The wire sends events as a dict keyed by eventId (not an array under
lotEvents), with lotId embedded inside each event value — matching the
native processLotEventBroadcast which reads json["events"] as a dict.
Normalize to { lotId, lotEvents[] } at the dispatch site so the reducer
is unchanged. Fixes crash when any live lot event is received.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Logs connection open/close, every inbound message with a concise summary of key fields, and outbound bids — all prefixed [LiveAuction] for easy filtering in Metro. Gated on __DEV__ so no prod impact. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Currency: sale.currency returns the ISO code ("USD"), not a symbol.
Replace Intl.NumberFormat (unreliable in Hermes) with a static lookup
map of currencies Artsy auctions use.
Asking price: calculateDerivedState only sets askingPriceCents when an
AskingPriceChanged event is present in history. Fall back to
serverDerived.askingPriceCents in both INITIAL_STATE_RECEIVED and
LOT_UPDATE_RECEIVED, matching the pattern already used for
sellingToBidderId.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
store-and-network is intercepted by the 15-min cacheMiddleware in RelayNetworkLayer, meaning auction state changes (upcoming → live) would not appear until the TTL expires. network-only sets force:true which bypasses the middleware cache and hits the backend directly. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Two bugs prevented bidding from working:
1. lotId was missing from FirstPriceBidPlaced/SecondPriceBidPlaced event payloads —
the server had no way to know which lot was being bid on.
2. LiveLotCarouselCard ignored the action param from LiveAuctionBidButton,
always calling placeBid regardless of button state.
Action routing is now correct: "bid" → placeBid, "registerToBid" → navigate
to /auction-registration/{slug}, "submitMaxBid" → stub pending max bid modal (step 3).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Fetch sale.bidIncrements from GraphQL and thread through context - computeBidAmounts() mirrors the native minimumNextBidCentsIncrement algorithm: find the highest-threshold rule <= current price, step forward - LiveAuctionMaxBidModal: bottom sheet with scrollable amount picker, loading/success/error states driven by pendingBids, auto-dismisses on success - Wire submitMaxBid action in LiveLotCarousel to open the modal Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…eply Server requires paddleNumber in the ArtsyBidder object — without it, Causality returns InvalidMessageReply and the bid silently hangs in pending state forever. Also typed and handled InvalidMessageReply so future payload errors surface as a bidFailed state rather than leaving the button stuck in biddingInProgress. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Read LiveAuctionSocketCommunicator.swift directly to spec out the full payload. Three gaps vs what we were sending: - SecondPriceBidPlaced uses amountCents (not maxAmountCents) — same key as FirstPrice - Both events require userId in the bidder object (already fetched via me.internalID) - Both events include clientMetadata User-Agent Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
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.
This PR resolves []
Description
Proof of concept and spike on a 1:1 port to support live auctions in React Native.
Here is a basic lots view with lot history responding to websocket events and emitting bid events:
https://github.com/user-attachments/assets/5e1dd5fc-71c5-453a-ba5b-f19ce4c13803
PR Checklist
To the reviewers 👀
Changelog updates
Changelog updates
Cross-platform user-facing changes
iOS user-facing changes
Android user-facing changes
Dev changes
Need help with something? Have a look at our docs, or get in touch with us.