Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ab053e8
placeholder screen for livesale
brainbicycle Jan 21, 2026
924ede3
add feature flag
brainbicycle Jan 21, 2026
0e2d324
add container to allow ff enable
brainbicycle Jan 21, 2026
57277a7
add close icon
brainbicycle Jan 21, 2026
d6f35e0
add relay query to fetch live auction data
brainbicycle Jan 21, 2026
8d5c080
implement live auction websocket provider
brainbicycle Jan 21, 2026
35ea579
fix: use causalityURL for websocket connection
brainbicycle Jan 21, 2026
9895c12
add tests for initial state parsing
brainbicycle Jan 21, 2026
065bb57
fix: correct initial state payload structure for live auction websocket
brainbicycle Jan 21, 2026
61b7a45
add comprehensive tests for all live auction reducer actions
brainbicycle Jan 21, 2026
5f019fd
remove debug log
brainbicycle Jan 21, 2026
0f978b8
remove unused imports
brainbicycle Jan 21, 2026
17d1200
feat: implement live auction lot carousel with animations
brainbicycle Jan 22, 2026
d426e69
feat: add artwork images and metadata to live lot carousel
brainbicycle Jan 22, 2026
f0e0a47
fix: key artwork metadata by internalID (UUID) to match WebSocket lot…
brainbicycle Jan 22, 2026
e830901
refactor: move live sale debug info to modal accessible via dev toggle
brainbicycle Jan 22, 2026
0a32f59
tidy up debug view, respect dark mode
brainbicycle Jan 30, 2026
53e5113
fix: preserve lot order from GraphQL when processing WebSocket data
brainbicycle Jan 30, 2026
aeacb59
fix some dark mode issues on lot view
brainbicycle Jan 30, 2026
4517eb0
fix: calculate image dimensions for proper scaling in lot carousel
brainbicycle Jan 31, 2026
86c3282
matching swift side
brainbicycle Feb 2, 2026
42b6fa2
Merge remote-tracking branch 'origin/main' into brian/live-auction-ex…
brainbicycle Apr 20, 2026
51f921d
remove unused var for now
brainbicycle Apr 20, 2026
9d2a31b
remove extra ui elements for now
brainbicycle Apr 21, 2026
b650f9a
feat: add LiveAuctionBidButton with full state machine and fix sold-l…
brainbicycle Apr 21, 2026
cf4fb66
feat: add LiveAuctionEventFeed with undo event fix
brainbicycle Apr 23, 2026
ffb577f
fix: correct event feed row colors to match Swift implementation
brainbicycle Apr 23, 2026
06f5b66
fix: floor bids are never pending
brainbicycle Apr 23, 2026
06e7bda
fix: only show event feed for the current live lot
brainbicycle Apr 23, 2026
b829ac0
fix: correct LotUpdateBroadcast wire format parsing
brainbicycle Apr 23, 2026
32a0eb7
revisit: add dev-only debug logging to live auction WebSocket
brainbicycle Apr 23, 2026
40563df
fix: correct bid button currency symbol and asking price
brainbicycle Apr 23, 2026
fc47452
revisit: use network-only fetch policy for SalesAuctionsOverview
brainbicycle Apr 29, 2026
a80c7d7
revisit: fix live bid submission and wire up bid action routing
brainbicycle Apr 29, 2026
e3ac2cf
feat: add max bid modal for upcoming lot pre-bidding
brainbicycle Apr 29, 2026
54b1e10
fix: add paddleNumber to bid event payload and handle InvalidMessageR…
brainbicycle Apr 29, 2026
249143f
fix: align bid event payloads exactly with native wire format
brainbicycle Apr 29, 2026
7bdf981
add networking doc
brainbicycle May 1, 2026
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
158 changes: 158 additions & 0 deletions docs/liveAuctions/bid_button_implementation_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Live Auction Bid Button — Implementation Plan

Porting `LiveAuctionBidButton.swift` to React Native. See `bid_button_states.md` for the full state reference.

## What already exists

- Full WebSocket state management in `src/app/Scenes/LiveSale/hooks/useLiveAuctionWebSocket.ts`
- Comprehensive TypeScript types in `src/app/Scenes/LiveSale/types/liveAuction.ts`
- `LiveAuctionContext` providing all lot and sale state to the tree
- The current bid button in `LiveLotCarouselCard` is a placeholder — only handles "Place Bid", "Sold", "Passed"

