CP-14878: add limit price to perps place order + open orders/activity tabs - #4007
CP-14878: add limit price to perps place order + open orders/activity tabs#4007bogdandobritoiu wants to merge 22 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve history screen - Extract PositionsList from PerpetualsPositionsScreen; filters live in the list header - Add pull-to-refresh to positions, open orders and activity via usePerpsPullToRefresh - Add Activity segment showing all fills in one feed (parity with core-web) - Remove the PositionsHistory screen/route, superseded by the Activity tab - Place-order card styling polish; Positions carousel always renders Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPlaceOrderScreen.test.tsx # packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPlaceOrderScreen.tsx
There was a problem hiding this comment.
Pull request overview
Adds limit-order support to the Perps place-order flow (including sizing/liquidation semantics keyed off the effective entry price) and refactors the “My positions” experience into a tabbed screen (Positions / Open orders / Activity) with shared pull-to-refresh, removing the superseded History modal route.
Changes:
- Introduces limit-price state in
PlaceOrderContext, a dedicated limit-price editor screen/route, and passesorderKind: 'limit'+limitPxthrough order submission. - Reworks the Positions screen into CollapsibleTabs with a bottom SegmentedControl, extracting
PositionsListand adding newOpenOrdersList+PerpsActivityList. - Adds shared perps pull-to-refresh hook and expands unit tests around the new behavior.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core-mobile/app/new/routes/(signedIn)/(modals)/perpetualsPositionsHistory/index.tsx | Removes the positions history modal route entrypoint. |
| packages/core-mobile/app/new/routes/(signedIn)/(modals)/perpetualsPositionsHistory/_layout.tsx | Removes the positions history modal stack layout. |
| packages/core-mobile/app/new/routes/(signedIn)/(modals)/perpetualsPlaceOrder/limitPrice.tsx | Adds routing entrypoint for the new limit-price editor screen. |
| packages/core-mobile/app/new/routes/(signedIn)/(modals)/perpetualsPlaceOrder/_layout.tsx | Registers the new limitPrice modal screen in the place-order stack. |
| packages/core-mobile/app/new/routes/(signedIn)/_layout.tsx | Removes the modal registration for positions history. |
| packages/core-mobile/app/new/features/trade/perpetuals/utils/economics.ts | Extracts pctParts formatting helper for reused “% above/below …” copy. |
| packages/core-mobile/app/new/features/trade/perpetuals/utils/economics.test.ts | Adds unit tests covering pctParts. |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsTriggerScreen.tsx | Updates trigger reference/sizing logic to use limit entry when enabled. |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsTriggerScreen.test.tsx | Adds tests ensuring projected P&L uses seeded entry vs limit entry appropriately. |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsScreen.tsx | Adjusts layout around the positions carousel (padding wrapper). |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPositionsScreen.tsx | Refactors “My positions” into tabbed UI and adds bottom SegmentedControl overlay. |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPositionsHistoryScreen.tsx | Removes the old History screen implementation. |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPlaceOrderScreen.tsx | Adds limit toggle/drill row, limit sizing, and limit submission parameters. |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPlaceOrderScreen.test.tsx | Adds/extends tests for limit toggle behavior, sizing, and submit params. |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsLimitPriceScreen.tsx | Adds the dedicated limit-price editor screen for the place-order flow. |
| packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsLimitPriceScreen.test.tsx | Adds tests for limit price validation, seeding, and commit-on-done behavior. |
| packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsUserFills.ts | Updates refetch typing to return a promise so callers can await refresh completion. |
| packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsPullToRefresh.ts | Introduces shared pull-to-refresh hook coordinating nonce bump + query invalidation + optional extra refresh. |
| packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsPullToRefresh.test.tsx | Adds tests for refresh settling and extra refresh rejection handling. |
| packages/core-mobile/app/new/features/trade/perpetuals/contexts/PlaceOrderContext.tsx | Adds limit price state and keys liquidation estimate off effective entry price. |
| packages/core-mobile/app/new/features/trade/perpetuals/components/PositionsList.tsx | Extracts the positions list + filter chips + pull-to-refresh + lazy load into a standalone tab component. |
| packages/core-mobile/app/new/features/trade/perpetuals/components/Positions.tsx | Simplifies carousel rendering (removes initial-load hiding behavior). |
| packages/core-mobile/app/new/features/trade/perpetuals/components/PerpsActivityList.tsx | Adds Activity tab list rendering all fills with pull-to-refresh. |
| packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrdersList.tsx | Adds Open orders tab list rendering resting orders with per-row cancel and pull-to-refresh. |
| packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrdersList.test.tsx | Adds tests for open-order row mapping and cancel action wiring. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Coverage report ✅2/2 packages passed thresholds 🟢 @avalabs/core-mobile
🟢 @avalabs/k2-alpine
Artifacts and threshold sources
Source run: Mobile PR |
Extract OpenOrderListItem; per-row cancelling state keyed by order id in the parent (FlashList recycling-safe) replaces the shared busy flag that dimmed every Cancel button while one cancel was in flight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the in-flight state into OpenOrderListItem (button swaps to a spinner for that row only); recycling-safe via render-phase reset on item.id change and a shown-id guard on the async settle. Drops the parent's cancelling-ids set and extraData. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Web-parity chips under the input: ±1/5/10% and Mid, side-aware (long rests below the market, short above), anchored to the live mid with mark/entry fallbacks, snapped to Hyperliquid's price grid via roundToHyperliquidPrice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsPullToRefresh.ts:27
onRefreshcan be triggered again while a previous refresh is still in flight; because each call schedules its ownsetIsRefreshing(false)when it settles, the spinner can turn off early. Guard against re-entrancy (or otherwise ensure only the latest refresh completion clearsisRefreshing).
const onRefresh = useCallback(() => {
setIsRefreshing(true)
refreshAfterTrade()
const waits: Promise<unknown>[] = [
queryClient.invalidateQueries({
…ditor Redundant now that the preset chips express the relation to the market. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrderListItem.tsx:45
setRenderedId/setCancellingare called during render whenitem.idchanges. Updating state during render can trigger React warnings and can lead to render loops under certain conditions. Consider resetting the transient cancel state in a layout effect keyed onitem.idinstead, while keeping theshownIdRefguard to avoid stale promises toggling state.
const [renderedId, setRenderedId] = useState(item.id)
if (renderedId !== item.id) {
setRenderedId(item.id)
setCancelling(false)
}
FiatAmountInput only seeds from the amount prop; the chips were updating state (Done committed the right price) while the on-screen number stayed stale. Presets now also push the value through the widget's imperative setValue handle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrderListItem.tsx:45
- This component calls setState during render when
item.idchanges (setRenderedId/setCancellinginside theif (renderedId !== item.id)block). React warns against this pattern and it can cause unexpected re-render loops. A safer approach is to key the cancelling state by the row id (derived during render) and only update state from event handlers.
const [cancelling, setCancelling] = useState(false)
// FlashList recycles cells: this instance can be handed a different order
// mid-flight. Reset the transient state during render (so the wrong row
// never paints a spinner) and track the shown id so a stale promise can't
// flip state for an order this cell no longer displays.
const [renderedId, setRenderedId] = useState(item.id)
if (renderedId !== item.id) {
setRenderedId(item.id)
setCancelling(false)
}
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop the double-counted safe-area inset from the positions tabs padding (the measured segmented-control height already contains it) - Pull-to-refresh: guard re-entrant pulls, use refreshClearinghouse instead of refreshAfterTrade (nonce is in the clearinghouse query key, so the extra invalidations were redundant), fix the docstring - pctParts: zero is neither above nor below — 'At current price' Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrderListItem.tsx:45
OpenOrderListItemcallssetStateduring render whenitem.idchanges (setRenderedId/setCancelling). This is an anti-pattern that can trigger React warnings and can cause extra renders. Instead, track the cancelling state keyed by the currentitem.idso recycled cells never show a stale spinner without mutating state during render.
const [cancelling, setCancelling] = useState(false)
// FlashList recycles cells: this instance can be handed a different order
// mid-flight. Reset the transient state during render (so the wrong row
// never paints a spinner) and track the shown id so a stale promise can't
// flip state for an order this cell no longer displays.
const [renderedId, setRenderedId] = useState(item.id)
if (renderedId !== item.id) {
setRenderedId(item.id)
setCancelling(false)
}
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsTriggerScreen.tsx:108
currentPriceis no longer always the live/"current" mark: when a limit entry is enabled it becomes the intended entry price. This name is misleading and makes it easy for later UI copy (e.g., "above/below current price") to become incorrect. Consider renaming this to something likereferencePrice/triggerReferencePriceand updating any user-facing strings that mention "current price" when the reference is the limit entry.
const currentPrice =
limitPriceEnabled && limitPrice !== undefined
? limitPrice
: liveMarkPrice > 0
? liveMarkPrice
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrderListItem.tsx:45
- This component calls
setRenderedId/setCancellingduring render whenitem.idchanges. Updating state during render is not supported in React and can cause warnings or render loops; it’s better to reset transient state in an effect keyed onitem.id(FlashList cell recycling case) while keeping the ref used to guard stale promises.
const [renderedId, setRenderedId] = useState(item.id)
if (renderedId !== item.id) {
setRenderedId(item.id)
setCancelling(false)
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsPullToRefresh.ts:27
- The re-entrancy guard currently relies on
isRefreshingstate, which can be stale within the same render (e.g. ifonRefresh()is called twice synchronously before React re-renders). This means re-entrant refreshes aren’t strictly ignored as the docstring claims. Use a ref-backed lock for an immediate guard and avoid readingisRefreshinginside the callback.
const [isRefreshing, setIsRefreshing] = useState(false)
const onRefresh = useCallback(() => {
if (isRefreshing) {
return
| @@ -0,0 +1,43 @@ | |||
| import { useQueryClient } from '@tanstack/react-query' | |||
| import { ReactQueryKeys } from 'consts/reactQueryKeys' | |||
| import { useCallback, useState } from 'react' | |||
…ions
HL rejects actions signed by an agent it no longer knows ('User or API
Wallet 0x… does not exist') — until now every order/cancel/close dead-ended
in that raw toast. Detect it in both failure shapes (business-reject string
and thrown error; the SDK matcher misses this wording), clear the stored
agent key so the manager falls back to master-wallet signing (retry works,
next enable-trading gate re-prompts), and show a session-expired message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| const referencePrice = markPrice > 0 ? markPrice : entryPrice | ||
| const { percent, suffix } = pctParts(pctFromEntry(limitPrice, referencePrice)) | ||
| return `${percent}${suffix}` | ||
| } |
Description
Ticket: CP-14878
Adds limit orders to the perps place-order flow and a tabbed My positions screen (positions / open orders / activity).
Place order — limit price
/perpetualsPlaceOrder/limitPrice, same pattern as the TP/SL trigger screen). The set price shows in a drill row with its % relation to the current pricePlaceOrderContextwith the same semantics as TP/SL: enabled only once a price exists, toggling off keeps the price for re-enableorderKind: 'limit'+limitPxthrough the existingusePerpsOrderSubmitplumbing (Gtc, TP/SL attachment supported)My positions — tabs
PositionsListwith the filter chips in its list headercancelOrderactionusePerpsPullToRefreshhookNo new dependencies. Figma: Place bet limit / Open positions
Screenshots/Videos
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-07-23.at.13.49.10.mov
Testing
iOS - 9395
Android - 9396
Dev Testing
Happy path:
maxOpenSizeCoin × limit priceEdge cases:
Bitrise:
iOS - 9391
Android - 9391
QA Testing
Checklist
Please check all that apply (if applicable)
🤖 Generated with Claude Code