Skip to content
Open
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
3 changes: 2 additions & 1 deletion apps/kyberswap-interface/.env
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ VITE_KYBER_AI_API_URL=https://kd-api.kyberswap.com/api
VITE_AFFILIATE_SERVICE=https://affiliate-service.kyberswap.com/api
VITE_SOLANA_RPC=https://solana-rpc.kyberswap.com

VITE_SMART_EXIT_API_URL=https://conditional-order.kyberswap.com/api
VITE_CONDITIONAL_SERVICE_URL=https://pre-conditional-order.kyberengineering.io/api
# VITE_CONDITIONAL_SERVICE_URL=https://conditional-order.kyberswap.com/api
VITE_CROSSCHAIN_AGGREGATOR_API=https://crosschain-aggregator.kyberswap.com
# VITE_CROSSCHAIN_AGGREGATOR_API=https://pre-crosschain-aggregator.kyberengineering.io
10 changes: 10 additions & 0 deletions apps/kyberswap-interface/src/assets/svg/ic_stoploss_recreate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ interface CurrencyInputPanelProps {
onClickSelect?: () => void
positionMax?: 'inline' | 'top'
label?: ReactNode
/** Rendered inside the panel, under the amount row — for a note that belongs to this field. */
footer?: ReactNode
positionLabel?: 'in' | 'out'
onCurrencySelect?: (currency: Currency) => void
onSwitchCurrency?: () => void
Expand Down Expand Up @@ -363,6 +365,7 @@ export default function CurrencyInputPanel({
onHalf,
positionMax = 'inline',
label = '',
footer,
positionLabel = 'out',
onCurrencySelect,
onSwitchCurrency,
Expand Down Expand Up @@ -488,6 +491,8 @@ export default function CurrencyInputPanel({
</CurrencySelect>
)}
</InputRow>

{footer}
</Container>
{!disableCurrencySelect && !isSwitchMode && onCurrencySelect && (
<TokenSelectorModal
Expand Down
30 changes: 26 additions & 4 deletions apps/kyberswap-interface/src/components/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import { useEffect, useState } from 'react'
import Picker from 'react-date-picker'

const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1)

/**
* The calendar is the control here, not the text field: the CSS collapses the input and shows the
* calendar inline. `isOpen` therefore drives it rather than focus — the picker renders no calendar at
* all until it has been opened, so leaving that to focus hides it the moment focus moves elsewhere.
*/
export default function DatePicker({ onChange, value }: { value: Date; onChange: (date: Date) => void }) {
const today = new Date()
const minDate = new Date(today.getFullYear(), today.getMonth(), today.getDate())
const dateKey = `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}`

/**
* Which month is on screen. The calendar keeps its own copy the moment the user pages through
* months, and that copy then outranks `value` — so a date chosen elsewhere (a preset, say) would
* change the selection while leaving the user looking at an unrelated month. Following `value`
* here re-anchors the view on every change; paging within an unchanged `value` is untouched.
*/
const valueTime = value.getTime()
const [activeStartDate, setActiveStartDate] = useState(() => startOfMonth(value))
useEffect(() => {
setActiveStartDate(startOfMonth(new Date(valueTime)))
}, [valueTime])

return (
<div className="ks-date-picker">
<Picker
key={dateKey}
isOpen
calendarIcon={null}
clearIcon={null}
autoFocus
calendarProps={{ className: 'custom-calendar' }}
calendarProps={{
className: 'custom-calendar',
activeStartDate,
onActiveStartDateChange: ({ activeStartDate: next }) => next && setActiveStartDate(next),
}}
className="custom-date-picker"
value={value}
closeCalendar={false}
Expand Down
13 changes: 12 additions & 1 deletion apps/kyberswap-interface/src/components/DropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type DropdownMenuProps = {
mobileFullWidth?: boolean
mobileHalfWidth?: boolean
usePortal?: boolean
/** Names the trigger; each option becomes `${dataTestId}-option-${value}`. */
dataTestId?: string
onChange: (value: string | number) => void
}

Expand All @@ -49,6 +51,7 @@ const DropdownMenu = ({
mobileFullWidth = false,
mobileHalfWidth = false,
usePortal = false,
dataTestId,
onChange,
}: DropdownMenuProps) => {
const [open, setOpen] = useState(false)
Expand Down Expand Up @@ -130,7 +133,13 @@ const DropdownMenu = ({
}

const dropdownContent = (
<DropdownContentWrapper ref={contentRef} $usePortal={usePortal} style={usePortal ? position : undefined}>
<DropdownContentWrapper
ref={contentRef}
$usePortal={usePortal}
style={usePortal ? position : undefined}
// A portalled menu is not a descendant of its trigger, so options carry the trigger's name.
data-testid={dataTestId && `${dataTestId}-menu`}
>
<ScrollIndicator $visible={canScrollUp} onClick={() => handleScrollClick('up')}>
<ChevronUp size={16} />
</ScrollIndicator>
Expand All @@ -139,6 +148,7 @@ const DropdownMenu = ({
<DropdownContentItem
key={option.value}
onClick={() => handleSelectItem(option.value)}
data-testid={dataTestId && `${dataTestId}-option-${option.value}`}
className={option.value === value ? 'selected' : ''}
>
{option.icon && <ItemIcon src={option.icon} alt={option.label} />}
Expand All @@ -165,6 +175,7 @@ const DropdownMenu = ({
background={background}
highlight={flatten && open}
onClick={handleOpenChange}
data-testid={dataTestId}
>
<DropdownTitle justifyContent={alignItems} width={width} fullWidth={fullWidth && !width}>
{optionValue?.icon && <ItemIcon src={optionValue.icon} alt={optionValue.label} />}
Expand Down
20 changes: 18 additions & 2 deletions apps/kyberswap-interface/src/components/ErrorWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@ type ErrorWarningProps = {
style?: CSSProperties
className?: string
action?: ReactNode
dataTestId?: string
}

export const ErrorWarning = ({ title, type, desc, style: customStyle = {}, className, action }: ErrorWarningProps) => {
export const ErrorWarning = ({
title,
type,
desc,
style: customStyle = {},
className,
action,
dataTestId,
}: ErrorWarningProps) => {
const detailsId = useId()
const { backgroundClass, colorClass, Icon } = WARNING_STYLES[type]
const [expanded, setExpanded] = useState(false)
Expand All @@ -30,6 +39,8 @@ export const ErrorWarning = ({ title, type, desc, style: customStyle = {}, class
<HStack
className={cn('items-start gap-2 rounded-2xl px-3 py-2', backgroundClass, colorClass, className)}
style={customStyle}
data-testid={dataTestId}
data-warning-type={type}
>
<Icon size={16} className="shrink-0" />
<div className="flex-1 text-xs font-medium italic text-text-60">{title}</div>
Expand All @@ -39,7 +50,12 @@ export const ErrorWarning = ({ title, type, desc, style: customStyle = {}, class
}

return (
<Stack className={cn('rounded-2xl px-3 py-2', backgroundClass, className)} style={customStyle}>
<Stack
className={cn('rounded-2xl px-3 py-2', backgroundClass, className)}
style={customStyle}
data-testid={dataTestId}
data-warning-type={type}
>
<HStack
className={cn('group cursor-pointer select-none items-start gap-2', colorClass)}
onClick={event => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ButtonHTMLAttributes } from 'react'
import { Calendar, ChevronDown } from 'react-feather'

import { HStack, Stack } from 'components/Stack'
import { DropdownIcon } from 'components/SwapForm/SlippageSetting'
import { DropdownIcon, type SettingGridCells } from 'components/SwapForm/SlippageSetting'
import { TextDashed } from 'components/Text'
import { MouseoverTooltip } from 'components/Tooltip'
import { TIMES_IN_SECS } from 'constants/index'
Expand Down Expand Up @@ -32,6 +32,8 @@ const ExpireOptionButton = ({
/>
)

export type ExpiryPresetOption = { value: number; label: string }

type Props = {
expiry?: {
expire?: number
Expand All @@ -44,13 +46,21 @@ type Props = {
onOpenDatePicker?: () => void
onExpireChange?: (val: Date | number) => void
}
/** Durations in seconds offered above the Custom Date entry. */
presetOptions?: ExpiryPresetOption[]
tooltip?: string
/** See `SettingGridCells`: splits the header and the panel across the caller's grid. */
gridCells?: SettingGridCells
}

const LimitOrderExpirySection = ({
expiry: { expire, expanded, customDateExpire, displayTime } = {},
events = {},
presetOptions,
tooltip,
gridCells,
}: Props) => {
const expirePresetOptions = [
const expirePresetOptions = presetOptions ?? [
{ value: TIMES_IN_SECS.ONE_HOUR, label: t`1 Hour` },
{ value: TIMES_IN_SECS.ONE_DAY, label: t`1 Day` },
{ value: 7 * TIMES_IN_SECS.ONE_DAY, label: t`7 Days` },
Expand All @@ -66,13 +76,15 @@ const LimitOrderExpirySection = ({
: expirePresetOptions.find(item => item.value === expire)?.label || displayTime

return (
<Stack>
<HStack className="items-center justify-between gap-1 text-subText">
<Stack className={cn(gridCells && 'contents')} data-testid="expiry-setting">
<HStack className={cn('items-center justify-between gap-1 text-subText', gridCells?.header)}>
<HStack className="items-center gap-2">
<TextDashed fontSize={14} className="flex h-fit items-center text-subText">
<MouseoverTooltip
placement="bottom"
text={t`Once an order expires, it will be cancelled automatically. No gas fees will be charged.`}
text={
tooltip ?? t`Once an order expires, it will be cancelled automatically. No gas fees will be charged.`
}
>
<Trans>Expires In</Trans>:
</MouseoverTooltip>
Expand All @@ -81,8 +93,11 @@ const LimitOrderExpirySection = ({
className="cursor-pointer items-center gap-1 hover:brightness-75"
role="button"
onClick={events.onToggleExpanded}
data-testid="expiry-setting-toggle"
>
<span className="text-sm font-medium text-text/80">{fullDisplayTime}</span>
<span className="text-sm font-medium text-text/80" data-testid="expiry-value">
{fullDisplayTime}
</span>
<DropdownIcon size={14} data-flip={expanded}>
<ChevronDown size={14} />
</DropdownIcon>
Expand All @@ -94,11 +109,17 @@ const LimitOrderExpirySection = ({
className={cn(
'grid transition-[grid-template-rows,opacity] duration-200 ease-in-out',
expanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',
// As a grid item the panel's automatic minimum size would hold the collapsed 0fr row open.
gridCells && 'min-h-0',
gridCells?.panel,
)}
>
<div className="min-h-0 overflow-hidden">
<div className="pt-2">
<div className="grid w-full max-w-full grid-cols-3 gap-1 rounded-[20px] bg-tabBackground p-1">
<div
className="grid w-full max-w-full grid-cols-3 gap-1 rounded-[20px] bg-tabBackground p-1"
data-testid="expiry-options"
>
{expireOptions.map(item => {
const active = customDateExpire ? item.custom : item.value === expire

Expand All @@ -111,6 +132,7 @@ const LimitOrderExpirySection = ({
}}
active={active}
custom={item.custom}
data-testid={item.custom ? 'expiry-option-custom' : `expiry-option-${item.value}`}
>
{item.custom ? (
<HStack as="span" className="items-center justify-center gap-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import MarketPrice from 'components/LimitOrder/Form/MarketPrice'
import { useLimitOrderFormState } from 'components/LimitOrder/Form/useLimitOrderFormState'
import { NetworkSelector } from 'components/NetworkSelector'
import { HStack, Stack } from 'components/Stack'
import OrderTypeSubTabs from 'components/StopLoss/OrderTypeSubTabs'
import ReverseTokenSelectionButton from 'components/SwapForm/ReverseTokenSelectionButton'
import { useActiveWeb3React } from 'hooks'
import { NETWORKS_INFO } from 'hooks/useChainsConfig'
Expand Down Expand Up @@ -172,6 +173,7 @@ const LimitOrderForm = ({ currencyIn: currencyInProp, currencyOut: currencyOutPr
return (
<>
<Stack className="gap-4">
{!isEmbeddedSwap && <OrderTypeSubTabs />}
{isEmbeddedSwap && <NetworkSelector chainId={form.chainId} />}
<Stack className="gap-3">
<LimitOrderInputTokenPanel {...tokenSectionProps} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type LimitOrderTokenPanelProps = {
tokens?: TokenSectionTokens
estimateUsd?: TokenSectionEstimateUsd
events?: TokenSectionEvents
/** Rendered inside the panel, under the amount row — for a note about the token chosen here. */
footer?: ReactNode
}

type TokenSectionTokens = {
Expand Down Expand Up @@ -45,11 +47,13 @@ export const LimitOrderInputTokenPanel = ({
tokens = {},
estimateUsd = DEFAULT_ESTIMATE_USD,
events = {},
footer,
}: LimitOrderTokenPanelProps) => {
const { currencyIn, currencyOut, inputAmount = '' } = tokens

return (
<CurrencyInputPanel
footer={footer}
value={inputAmount}
positionMax="top"
onUserInput={events.onInputAmountChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ const MyOrders = () => {
value={orderType}
width={130}
mobileHalfWidth
// The panel is clipped by the order list's rounded `overflow-hidden` shell, which cuts the
// last options off whenever the table is short. A portalled menu escapes that box.
usePortal
onChange={onSelectOrderType}
/>
<DropdownMenu
Expand Down
Loading
Loading