## Steps

### 1. Add button state types to `types/liveAuction.ts`

Two new discriminated unions mirroring the Swift enums:

```ts
type LiveAuctionBiddingProgressState =
| { kind: "userRegistrationRequired" }
| { kind: "userRegistrationPending" }
| { kind: "userRegistrationClosed" }
| { kind: "biddable"; askingPriceCents: number; currencySymbol: string }
| { kind: "biddingInProgress" }
| { kind: "bidNotYetAccepted"; askingPriceCents: number; currencySymbol: string }
| { kind: "bidBecameMaxBidder" }
| { kind: "bidAcknowledged" }
| { kind: "bidOutbid" }
| { kind: "bidNetworkFail" }
| { kind: "bidFailed"; reason: string }
| { kind: "lotWaitingToOpen" }
| { kind: "lotSold" }

type LiveAuctionBidButtonState =
| { kind: "active"; biddingState: LiveAuctionBiddingProgressState }
| {
kind: "inactive"
lotPhase: "upcoming" | "closedSold" | "closedPassed"
isHighestBidder?: boolean
}
```

Also add a `RegistrationStatus` type and field to `LiveAuctionState`:

```ts
type RegistrationStatus = "registered" | "pending" | "closed" | "unregistered"
```

### 2. Update `LiveSaleProviderQuery` to fetch registration data

Add `qualifiedForBidding` to the existing `me.bidders` fetch, and add `registrationEndsAt` to the sale:

```graphql
sale {
registrationEndsAt
}
me {
bidders(saleID: $saleID) {
internalID
qualifiedForBidding # add this
}
}
```

Derive `RegistrationStatus` from this data before passing to the WebSocket hook:

- `bidders` non-empty && `qualifiedForBidding == true` → `"registered"`
- `bidders` non-empty && `qualifiedForBidding == false` → `"pending"`
- `bidders` empty && `now > registrationEndsAt` → `"closed"`
- `bidders` empty && `now <= registrationEndsAt` → `"unregistered"`

Store as `registrationStatus: RegistrationStatus` on `LiveAuctionState`. Set once from GraphQL; never updated by WebSocket.

> **Note — possible future improvement:** The GraphQL `Sale` type has a `registrationStatus: Bidder` field that is purpose-built for this use case (`sale.registrationStatus.qualifiedForBidding`). We deliberately matched the native iOS implementation here (which uses `me.bidders(saleID).qualifiedForBidding`) rather than switching to this field, because it's unclear whether native had a reason to avoid it or whether the field simply didn't exist at the time. Worth revisiting — using `sale.registrationStatus` would simplify the query and remove the need for `registrationEndsAt` date comparison.

### 3. New hook: `hooks/useLiveAuctionBidButtonState.ts`

Pure function `(lotId: string, state: LiveAuctionState) => LiveAuctionBidButtonState`.

Mirrors `LiveAuctionBiddingViewModel.stateToBidButtonState` in Swift. Priority order (same as native):

1. Closed lot → always `inactive`
2. Registration closed (if not registered) → `active: userRegistrationClosed`
3. Registration pending → `active: userRegistrationPending`
4. Registration required → `active: userRegistrationRequired`
5. Upcoming lot that is the current lot (waiting for auctioneer) → `active: lotWaitingToOpen`
6. Upcoming lot that is not the current lot → `inactive: upcoming`
7. Live lot — derive from `DerivedLotState` and `pendingBids`:
- Pending bid with status `"pending"` → `biddingInProgress`
- Pending bid with status `"error"` → `bidFailed` or `bidNetworkFail`
- `sellingToBidderId === myBidderId && floorWinningBidderId === myBidderId` → `bidBecameMaxBidder`
- `sellingToBidderId === myBidderId` → `bidNotYetAccepted`
- Otherwise → `biddable`

### 4. New component: `components/LiveAuctionBidButton/LiveAuctionBidButton.tsx`

```ts
interface LiveAuctionBidButtonProps {
buttonState: LiveAuctionBidButtonState
onPress: (action: "bid" | "registerToBid" | "submitMaxBid") => void
flashOutbidOnBiddableStateChanges?: boolean // default true — set false on max-bid modal
hideOnError?: boolean // default false — set true on max-bid modal
}
```

State → UI mapping:

| State | Label | Variant | Disabled |
| ----------------------------------------- | --------------------------- | -------------- | --------------- |
| `userRegistrationRequired` | "Register to Bid" | fill/black | No |
| `userRegistrationPending` | "Registration Pending" | fill/black | Yes |
| `userRegistrationClosed` | "Registration Closed" | fill/black | Yes |
| `lotWaitingToOpen` | "Waiting for Auctioneer…" | outline/grey | Yes |
| `biddable` | "Bid $X,XXX" | fill/black | No |
| `biddingInProgress` | _(spinner)_ | fill/purple | Yes |
| `bidNotYetAccepted` | "Bid $X,XXX" | fill/grey | Yes |
| `bidBecameMaxBidder` / `bidAcknowledged` | "You're the Highest Bidder" | outline/green | Yes |
| `bidOutbid` | "Outbid" | fill/red | Yes |
| `bidNetworkFail` | "Network Failed" | outline/red | Yes |
| `bidFailed` | "An Error Occurred" | fill/red | Yes (or hidden) |
| `lotSold` | "Sold" | outline/purple | Yes |
| `inactive: upcoming` (not highest bidder) | "Bid" | fill/black | No |
| `inactive: upcoming` (highest bidder) | "Increase Max Bid" | fill/black | No |
| `inactive: closedSold` | "Sold" | outline/purple | Yes |
| `inactive: closedPassed` | "Lot Closed" | outline/grey | Yes |

Special behaviors (matching native):

- **Outbid animation:** `bidBecameMaxBidder` → `biddable` triggers a brief "Outbid" flash before settling. Any state updates received during animation are queued and applied after. Controlled by `flashOutbidOnBiddableStateChanges`.
- **`bidFailed` auto-revert:** reverts to previous state after 2s unless `hideOnError=true`, in which case the button is hidden.
- **`bidAcknowledged` and `bidBecameMaxBidder`** render identically.

### 5. Tests: `hooks/__tests__/useLiveAuctionBidButtonState.tests.ts`

- All `inactive` states from lot data
- All `active` states
- Priority ordering (closed > registration states > live lot states)
- Pending bid drives `biddingInProgress` / error states
- `sellingToBidderId` + `floorWinningBidderId` combination → `bidBecameMaxBidder` vs `bidNotYetAccepted`

### 6. Tests: `components/LiveAuctionBidButton/__tests__/LiveAuctionBidButton.tests.tsx`

- Render test for every state (matches Swift snapshot test coverage)
- Tap handlers: correct `action` fires for `userRegistrationRequired`, `biddable`, `upcomingLot`
- Outbid animation: `bidBecameMaxBidder` → `biddable` queues biddable state until animation completes
- State received mid-animation is applied after, not dropped
- `hideOnError=true` hides button on `bidFailed`
- `bidFailed` auto-reverts after 2s

## File checklist

- [ ] `src/app/Scenes/LiveSale/types/liveAuction.ts` — add `LiveAuctionBidButtonState`, `LiveAuctionBiddingProgressState`, `RegistrationStatus`; extend `LiveAuctionState`
- [ ] `src/app/Scenes/LiveSale/LiveSaleProvider.tsx` — add `qualifiedForBidding` + `registrationEndsAt` to query; derive and pass `registrationStatus`
- [ ] `src/app/Scenes/LiveSale/hooks/useLiveAuctionBidButtonState.ts` — new hook
- [ ] `src/app/Scenes/LiveSale/hooks/__tests__/useLiveAuctionBidButtonState.tests.ts` — new tests
- [ ] `src/app/Scenes/LiveSale/components/LiveAuctionBidButton/LiveAuctionBidButton.tsx` — new component
- [ ] `src/app/Scenes/LiveSale/components/LiveAuctionBidButton/__tests__/LiveAuctionBidButton.tests.tsx` — new tests
- [ ] `src/app/Scenes/LiveSale/components/LiveLotCarouselCard.tsx` — replace placeholder button with `LiveAuctionBidButton`
59 changes: 59 additions & 0 deletions docs/liveAuctions/bid_button_states.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Live Auction Bid Button States

The bid button responds to websocket events and renders differently depending on the state of both the lot and the sale. State is a two-level structure: an outer `LiveAuctionBidButtonState` with an inner state for each branch.

## `.active` — lot is currently live

These states come from `LiveAuctionBiddingProgressState` (`LiveAuctionBidViewModel.swift`):

| State | Label | Tappable | Notes |
| ------------------------------------------------ | --------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------- |
| `userRegistrationRequired` | "Register To Bid" | Yes | Triggers registration flow |
| `userRegistrationPending` | "Registration Pending" | No | — |
| `userRegistrationClosed` | "Registration Closed" | No | — |
| `lotWaitingToOpen` | "Waiting for Auctioneer…" | No | Lot is live but auctioneer hasn't opened bidding yet |
| `biddable(askingPrice, currencySymbol)` | "Bid $X,XXX" | Yes | Normal biddable state; triggers outbid animation if transitioning from max bidder |
| `biddingInProgress` | _(spinner)_ | No | After tapping bid, waiting for server confirmation |
| `bidNotYetAccepted(askingPrice, currencySymbol)` | "Bid $X,XXX" (grey) | No | User is being sold to on the floor but not yet confirmed as max bidder |
| `bidAcknowledged` | "You're currently the highest bidder" (white/green) | No | Treated identically to `bidBecameMaxBidder` in the UI |
| `bidBecameMaxBidder` | "You're currently the highest bidder" (white/green) | No | — |
| `bidOutbid` | "Outbid" (red) | No | Transient animation state — auto-transitions back to `biddable` |
| `bidNetworkFail` | "Network Failed" | No | — |
| `bidFailed(reason)` | "An Error Occurred" (red) | No | Auto-transitions back to previous state after 2s; hides button instead if `hideOnError=true` |
| `lotSold` | "Sold" (purple) | No | — |

## `.inActive` — lot is not currently live

These states come from `LotState` (`LiveAuctionLotViewModel.swift`):

| State | Label | Tappable | Notes |
| ------------------------------------- | ------------------- | -------- | ----------------------------------------------- |
| `upcomingLot(isHighestBidder: false)` | "Bid" | Yes | Triggers max-bid flow |
| `upcomingLot(isHighestBidder: true)` | "Increase Max Bid" | Yes | User already has a max bid on this upcoming lot |
| `closedLot(wasPassed: false)` | "Sold" (purple) | No | — |
| `closedLot(wasPassed: true)` | "Lot Closed" (grey) | No | Lot passed without selling |

## Special behaviors

**Outbid animation** — transitioning from `bidBecameMaxBidder` or `bidNotYetAccepted` → `biddable` triggers a brief "Outbid" animation. Any state updates received during the animation are queued and applied once it completes. Controlled by `flashOutbidOnBiddableStateChanges` (disabled on the max-bid modal).

**`hideOnError`** — the `bidFailed` state either shows the error button or hides the button entirely. The lot view controller shows it; the max-bid overlay hides it.

**`bidAcknowledged` vs `bidBecameMaxBidder`** — these are distinct enum cases but render identically. Both transition back to `biddable` (with an outbid animation) when the user is outbid.

**State priority** (from `LiveAuctionBiddingViewModel.stateToBidButtonState`) — when computing button state from lot + auction state:

1. Closed lot always shows as closed
2. Registration closed (if not registered)
3. Registration pending
4. Registration required (if not registered)
5. Upcoming lot (live lot waiting to open, or pre-sale)
6. Live lot (biddable / selling-to-me states)

## Source files

- `ios/Artsy/View_Controllers/Live_Auctions/LiveAuctionBidButton.swift` — button UI and animation logic
- `ios/Artsy/View_Controllers/Live_Auctions/LiveAuctionBidViewModel.swift` — `LiveAuctionBiddingProgressState` enum
- `ios/Artsy/View_Controllers/Live_Auctions/Views/LiveAuctionBiddingViewModel.swift` — maps lot/auction signals → button state
- `ios/Artsy/View_Controllers/Live_Auctions/ViewModels/LiveAuctionLotViewModel.swift` — `LotState` enum
- `ios/ArtsyTests/View_Controller_Tests/Live_Auction/LiveAuctionBidButtonTests.swift` — snapshot tests covering all states
Loading
Loading