Compare commits

..
Author SHA1 Message Date
Adam Fraser 9058ccf6c1 test 2023-11-29 14:44:48 -05:00
Adam Fraser 2ab756ad71 Make additional markets visible 2023-11-29 09:49:01 -05:00
Bill 15a64232c0 Fix USDT approval (#170)
* Fix USDT approval

* address comment
2023-11-28 11:44:21 -08:00
aleka 28d3be0370 Release notification (#169)
* add release notif
2023-11-28 12:36:29 -05:00
Rui 90ff9ccc5a Add token address to cctp.json (#167)
* Add token address to cctp.json

* Update field name to tokenAddress
2023-11-27 17:55:52 -08:00
Jared Vu 33516422e7 Filter markets and add test flag (#168) 2023-11-27 17:05:19 -08:00
Rui eb31610a38 Add CCTP supported chains to cctp.json (#166) 2023-11-27 14:27:42 -08:00
Jared Vu b0e1655018 Bump Abacus@1.0.30 (#165) 2023-11-27 13:01:25 -08:00
Jared Vu b4cc5f87ec Add href prop to button component (#164)
* Add href prop to button component

* Use ButtonType.Link
2023-11-27 12:39:57 -08:00
Bill 549ae558e7 Default Locale to EN if not supported (#163) 2023-11-27 08:57:57 -08:00
Jared Vu 5b06110e2d Update Abacus@1.0.28 (#161) 2023-11-22 14:08:03 -08:00
aleka e3da2ee4b1 update copy and other polish (#157)
* update copy and other polish

* fix mobile nav menu nested buttons

* feedback
2023-11-22 15:06:21 -05:00
Rui 6b38d68b09 Remove universal link for trust wallet since it's no longer valid. (#160) 2023-11-21 19:25:03 -08:00
Jared Vu 1dedc93a56 Mobile friendly Toast: Collapse/Close (#159) 2023-11-21 19:24:19 -08:00
Jared Vu 1427b55728 MarketsTable: Sort OpenInterest by notional, Display 24h volume as fiat value (#158)
* Fix Sort by notional, Display fiat for volume

* OpenInterest notional on top
2023-11-21 16:10:23 -08:00
Jared Vu 20521b5b3f Add button to stack/unstack Toast notifications (#149)
* Add button to stack/unstack

* Button -> Toggle button, add offset for notifs
2023-11-20 15:12:38 -08:00
Jared Vu 532df49a06 Use new redux tradeFormInputs object for TradingForm input display values (#144)
* Redux slice for TradeFormInputs

* Move sideEffects from TradeForm to hook in Trade
2023-11-20 11:56:09 -08:00
aleka fb65db633a move buttons to footer in ack terms (#155) 2023-11-20 13:21:06 -05:00
aleka 279ef6b4f9 bump abacus (#153) 2023-11-13 10:57:40 -05:00
aleka 0def18db07 fix sorting 24h price change in markets table (#152) 2023-11-13 09:36:32 -05:00
Jared Vu 50bd860ced Bump Abacus 1.0.25 (#151) 2023-11-13 09:36:16 -05:00
Jared Vu 818574cedb Change dep in hook to fix input bug (#150) 2023-11-13 09:11:54 -05:00
aleka 3be0429a60 add back border to toggle buttons (#148) 2023-11-11 20:46:04 -08:00
aleka b8e72246b9 add manage funds dialog and transfers form styling (#147)
* add manage funds dialog and transfers form styling

* add sticky footer backdrop

* add testnet deposit toggle
2023-11-10 20:56:27 -08:00
Jared VuandBill He 99e708a984 Notifications Refresh (#135)
* Remove X button from tablet/mobile Notification menu

* Toast styling

* missed in merge

* .env.example

* Update abacus@1.0.9

* Add notif strings, and abaucs notif state

* Update Notification hooks, add Notification Preference controls

* Consolidate Notification into single component

* withLabel -> withTrigger prop

* nits

* OrderStatusChanged -> AbacusGenerated

* Notification OnClick

* Move Notification -> Components

* withAnimation -> $withAnimation

* Notification styling and click handle

* load state, onClick, localStorage

* add error count

* Rez localStorage Notifications

* Update TransferStatus UI

* Use Notification component for other abacusNotifs

* fix uselocalnotif query

* Default to Notification from NotificationMenu

---------

Co-authored-by: Bill He <bill@dydx.exchange>
2023-11-10 20:55:34 -08:00
aleka 876f620ada Account menu and components improvements (#145)
* better copy button

* hide sign into mobile for production and use new copy icon button

* new checkbox
2023-11-10 22:07:29 -05:00
Bill b9550890da Add additional onboarding entry points (#146)
* Add additional onboarding entry points

* flex-wrap
2023-11-10 19:05:46 -08:00
aleka 3732b955f5 add sample terms of use (#138)
* add sample terms of use

* add back article mixin
2023-11-10 14:58:26 -05:00
aleka 253bfc0eff update nav menu dropdown behavior to click instead of hover to open (#142) 2023-11-10 14:58:17 -05:00
98 changed files with 4186 additions and 1537 deletions
+5 -3
View File
@@ -39,14 +39,16 @@
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.0.24",
"@dydxprotocol/v4-abacus": "^1.0.30",
"@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.5",
"@dydxprotocol/v4-localization": "^1.0.8",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.1.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -109,7 +111,7 @@
"styled-components": "^5.3.11",
"use-latest": "^1.2.1",
"viem": "^1.12.2",
"wagmi": "^1.3.7"
"wagmi": "^1.4.5"
},
"devDependencies": {
"@babel/core": "^7.22.5",
+122 -81
View File
@@ -1,9 +1,5 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@0xsquid/sdk':
specifier: ^1.10.0
@@ -27,20 +23,23 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/v4-abacus':
specifier: ^1.0.24
version: 1.0.24
specifier: ^1.0.30
version: 1.0.30
'@dydxprotocol/v4-client-js':
specifier: ^1.0.0
version: 1.0.0
'@dydxprotocol/v4-localization':
specifier: ^1.0.5
version: 1.0.5
specifier: ^1.0.8
version: 1.0.8
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
'@js-joda/core':
specifier: ^5.5.3
version: 5.5.3
'@radix-ui/react-checkbox':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-collapsible':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -50,6 +49,9 @@ dependencies:
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.5
version: 2.0.5(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-icons':
specifier: ^1.3.0
version: 1.3.0(react@18.2.0)
'@radix-ui/react-navigation-menu':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -237,8 +239,8 @@ dependencies:
specifier: ^1.12.2
version: 1.12.2(typescript@5.1.3)
wagmi:
specifier: ^1.3.7
version: 1.3.7(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
specifier: ^1.4.5
version: 1.4.5(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
devDependencies:
'@babel/core':
@@ -982,8 +984,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/v4-abacus@1.0.24:
resolution: {integrity: sha512-wDGSjkrc3Se6Ev7UTjPgJV7PiyzZSz2mJwOTZikLwH7W3k1iPYGQmaCd7TudFk8h2aSdlEwNsQBCf0sLoyvHaQ==}
/@dydxprotocol/v4-abacus@1.0.30:
resolution: {integrity: sha512-zzJzNzMR3Stwv1Mu0XGUNlyo1V/ywo3yi3QSzE4mzUtXB+p5Shj5hRQCcwhHcNjLgp6a+T0BcRjBPa4QMETmwg==}
dev: false
/@dydxprotocol/v4-client-js@1.0.0:
@@ -1014,8 +1016,8 @@ packages:
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@1.0.5:
resolution: {integrity: sha512-zZm3GFp/ORz9YjigQFVBlcWmaxYY+XEdIw8AWN5nEbsuiA2Jay+/OA7lO9+nV1J1p1T+TgLo9iczIHJsAo3IsA==}
/@dydxprotocol/v4-localization@1.0.8:
resolution: {integrity: sha512-h5CSkWHPct1BWmiwJBeX9vOauXSwlx8RK9ryOsaCipJWS4AnBni07p3/jj3L/HgmU1BZXwfcr2hBouXVxxZS8A==}
dev: false
/@dydxprotocol/v4-proto@0.4.1:
@@ -2181,6 +2183,34 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.10
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@types/react': 18.2.14
'@types/react-dom': 18.2.6
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies:
@@ -2482,6 +2512,14 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-icons@1.3.0(react@18.2.0):
resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==}
peerDependencies:
react: ^16.x || ^17.x || ^18.x
dependencies:
react: 18.2.0
dev: false
/@radix-ui/react-id@1.0.0(react@18.2.0):
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
peerDependencies:
@@ -5937,26 +5975,12 @@ packages:
pretty-format: 27.5.1
dev: true
/@wagmi/chains@1.4.0(typescript@5.1.3):
resolution: {integrity: sha512-9HwJrhcZ1TxyrCbE10y7s1eSiSiyfGam7AHIOLYExaOX+vpOZ+MNTt4orFEDbEpz1fxwJDPPI38lanAUix1OSA==}
/@wagmi/connectors@3.1.3(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-UgwsQKQDFObJVJMf9pDfFoXTv710o4zrTHyhIWKBTMMkLpCMsMxN5+ZaDhBYt/BgoRinfRYQo8uwuwLhxE6Log==}
peerDependencies:
typescript: '>=5.0.4'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
typescript: 5.1.3
dev: false
/@wagmi/connectors@2.6.5(@wagmi/chains@1.4.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-klF31togMDd0qQqEcLl5cCGxjMbL0RRXQ8I1vxmEa3KdGzw6Z3ICVzX7/bDfnNEZcOW7BKyAnZDq7rCt5jTOiw==}
peerDependencies:
'@wagmi/chains': '>=1.3.0'
typescript: '>=5.0.4'
viem: '>=0.3.35'
peerDependenciesMeta:
'@wagmi/chains':
optional: true
typescript:
optional: true
dependencies:
@@ -5964,17 +5988,17 @@ packages:
'@ledgerhq/connect-kit-loader': 1.1.2
'@safe-global/safe-apps-provider': 0.17.1(typescript@5.1.3)
'@safe-global/safe-apps-sdk': 8.1.0(typescript@5.1.3)
'@wagmi/chains': 1.4.0(typescript@5.1.3)
'@walletconnect/ethereum-provider': 2.8.6(@walletconnect/modal@2.5.9)
'@walletconnect/ethereum-provider': 2.10.2(@walletconnect/modal@2.6.2)
'@walletconnect/legacy-provider': 2.0.0
'@walletconnect/modal': 2.5.9(react@18.2.0)
'@walletconnect/utils': 2.8.6
'@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/utils': 2.10.2
abitype: 0.8.7(typescript@5.1.3)
eventemitter3: 4.0.7
typescript: 5.1.3
viem: 1.12.2(typescript@5.1.3)
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
- '@types/react'
- bufferutil
- encoding
- lokijs
@@ -5984,8 +6008,8 @@ packages:
- zod
dev: false
/@wagmi/core@1.3.6(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-TXv9ZlRR5aySfERFuWMuo+lKXC/CoqtxVJZVHPqhK1jY+nldMx3AvrWzzF4CccRaMYcVdvPFepvmxzq2A2VvWg==}
/@wagmi/core@1.4.5(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-N9luRb1Uk4tBN9kaYcQSWKE9AsRt/rvZaFt5IZech4JPzNN2sQlfhKd9GEjOXYRDqEPHdDvos7qyBKiDNTz4GA==}
peerDependencies:
typescript: '>=5.0.4'
viem: '>=0.3.35'
@@ -5993,8 +6017,7 @@ packages:
typescript:
optional: true
dependencies:
'@wagmi/chains': 1.4.0(typescript@5.1.3)
'@wagmi/connectors': 2.6.5(@wagmi/chains@1.4.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
'@wagmi/connectors': 3.1.3(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
abitype: 0.8.7(typescript@5.1.3)
eventemitter3: 4.0.7
typescript: 5.1.3
@@ -6039,8 +6062,8 @@ packages:
- utf-8-validate
dev: false
/@walletconnect/core@2.8.6:
resolution: {integrity: sha512-rnSqm1KJLcww/v6+UH8JeibQkJ3EKgyUDPfEK0stSEkrIUIcXaFlq3Et8S+vgV8bPhI0MVUhAhFL5OJZ3t2ryg==}
/@walletconnect/core@2.10.2:
resolution: {integrity: sha512-JQz/xp3SLEpTeRQctdck2ugSBVEpMxoSE+lFi2voJkZop1hv6P+uqr6E4PzjFluAjeAnKlT1xvra0aFWjPWVcw==}
dependencies:
'@walletconnect/heartbeat': 1.2.1
'@walletconnect/jsonrpc-provider': 1.0.13
@@ -6053,8 +6076,8 @@ packages:
'@walletconnect/relay-auth': 1.0.4
'@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
events: 3.3.0
lodash.isequal: 4.5.0
uint8arrays: 3.1.1
@@ -6090,8 +6113,8 @@ packages:
tslib: 1.14.1
dev: false
/@walletconnect/ethereum-provider@2.8.6(@walletconnect/modal@2.5.9):
resolution: {integrity: sha512-wUvJEsXTLmMihrOhQxAs1k9hrWEOT03QBn54P9r9GpJbJ1zEfIjQaXFfi8uup6gldhH+vN38PsbOiLyv/6d3qQ==}
/@walletconnect/ethereum-provider@2.10.2(@walletconnect/modal@2.6.2):
resolution: {integrity: sha512-QMYFZ6+rVq2CJLdIPdKK0j1Qm66UA27oQU5V2SrL8EVwl7wFfm0Bq7fnL+qAWeDpn612dNeNErpk/ROa1zWlWg==}
peerDependencies:
'@walletconnect/modal': '>=2'
peerDependenciesMeta:
@@ -6102,11 +6125,11 @@ packages:
'@walletconnect/jsonrpc-provider': 1.0.13
'@walletconnect/jsonrpc-types': 1.0.3
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/modal': 2.5.9(react@18.2.0)
'@walletconnect/sign-client': 2.8.6
'@walletconnect/types': 2.8.6
'@walletconnect/universal-provider': 2.8.6
'@walletconnect/utils': 2.8.6
'@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/sign-client': 2.10.2
'@walletconnect/types': 2.10.2
'@walletconnect/universal-provider': 2.10.2
'@walletconnect/utils': 2.10.2
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6257,32 +6280,34 @@ packages:
tslib: 1.14.1
dev: false
/@walletconnect/modal-core@2.5.9(react@18.2.0):
resolution: {integrity: sha512-isIebwF9hOknGouhS/Ob4YJ9Sa/tqNYG2v6Ua9EkCqIoLimepkG5eC53tslUWW29SLSfQ9qqBNG2+iE7yQXqgw==}
/@walletconnect/modal-core@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-cv8ibvdOJQv2B+nyxP9IIFdxvQznMz8OOr/oR/AaUZym4hjXNL/l1a2UlSQBXrVjo3xxbouMxLb3kBsHoYP2CA==}
dependencies:
buffer: 6.0.3
valtio: 1.10.6(react@18.2.0)
valtio: 1.11.2(@types/react@18.2.14)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- react
dev: false
/@walletconnect/modal-ui@2.5.9(react@18.2.0):
resolution: {integrity: sha512-nfBaAT9Ls7RZTBBgAq+Nt/3AoUcinIJ9bcq5UHXTV3lOPu/qCKmUC/0HY3GvUK8ykabUAsjr0OAGmcqkB91qug==}
/@walletconnect/modal-ui@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-rbdstM1HPGvr7jprQkyPggX7rP4XiCG85ZA+zWBEX0dVQg8PpAgRUqpeub4xQKDgY7pY/xLRXSiCVdWGqvG2HA==}
dependencies:
'@walletconnect/modal-core': 2.5.9(react@18.2.0)
lit: 2.7.5
'@walletconnect/modal-core': 2.6.2(@types/react@18.2.14)(react@18.2.0)
lit: 2.8.0
motion: 10.16.2
qrcode: 1.5.3
transitivePeerDependencies:
- '@types/react'
- react
dev: false
/@walletconnect/modal@2.5.9(react@18.2.0):
resolution: {integrity: sha512-Zs2RvPwbBNRdBhb50FuJCxi3FJltt1KSpI7odjU/x9GTpTOcSOkmR66PBCy2JvNA0+ztnS1Xs0LVEr3lu7/Jzw==}
/@walletconnect/modal@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-eFopgKi8AjKf/0U4SemvcYw9zlLpx9njVN8sf6DAkowC2Md0gPU/UNEbH1Wwj407pEKnEds98pKWib1NN1ACoA==}
dependencies:
'@walletconnect/modal-core': 2.5.9(react@18.2.0)
'@walletconnect/modal-ui': 2.5.9(react@18.2.0)
'@walletconnect/modal-core': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/modal-ui': 2.6.2(@types/react@18.2.14)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- react
dev: false
@@ -6338,17 +6363,17 @@ packages:
- utf-8-validate
dev: false
/@walletconnect/sign-client@2.8.6:
resolution: {integrity: sha512-rOFTKTHP7oJfXgYHX7+SdB8VbcsEE3ZFG/bMdmZboWaBim1mrY3vUyDdKrNr0VgI3AwBiEQezQDfKxBX0pMSQQ==}
/@walletconnect/sign-client@2.10.2:
resolution: {integrity: sha512-vviSLV3f92I0bReX+OLr1HmbH0uIzYEQQFd1MzIfDk9PkfFT/LLAHhUnDaIAMkIdippqDcJia+5QEtT4JihL3Q==}
dependencies:
'@walletconnect/core': 2.8.6
'@walletconnect/core': 2.10.2
'@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.1
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 2.0.1
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6377,8 +6402,8 @@ packages:
- lokijs
dev: false
/@walletconnect/types@2.8.6:
resolution: {integrity: sha512-Z/PFa3W1XdxeTcCtdR6lUsFgZfU/69wWJBPyclPwn7cu1+eriuCr6XZXQpJjib3flU+HnwHiXeUuqZaheehPxw==}
/@walletconnect/types@2.10.2:
resolution: {integrity: sha512-luNV+07Wdla4STi9AejseCQY31tzWKQ5a7C3zZZaRK/di+rFaAAb7YW04OP4klE7tw/mJRGPTlekZElmHxO8kQ==}
dependencies:
'@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.1
@@ -6391,17 +6416,17 @@ packages:
- lokijs
dev: false
/@walletconnect/universal-provider@2.8.6:
resolution: {integrity: sha512-ln1RVv8+oHu9enOJ/oVkjiarneB+4vJCk16znOklIN2JtDHwB8iObDHlQH3UE6ynNTw1iRvaGuPR4g+YdIfB6w==}
/@walletconnect/universal-provider@2.10.2:
resolution: {integrity: sha512-wFgI0LbQ3D56sgaUMsgOHCM5m8WLxiC71BGuCKQfApgsbNMVKugYVy2zWHyUyi8sqTQHI+uSaVpDev4UHq9LEw==}
dependencies:
'@walletconnect/jsonrpc-http-connection': 1.0.7
'@walletconnect/jsonrpc-provider': 1.0.13
'@walletconnect/jsonrpc-types': 1.0.3
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 2.0.1
'@walletconnect/sign-client': 2.8.6
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
'@walletconnect/sign-client': 2.10.2
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6433,8 +6458,8 @@ packages:
- lokijs
dev: false
/@walletconnect/utils@2.8.6:
resolution: {integrity: sha512-wcy6e5+COYo7tfNnW8YqidnATdJDIW6vDiWWE7A1F78Sl/VflkaevB9cIgyn8eLdxC1SxXgGoeC2oLP90nnHJg==}
/@walletconnect/utils@2.10.2:
resolution: {integrity: sha512-syxXRpc2yhSknMu3IfiBGobxOY7fLfLTJuw+ppKaeO6WUdZpIit3wfuGOcc0Ms3ZPFCrGfyGOoZsCvgdXtptRg==}
dependencies:
'@stablelib/chacha20poly1305': 1.0.1
'@stablelib/hkdf': 1.0.1
@@ -6444,7 +6469,7 @@ packages:
'@walletconnect/relay-api': 1.0.9
'@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.8.6
'@walletconnect/types': 2.10.2
'@walletconnect/window-getters': 1.0.1
'@walletconnect/window-metadata': 1.0.1
detect-browser: 5.3.0
@@ -10373,6 +10398,14 @@ packages:
lit-html: 2.8.0
dev: false
/lit@2.8.0:
resolution: {integrity: sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==}
dependencies:
'@lit/reactive-element': 1.6.3
lit-element: 3.3.3
lit-html: 2.8.0
dev: false
/loader-runner@4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'}
@@ -13506,15 +13539,19 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/valtio@1.10.6(react@18.2.0):
resolution: {integrity: sha512-SxN1bHUmdhW6V8qsQTpCgJEwp7uHbntuH0S9cdLQtiohuevwBksbpXjwj5uDMA7bLwg1WKyq9sEpZrx3TIMrkA==}
/valtio@1.11.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=16.8'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
dependencies:
'@types/react': 18.2.14
proxy-compare: 2.5.1
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
@@ -13737,8 +13774,8 @@ packages:
- supports-color
dev: true
/wagmi@1.3.7(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-n4Qwby0QB8/JSGjc0gGn4MWZ0BT229agEg6saPGS6VswmPjJLdkLq9qxKn1JptXxPREmb/ZvwHxU++3Z+gQJVA==}
/wagmi@1.4.5(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-Ph62E6cO5n2Z8Z5LTyZrkaNprxTsbC4w0qZJT4OJdXrEELziI8z/b4FO6amVFXdu2rDp/wpvF56e4mhKC8/Kdw==}
peerDependencies:
react: '>=17.0.0'
typescript: '>=5.0.4'
@@ -13750,7 +13787,7 @@ packages:
'@tanstack/query-sync-storage-persister': 4.32.6
'@tanstack/react-query': 4.32.6(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query-persist-client': 4.32.6(@tanstack/react-query@4.32.6)
'@wagmi/core': 1.3.6(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
'@wagmi/core': 1.4.5(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
abitype: 0.8.7(typescript@5.1.3)
react: 18.2.0
typescript: 5.1.3
@@ -14099,3 +14136,7 @@ packages:
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+61
View File
@@ -0,0 +1,61 @@
[
{
"chainId": "42161",
"tokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"name": "Arbitrum"
},
{
"chainId": "43114",
"tokenAddress": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
"name": "Avalanche"
},
{
"chainId": "8453",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"name": "Base"
},
{
"chainId": "1",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"name": "Ethereum"
},
{
"chainId": "10",
"tokenAddress": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
"name": "OP Mainnet"
},
{
"chainId": "421613",
"tokenAddress": "0xfd064a18f3bf249cf1f87fc203e90d8f650f2d63",
"name": "Arbitrum Goerli"
},
{
"chainId": "43113",
"tokenAddress": "0x5425890298aed601595a70AB815c96711a31Bc65",
"name": "Avalanche Fuji"
},
{
"chainId": "84531",
"tokenAddress": "0xf175520c52418dfe19c8098071a252da48cd1c19",
"name": "Base Goerli"
},
{
"chainId": "5",
"tokenAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F",
"name": "Ethereum Goerli"
},
{
"chainId": "grand-1",
"tokenAddress": "uusdc",
"name": "Noble Testnet"
},
{
"chainId": "420",
"tokenAddress": "0xe05606174bac4a6364b31bd0eca4bf4dd368f8c6",
"name": "OP Goerli"
},
{
"chainId": "103",
"name": "Solana Devnet"
}
]
+1 -1
View File
@@ -199,7 +199,7 @@
},
"mobile": {
"native": "trust:",
"universal": "https://link.trustwallet.com"
"universal": ""
},
"desktop": {
"native": "",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 297 B

+390
View File
@@ -0,0 +1,390 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_upgradedAddress", "type": "address" }],
"name": "deprecate",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_spender", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "deprecated",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_evilUser", "type": "address" }],
"name": "addBlackList",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "upgradedAddress",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "balances",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "maximumFee",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "_totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "unpause",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_maker", "type": "address" }],
"name": "getBlackListStatus",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "", "type": "address" },
{ "name": "", "type": "address" }
],
"name": "allowed",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "paused",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "who", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "pause",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getOwner",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "newBasisPoints", "type": "uint256" },
{ "name": "newMaxFee", "type": "uint256" }
],
"name": "setParams",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "amount", "type": "uint256" }],
"name": "issue",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "amount", "type": "uint256" }],
"name": "redeem",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "_owner", "type": "address" },
{ "name": "_spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "name": "remaining", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "basisPointsRate",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "isBlackListed",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_clearedUser", "type": "address" }],
"name": "removeBlackList",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "MAX_UINT",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "newOwner", "type": "address" }],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_blackListedUser", "type": "address" }],
"name": "destroyBlackFunds",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "name": "_initialSupply", "type": "uint256" },
{ "name": "_name", "type": "string" },
{ "name": "_symbol", "type": "string" },
{ "name": "_decimals", "type": "uint256" }
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
"name": "Issue",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
"name": "Redeem",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "newAddress", "type": "address" }],
"name": "Deprecate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "name": "feeBasisPoints", "type": "uint256" },
{ "indexed": false, "name": "maxFee", "type": "uint256" }
],
"name": "Params",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "name": "_blackListedUser", "type": "address" },
{ "indexed": false, "name": "_balance", "type": "uint256" }
],
"name": "DestroyedBlackFunds",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
"name": "AddedBlackList",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
"name": "RemovedBlackList",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
},
{ "anonymous": false, "inputs": [], "name": "Pause", "type": "event" },
{ "anonymous": false, "inputs": [], "name": "Unpause", "type": "event" }
]
+3 -1
View File
@@ -13,7 +13,9 @@ export const Checkboxes: Story<CheckboxProps> = (args) => {
<Checkbox
{...args}
checked={isChecked}
onClick={(e: React.ChangeEvent) => setIsChecked((e.target as HTMLInputElement).checked)}
onCheckedChange={setIsChecked}
id="story-checkbox"
label="example label"
/>
</StoryWrapper>
);
+45 -72
View File
@@ -1,10 +1,14 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { Root, Indicator } from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
checked: boolean;
onClick: (e: React.ChangeEvent) => void;
onCheckedChange: (checked: boolean) => void;
id?: string;
label?: React.ReactNode;
};
type StyleProps = {
@@ -13,85 +17,54 @@ type StyleProps = {
export type CheckboxProps = ElementProps & StyleProps;
export const Checkbox: React.FC<CheckboxProps> = ({ checked, className, onClick }) => {
return (
<Styled.CheckboxWrapper className={className}>
<Styled.Checkbox
type="checkbox"
checked={checked}
onChange={onClick}
/>
<Styled.CustomCheckbox />
</Styled.CheckboxWrapper>
);
};
export const Checkbox: React.FC<CheckboxProps> = ({
checked,
className,
onCheckedChange,
id,
label,
}) => (
<Styled.Container>
<Styled.Root className={className} checked={checked} onCheckedChange={onCheckedChange} id={id}>
<Styled.Indicator>
<CheckIcon />
</Styled.Indicator>
</Styled.Root>
{label && <label htmlFor={id}>{label}</label>}
</Styled.Container>
);
const Styled: Record<string, AnyStyledComponent> = {};
Styled.CustomCheckbox = styled.span`
width: 1.25em;
height: 1.25em;
Styled.Container = styled.div`
${layoutMixins.row}
gap: 1ch;
font: var(--font-small-book);
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
label {
cursor: pointer;
}
`;
Styled.Root = styled(Root)`
--checkbox-backgroundColor: var(--color-layer-0);
min-width: 1.25rem;
height: 1.25rem;
border-radius: 0.375rem;
border: var(--border-width) solid var(--color-border);
background-color: var(--checkbox-backgroundColor);
&::after {
position: absolute;
content: '';
top: 0.25em;
left: 0.4375em;
width: 0.3125em;
height: 0.5em;
border: solid var(--checkbox-checkColor);
border-width: 0 0.125em 0.125em 0;
border-radius: 0.0625em;
opacity: 0;
transform: rotate(0deg) scale(0);
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
@media ${breakpoints.tablet} {
width: 1.5em;
height: 1.5em;
&::after {
top: 0.25em;
left: 0.4375em;
width: 0.4375em;
height: 0.625em;
}
&[data-state='checked'] {
--checkbox-backgroundColor: var(--color-accent);
}
`;
Styled.CheckboxWrapper = styled.div`
--checkbox-backgroundColor: var(--color-layer-1);
--checkbox-checkColor: var(--color-text-1);
Styled.Indicator = styled(Indicator)`
display: flex;
border-radius: 0.25em;
overflow: hidden;
position: relative;
cursor: pointer;
align-items: center;
justify-content: center;
> input:checked ~ ${Styled.CustomCheckbox}::after {
opacity: 1;
transform: rotate(40deg) scale(1);
}
`;
Styled.Checkbox = styled.input`
width: 1.25em;
height: 1.25em;
z-index: 1;
cursor: pointer;
opacity: 0;
@media ${breakpoints.tablet} {
width: 1.5em;
height: 1.5em;
}
color: var(--color-text-2);
`;
+5 -6
View File
@@ -1,16 +1,13 @@
import type { Story } from '@ladle/react';
import { Collapsible } from '@/components/Collapsible';
import { Collapsible, type CollapsibleProps } from '@/components/Collapsible';
import { StoryWrapper } from '.ladle/components';
import { IconName } from './Icon';
export const CollapsibleStory: Story<Parameters<typeof Collapsible>> = (args) => (
export const CollapsibleStory: Story<CollapsibleProps> = (args) => (
<StoryWrapper>
<Collapsible
label="Collapsible List of Items"
{...args}
>
<Collapsible {...args}>
<ul>
<li>Collapsible Item 1</li>
<li>Collapsible Item 2</li>
@@ -23,6 +20,8 @@ export const CollapsibleStory: Story<Parameters<typeof Collapsible>> = (args) =>
CollapsibleStory.args = {
disabled: false,
withTrigger: true,
label: 'Collapsible List of Items',
};
CollapsibleStory.argTypes = {
+24 -18
View File
@@ -14,6 +14,7 @@ type ElementProps = {
label: React.ReactNode;
triggerIcon?: IconName;
children: React.ReactNode;
withTrigger?: boolean;
};
type StyleProps = {
@@ -23,6 +24,8 @@ type StyleProps = {
fullWidth?: boolean;
};
export type CollapsibleProps = ElementProps & StyleProps;
export const Collapsible = ({
defaultOpen = false,
disabled,
@@ -35,26 +38,29 @@ export const Collapsible = ({
triggerIconSide = 'left',
fullWidth,
className,
}: ElementProps & StyleProps) => (
withTrigger = true,
}: CollapsibleProps) => (
<Styled.Root defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}>
<Styled.Trigger className={className} disabled={disabled}>
{triggerIconSide === 'right' && (
<>
{label}
{fullWidth && <HorizontalSeparatorFiller />}
</>
)}
{withTrigger && (
<Styled.Trigger className={className} disabled={disabled}>
{triggerIconSide === 'right' && (
<>
{label}
{fullWidth && <HorizontalSeparatorFiller />}
</>
)}
<Styled.TriggerIcon>
<Icon iconName={triggerIcon} />
</Styled.TriggerIcon>
{triggerIconSide === 'left' && (
<>
{fullWidth && <HorizontalSeparatorFiller />}
{label}
</>
)}
</Styled.Trigger>
<Styled.TriggerIcon>
<Icon iconName={triggerIcon} />
</Styled.TriggerIcon>
{triggerIconSide === 'left' && (
<>
{fullWidth && <HorizontalSeparatorFiller />}
{label}
</>
)}
</Styled.Trigger>
)}
<Styled.Content $transitionDuration={transitionDuration}>{children}</Styled.Content>
</Styled.Root>
);
+13 -2
View File
@@ -22,7 +22,12 @@ type PickComboxMenuProps<
MenuGroupValue extends string | number
> = Pick<
ComboboxMenuProps<MenuItemValue, MenuGroupValue>,
'inputPlaceholder' | 'onItemSelected' | 'slotEmpty' | 'withSearch' | 'withStickyLayout'
| 'inputPlaceholder'
| 'onItemSelected'
| 'slotEmpty'
| 'withItemBorders'
| 'withSearch'
| 'withStickyLayout'
>;
type PickDialogProps = Pick<
@@ -53,6 +58,7 @@ export const ComboboxDialogMenu = <
onItemSelected,
inputPlaceholder,
slotEmpty,
withItemBorders,
withSearch,
withStickyLayout = true,
children,
@@ -83,6 +89,7 @@ export const ComboboxDialogMenu = <
title={title}
inputPlaceholder={inputPlaceholder}
slotEmpty={slotEmpty}
withItemBorders={withItemBorders}
withSearch={withSearch}
withStickyLayout={withStickyLayout}
/>
@@ -95,6 +102,8 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)`
/* Params */
--comboboxDialogMenu-backgroundColor: var(--color-layer-2);
--comboboxDialogMenu-item-gap: 0.5rem;
--comboboxDialogMenu-item-padding: 0.5rem 1rem;
/* Overrides */
& {
@@ -113,10 +122,12 @@ Styled.Dialog = styled(Dialog)`
/* Net 0 sticky top inset (let stickyArea1 header stick to top) */
--stickyArea0-topGap: calc(-1 * var(--stickyArea0-topHeight));
overflow-x: clip;
}
`;
Styled.ComboboxMenu = styled(ComboboxMenu)`
--comboboxMenu-backgroundColor: var(--comboboxDialogMenu-backgroundColor);
--comboboxMenu-item-gap: var(--comboxDialogMenu-item-gap, 0.5rem);
--comboboxMenu-item-gap: var(--comboboxDialogMenu-item-gap);
--comboboxMenu-item-padding: var(--comboboxDialogMenu-item-padding);
`;
+56 -26
View File
@@ -2,7 +2,7 @@ import { Fragment, type ReactNode, useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { Command } from 'cmdk';
import { type MenuConfig } from '@/constants/menus';
import { MenuItem, type MenuConfig } from '@/constants/menus';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -20,6 +20,7 @@ type ElementProps<MenuItemValue extends string | number, MenuGroupValue extends
type StyleProps = {
className?: string;
withItemBorders?: boolean;
withStickyLayout?: boolean;
};
@@ -38,17 +39,11 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
withSearch = true,
className,
withItemBorders,
withStickyLayout,
}: ComboboxMenuProps<MenuItemValue, MenuGroupValue>) => {
const [highlightedCommand, setHighlightedCommand] = useState<MenuItemValue>();
const [searchValue, setSearchValue] = useState('');
// const inputRef = useRef<HTMLInputElement | null>(null);
// console.log({ commandValue: highlightedCommand });
// useEffect(() => {
// inputRef?.current?.focus();
// }, []);
return (
<Styled.Command
@@ -65,11 +60,14 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
{withSearch && (
<Styled.Header $withStickyLayout={withStickyLayout}>
<Styled.Input
// ref={inputRef}
/**
* Mobile Issue: Search Input will always trigger mobile keyboard drawer. There is no fix.
* https://github.com/pacocoursey/cmdk/issues/127
*/
autoFocus
type="search"
value={searchValue}
onValueChange={setSearchValue}
autoFocus
placeholder={inputPlaceholder}
/>
</Styled.Header>
@@ -80,6 +78,7 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
<Styled.Group
key={group.group}
heading={group.groupLabel}
$withItemBorders={withItemBorders}
$withStickyLayout={withStickyLayout}
>
{group.items.map((item) => (
@@ -99,22 +98,31 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
}
}}
disabled={item.disabled}
$withItemBorders={withItemBorders}
>
{item.slotBefore}
<Styled.ItemLabel>
<span>
{`${item.label}${item.subitems?.length ? '…' : ''}`}
{item.tag && (
<>
{' '}
<Tag>{item.tag}</Tag>
</>
{
<>
{item.slotBefore}
{item.slotCustomContent ?? (
<Styled.ItemLabel>
<span>
{typeof item.label === 'string'
? `${item.label}${item.subitems?.length ? '…' : ''}`
: item.label}
{item.tag && (
<>
{' '}
<Tag>{item.tag}</Tag>
</>
)}
</span>
{item.description && <span>{item.description}</span>}
</Styled.ItemLabel>
)}
</span>
{item.description && <span>{item.description}</span>}
</Styled.ItemLabel>
{item.slotAfter}
{item.subitems && '→'}
{item.slotAfter}
{item.subitems && '→'}
</>
}
</Styled.Item>
{searchValue &&
@@ -137,6 +145,7 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
onItemSelected?.();
}}
disabled={subitem.disabled}
$withItemBorders={withItemBorders}
>
{subitem.slotBefore}
<Styled.ItemLabel>
@@ -169,14 +178,17 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>`
--comboboxMenu-backgroundColor: var(--color-layer-2);
--comboboxMenu-input-backgroundColor: var(--color-layer-3);
--comboboxMenu-input-height: 2.5rem;
--comboboxMenu-item-checked-backgroundColor: ;
--comboboxMenu-item-checked-textColor: ;
--comboboxMenu-item-highlighted-backgroundColor: var(--color-layer-3);
--comboboxMenu-item-highlighted-textColor: var(--color-text-1);
--comboboxMenu-item-backgroundColor: ;
--comboboxMenu-item-gap: 0.5rem;
--comboboxMenu-item-padding: 0.5em 1em;
display: grid;
align-content: start;
@@ -225,7 +237,7 @@ Styled.Input = styled(Command.Input)`
gap: 0.5rem;
`;
Styled.Group = styled(Command.Group)<{ $withStickyLayout?: boolean }>`
Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLayout?: boolean }>`
color: var(--color-text-0);
> [cmdk-group-heading] {
@@ -242,12 +254,21 @@ Styled.Group = styled(Command.Group)<{ $withStickyLayout?: boolean }>`
> [cmdk-group-heading] {
${layoutMixins.stickyHeader}
z-index: 2;
}
> [cmdk-group-items] {
${layoutMixins.stickyArea3}
}
`}
${({ $withItemBorders }) =>
$withItemBorders &&
css`
> [cmdk-group-items] {
padding: var(--border-width) 0;
}
`}
`;
Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
@@ -274,13 +295,14 @@ Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
`}
`;
Styled.Item = styled(Command.Item)`
Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>`
${layoutMixins.scrollSnapItem}
${popoverMixins.item}
--item-checked-backgroundColor: var(--comboboxMenu-item-checked-backgroundColor);
--item-checked-textColor: var(--comboboxMenu-item-checked-textColor);
--item-highlighted-textColor: var(--comboboxMenu-item-highlighted-textColor);
--item-gap: var(--comboboxMenu-item-gap);
--item-padding: var(--comboboxMenu-item-padding);
background-color: var(--comboboxMenu-backgroundColor, inherit);
@@ -291,6 +313,12 @@ Styled.Item = styled(Command.Item)`
opacity: 0.75;
cursor: not-allowed;
}
${({ $withItemBorders }) =>
$withItemBorders &&
css`
${layoutMixins.withOuterBorder}
`}
`;
Styled.ItemLabel = styled.div`
@@ -309,6 +337,8 @@ Styled.ItemLabel = styled.div`
opacity: 0.8;
}
}
min-width: 0;
`;
Styled.Empty = styled(Command.Empty)`
+3 -3
View File
@@ -15,10 +15,10 @@ CopyButtonStory.args = {
};
CopyButtonStory.argTypes = {
shownAsText: {
options: [true, false],
buttonType: {
options: ["text", "icon", "default"],
control: { type: 'select' },
defaultValue: false,
defaultValue: "default",
},
children: {
options: ['some text to copy'],
+41 -4
View File
@@ -9,14 +9,21 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { Button, ButtonProps } from './Button';
import { Icon, IconName } from './Icon';
import { IconButton } from './IconButton';
import { WithTooltip } from './WithTooltip';
export type CopyButtonProps = {
value?: string;
shownAsText?: boolean;
buttonType?: 'text' | 'icon' | 'default';
children?: React.ReactNode;
} & ButtonProps;
export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: CopyButtonProps) => {
export const CopyButton = ({
value,
buttonType = 'default',
children,
...buttonProps
}: CopyButtonProps) => {
const stringGetter = useStringGetter();
const [copied, setCopied] = useState(false);
@@ -28,11 +35,23 @@ export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: Cop
setTimeout(() => setCopied(false), 500);
};
return shownAsText ? (
return buttonType === 'text' ? (
<Styled.InlineRow onClick={onCopy} copied={copied}>
{children}
<Icon iconName={IconName.Copy} />
<Styled.Icon copied={copied} iconName={copied ? IconName.Check : IconName.Copy} />
</Styled.InlineRow>
) : buttonType === 'icon' ? (
<WithTooltip
tooltipString={stringGetter({ key: copied ? STRING_KEYS.COPIED : STRING_KEYS.COPY })}
>
<Styled.IconButton
{...buttonProps}
copied={copied}
action={ButtonAction.Base}
iconName={copied ? IconName.Check : IconName.Copy}
onClick={onCopy}
/>
</WithTooltip>
) : (
<Button
{...buttonProps}
@@ -63,3 +82,21 @@ Styled.InlineRow = styled.div<{ copied: boolean }>`
}
`}
`;
Styled.Icon = styled(Icon)<{ copied: boolean }>`
${({ copied }) =>
copied &&
css`
color: var(--color-positive);
`}
`;
Styled.IconButton = styled(IconButton)<{ copied: boolean }>`
${({ copied }) =>
copied &&
css`
svg {
color: var(--color-positive);
}
`}
`;
+38 -28
View File
@@ -38,6 +38,7 @@ type ElementProps = {
slotTrigger?: React.ReactNode;
slotHeaderInner?: React.ReactNode;
slotFooter?: React.ReactNode;
withClose?: boolean;
};
type StyleProps = {
@@ -46,6 +47,7 @@ type StyleProps = {
hasHeaderBorder?: boolean;
children?: React.ReactNode;
className?: string;
withAnimation?: boolean;
};
export type DialogProps = ElementProps & StyleProps;
@@ -79,9 +81,11 @@ export const Dialog = ({
slotTrigger,
slotHeaderInner,
slotFooter,
withClose = true,
placement = DialogPlacement.Default,
portalContainer,
hasHeaderBorder = false,
withAnimation = false,
children,
className,
}: DialogProps) => {
@@ -105,6 +109,7 @@ export const Dialog = ({
e.preventDefault();
}
}}
$withAnimation={withAnimation}
>
<Styled.Header $withBorder={hasHeaderBorder}>
<Styled.HeaderTopRow>
@@ -114,7 +119,7 @@ export const Dialog = ({
{title && <Styled.Title>{title}</Styled.Title>}
{!preventClose && (
{!preventClose && withClose && (
<Styled.Close ref={closeButtonRef}>
<Icon iconName={IconName.Close} />
</Styled.Close>
@@ -168,7 +173,7 @@ Styled.Overlay = styled(Overlay)`
}
`;
Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?: boolean }>`
/* Params */
--dialog-inset: 1rem;
--dialog-width: 30rem;
@@ -224,7 +229,7 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
outline: none;
${({ placement }) =>
${({ placement, $withAnimation }) =>
({
[DialogPlacement.Default]: css`
inset: var(--dialog-inset);
@@ -260,9 +265,11 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
padding-bottom: var(--dialog-radius); */
}
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
opacity: 0;
}
@@ -270,18 +277,19 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
max-height: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
opacity: 0;
scale: 0.9;
max-height: 0;
}
`} 0.15s;
}
}
}
`}
`,
[DialogPlacement.Sidebar]: css`
--dialog-width: var(--sidebar-width);
@@ -291,50 +299,52 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
margin-left: auto;
}
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
translate: 100% 0;
opacity: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
translate: 100% 0;
opacity: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
}
`}
`,
[DialogPlacement.Inline]: css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
scale: 0.99;
opacity: 0;
/* filter: blur(2px); */
/* backdrop-filter: none; */
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
scale: 0.99;
opacity: 0;
/* filter: blur(2px); */
/* backdrop-filter: none; */
}
`} 0.15s var(--ease-out-expo);
}
}
}
`}
`,
[DialogPlacement.FullScreen]: css`
--dialog-width: 100vw;
+4 -1
View File
@@ -9,6 +9,7 @@ import {
RadioItem,
Portal,
} from '@radix-ui/react-dropdown-menu';
import { CheckIcon } from '@radix-ui/react-icons';
import { type MenuItem } from '@/constants/menus';
@@ -90,7 +91,9 @@ export const DropdownSelectMenu = <MenuItemValue extends string>({
{slotAfter}
<Styled.ItemIndicator>{/* <CheckIcon /> */}</Styled.ItemIndicator>
<Styled.ItemIndicator>
<CheckIcon />
</Styled.ItemIndicator>
</Styled.RadioItem>
))}
</RadioGroup>
+2 -29
View File
@@ -1,43 +1,16 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
import { Icon, IconName } from '@/components/Icon';
export const GreenCheckCircle = ({ className }: { className?: string }) => (
<Styled.GreenCheckCircle className={className}>
<Icon iconName={IconName.Check} />
</Styled.GreenCheckCircle>
<Styled.Icon className={className} iconName={IconName.CheckCircle} />
);
const Styled: Record<string, AnyStyledComponent> = {};
Styled.GreenCheckCircle = styled.div`
${layoutMixins.stack}
Styled.Icon = styled(Icon)`
--icon-size: 1.25rem;
--icon-border-width: 3px;
width: var(--icon-size);
height: var(--icon-size);
border-radius: 50%;
align-items: center;
&:before {
content: '';
width: calc(var(--icon-size) - var(--icon-border-width) * 2);
height: calc(var(--icon-size) - var(--icon-border-width) * 2);
border: solid var(--icon-border-width) var(--color-positive);
border-radius: 50%;
opacity: 0.6;
}
svg {
width: calc(var(--icon-size) / 3);
height: calc(var(--icon-size) / 3);
color: var(--color-positive);
justify-self: center;
}
`;
+6
View File
@@ -15,6 +15,7 @@ import {
CautionCircleIcon,
ChatIcon,
CheckIcon,
CheckCircleIcon,
ChevronLeftIcon,
ChevronRightIcon,
ClockIcon,
@@ -52,6 +53,7 @@ import {
OrderUntriggeredIcon,
OverviewIcon,
PencilIcon,
PlayIcon,
PositionsIcon,
PriceChartIcon,
PrivacyIcon,
@@ -86,6 +88,7 @@ export enum IconName {
CautionCircleStroked = 'CautionCircleStroked',
Chat = 'Chat',
Check = 'Check',
CheckCircle = 'CheckCircle',
ChevronLeft = 'ChevronLeft',
ChevronRight = 'ChevronRight',
Clock = 'Clock',
@@ -124,6 +127,7 @@ export enum IconName {
OrderUntriggered = 'OrderUntriggered',
Overview = 'Overview',
Pencil = 'Pencil',
Play = 'Play',
Positions = 'Positions',
PriceChart = 'PriceChart',
Privacy = 'Privacy',
@@ -158,6 +162,7 @@ const icons = {
[IconName.CautionCircleStroked]: CautionCircleStrokeIcon,
[IconName.Chat]: ChatIcon,
[IconName.Check]: CheckIcon,
[IconName.CheckCircle]: CheckCircleIcon,
[IconName.ChevronLeft]: ChevronLeftIcon,
[IconName.ChevronRight]: ChevronRightIcon,
[IconName.Clock]: ClockIcon,
@@ -195,6 +200,7 @@ const icons = {
[IconName.OrderUntriggered]: OrderUntriggeredIcon,
[IconName.Overview]: OverviewIcon,
[IconName.Pencil]: PencilIcon,
[IconName.Play]: PlayIcon,
[IconName.Positions]: PositionsIcon,
[IconName.PriceChart]: PriceChartIcon,
[IconName.Privacy]: PrivacyIcon,
+11 -64
View File
@@ -1,4 +1,4 @@
import { forwardRef, Ref, useEffect, useRef, useState } from 'react';
import { forwardRef, Ref } from 'react';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { NavLink, matchPath, useLocation } from 'react-router-dom';
@@ -21,8 +21,6 @@ import {
import { layoutMixins } from '@/styles/layoutMixins';
import { isTruthy } from '@/lib/isTruthy';
import { Tag } from './Tag';
import { Icon, IconName } from './Icon';
@@ -61,7 +59,6 @@ const NavItem = forwardRef(
<>
{slotBefore}
<span>
{/* {`${label}${subitems?.length ? ' ' : ''}`} */}
{label}
{tag && (
<>
@@ -100,8 +97,6 @@ const NavItem = forwardRef(
}
);
type TriggerRef = HTMLAnchorElement | HTMLDivElement | HTMLButtonElement | null;
export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue extends string>({
onSelectItem,
onSelectGroup,
@@ -112,33 +107,6 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
dir = 'ltr',
className,
}: ElementProps<MenuItemValue, MenuGroupValue> & StyleProps) => {
// Disable click (close) in the first 500ms after hover (open)
// https://github.com/radix-ui/primitives/issues/1630#issuecomment-1545995075
const [clickIsDisabled, setClickIsDisabled] = useState(false);
const triggerRefs = useRef({} as { [value: string]: TriggerRef });
useEffect(() => {
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'data-state' &&
(mutation.target as unknown as HTMLOrSVGElement).dataset.state === 'open' &&
mutation.target !== document.activeElement
) {
setClickIsDisabled(true);
setTimeout(() => setClickIsDisabled(false), 500);
}
}
});
for (const element of Object.values(triggerRefs.current).filter(isTruthy)) {
observer.observe(element, { attributes: true });
}
return () => observer.disconnect();
}, []);
const renderSubitems = ({
item,
depth = 0,
@@ -148,18 +116,18 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
}) => (
<>
<Styled.SubMenuTrigger
asChild={depth > 0}
ref={(ref: TriggerRef) => (triggerRefs.current[item.value] = ref)}
onClick={(e: MouseEvent) => {
if (clickIsDisabled) {
e.preventDefault();
}
}}
asChild
onPointerMove={(e: MouseEvent) => e.preventDefault()}
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
>
<Styled.NavItem onSelect={onSelectGroup} orientation={itemOrientation} {...item} />
</Styled.SubMenuTrigger>
<Styled.Content data-placement={submenuPlacement}>
<Styled.Content
onPointerEnter={(e: MouseEvent) => e.preventDefault()}
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
data-placement={submenuPlacement}
>
<Styled.Sub data-placement={submenuPlacement}>
<Styled.List
data-orientation={depth > 0 ? 'menu' : orientation === 'vertical' ? 'vertical' : 'menu'}
@@ -410,25 +378,12 @@ Styled.List = styled(List)`
`;
Styled.ListItem = styled(Item)`
/* display: contents; */
display: grid;
position: relative;
${Styled.List}[data-orientation="horizontal"] > & {
gap: var(--submenu-side-offset);
}
/* ${Styled.List}[data-orientation="menu"] > & {
grid-template-columns: 1fr 0;
align-items: start;
gap: 2rem;
} */
/* &:has([data-state="open"]) {
position: sticky;
left: 0;
right: 0;
} */
`;
Styled.SubMenuTrigger = styled(Trigger)`
@@ -436,11 +391,6 @@ Styled.SubMenuTrigger = styled(Trigger)`
outline-offset: -2px;
&[data-state='open'] {
div {
background-color: var(--navigationMenu-item-checked-backgroundColor);
color: var(--navigationMenu-item-checked-textColor);
}
svg {
rotate: 0.5turn;
}
@@ -452,11 +402,10 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
subitems?.length
? css`
${popoverMixins.trigger}
--trigger-backgroundColor: transparent;
--trigger-open-backgroundColor: var(--color-layer-3);
--trigger-open-backgroundColor: var(--navigationMenu-item-checked-backgroundColor);
`
: css`
&:hover:not(.active) {
&:hover:not(:active) {
background-color: var(--navigationMenu-item-highlighted-backgroundColor);
color: var(--navigationMenu-item-highlighted-textColor);
}
@@ -470,8 +419,6 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
--item-radius: var(--navigationMenu-item-radius);
--item-padding: var(--navigationMenu-item-padding);
/* ${popoverMixins.backdropOverlay} */
${layoutMixins.scrollSnapItem}
min-height: var(--navigationMenu-item-height);
+201
View File
@@ -0,0 +1,201 @@
import React, { type MouseEvent } from 'react';
import styled, { css } from 'styled-components';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import {
NotificationStatus,
type Notification as NotificationDataType,
} from '@/constants/notifications';
import { useNotifications } from '@/hooks/useNotifications';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import { IconButton } from '@/components/IconButton';
import { IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
type ElementProps = {
notification: NotificationDataType;
onClick?: () => void;
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotTitleLeft?: React.ReactNode;
slotTitleRight?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
withClose?: boolean;
};
type StyleProps = {
className?: string;
isToast?: boolean;
};
export type NotificationProps = ElementProps & StyleProps;
export const Notification = ({
className,
isToast,
notification,
onClick,
slotIcon,
slotTitle,
slotTitleLeft,
slotTitleRight,
slotDescription,
slotCustomContent,
slotAction,
withClose = !isToast,
}: NotificationProps) => {
const { markCleared, markSeen } = useNotifications();
return (
<$Container className={className} isToast={isToast} onClick={onClick}>
<$Header>
{slotTitleLeft ?? (slotIcon && <$Icon>{slotIcon}</$Icon>)}
<$Title>{slotTitle}</$Title>
{slotTitleRight}
{withClose && !isToast && (
<$ActionItems>
<$Output
type={OutputType.RelativeTime}
value={notification.timestamps[NotificationStatus.Triggered]}
/>
{notification.status < NotificationStatus.Seen ? <$UnreadIndicator /> : null}
{notification.status < NotificationStatus.Cleared ? (
<$IconButton
iconName={IconName.Close}
shape={ButtonShape.Square}
size={ButtonSize.XSmall}
onClick={(e: MouseEvent) => {
e.stopPropagation();
if (notification.status < NotificationStatus.Seen) {
markSeen(notification);
} else if (notification.status < NotificationStatus.Cleared) {
markCleared(notification);
}
}}
/>
) : null}
</$ActionItems>
)}
</$Header>
<$Description>{slotCustomContent ?? slotDescription}</$Description>
{slotAction && <$Action>{slotAction}</$Action>}
</$Container>
);
};
const $Container = styled.div<{ isToast?: boolean }>`
// Params
--toast-icon-size: 1.75em;
// Rules
${popoverMixins.popover}
overflow: visible;
padding: 1rem 1.25rem;
${({ isToast }) =>
isToast
? css`
padding: 1rem;
box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
`
: css`
flex: 1;
background-color: transparent;
border-radius: 0;
backdrop-filter: none;
`}
`;
const $Header = styled.header`
${layoutMixins.row}
position: relative;
`;
const $Icon = styled.div`
${layoutMixins.row}
float: left;
margin-right: 0.5rem;
line-height: 1;
> svg,
img {
width: 1.5rem;
height: 1.5rem;
}
`;
const $Title = styled.div`
flex: 1;
font: var(--font-base-medium);
color: var(--color-text-2);
overflow: hidden;
text-overflow: ellipsis;
`;
const $Description = styled.div`
margin-top: 0.5rem;
color: var(--color-text-0);
font: var(--font-small-book);
`;
const $Action = styled.div`
margin-top: 0.5rem;
font: var(--font-small-book);
`;
const $ActionItems = styled.div`
min-width: fit-content;
padding-left: 0.5rem;
height: 100%;
${layoutMixins.row};
justify-content: end;
gap: 0.5rem;
position: absolute;
right: 0;
top: 0;
background-color: var(--color-layer-2);
font: var(--font-mini-book);
display: none;
z-index: 1;
${$Container}:hover & {
display: flex;
}
`;
const $UnreadIndicator = styled.div`
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: var(--color-accent);
border: 1px solid var(--color-layer-2);
`;
const $Output = styled(Output)`
color: var(--color-text-0);
`;
const $IconButton = styled(IconButton)`
--button-border: none;
--button-textColor: var(--color-text-0);
--button-hover-textColor: var(--color-text-1);
`;
+12 -7
View File
@@ -12,6 +12,8 @@ import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { WithLabel } from '@/components/WithLabel';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import breakpoints from '@/styles/breakpoints';
type ElementProps = {
asChild?: boolean;
@@ -84,27 +86,30 @@ export const SearchSelectMenu = ({
const Styled: Record<string, AnyStyledComponent> = {};
Styled.SearchSelectMenu = styled.div`
${layoutMixins.column}
`;
Styled.MenuTrigger = styled.div`
height: var(--form-input-height);
${layoutMixins.spacedRow}
align-items: center;
padding: 0 1rem;
padding: var(--form-input-paddingY) var(--form-input-paddingX);
@media ${breakpoints.tablet} {
height: var(--form-input-height-mobile);
}
`;
Styled.WithLabel = styled(WithLabel)`
--label-textColor: var(--color-text-0);
gap: 0.25rem;
${formMixins.inputLabel}
label {
font: var(--font-mini-book);
}
`;
Styled.SearchSelectMenu = styled.div`
${layoutMixins.column}
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
+15 -9
View File
@@ -1,5 +1,5 @@
import React from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import {
Root,
@@ -13,11 +13,13 @@ import {
Portal,
Icon as SelectIcon,
} from '@radix-ui/react-select';
import { CheckIcon } from '@radix-ui/react-icons';
import { popoverMixins } from '@/styles/popoverMixins';
import { formMixins } from '@/styles/formMixins';
import { WithLabel } from '@/components/WithLabel';
import { Icon, IconName } from './Icon';
export const SelectMenu = <T extends string>({
children,
@@ -44,7 +46,9 @@ export const SelectMenu = <T extends string>({
) : (
<Value />
)}
{React.Children.toArray(children).length > 1 && <Styled.DropdownIcon />}
{React.Children.toArray(children).length > 1 && (
<Styled.TriggerIcon iconName={IconName.Triangle} />
)}
</Styled.Trigger>
<Portal>
<Styled.Content className={className}>
@@ -68,7 +72,9 @@ export const SelectItem = <T extends string>({
}) => (
<Styled.Item className={className} value={value}>
<ItemText>{label}</ItemText>
<Styled.ItemIndicator>{/* <CheckIcon /> */}</Styled.ItemIndicator>
<Styled.ItemIndicator>
<CheckIcon />
</Styled.ItemIndicator>
</Styled.Item>
);
@@ -88,12 +94,6 @@ Styled.Trigger = styled(Trigger)<{ $withBlur?: boolean }>`
}
`;
Styled.DropdownIcon = styled(SelectIcon)`
font-size: 0.675em;
color: var(--color-text-1);
margin-left: auto;
`;
Styled.Content = styled(Content)`
--select-menu-content-maxWidth: ;
max-width: var(--select-menu-content-maxWidth);
@@ -116,3 +116,9 @@ Styled.WithLabel = styled(WithLabel)`
${formMixins.inputLabel}
border-radius: 0;
`;
Styled.TriggerIcon = styled(Icon)`
width: 0.625rem;
height: 0.375rem;
color: var(--color-text-0);
`;
+76 -77
View File
@@ -1,24 +1,19 @@
import React, { useEffect, useRef, useState } from 'react';
import { Root, Title, Description, Action, Close } from '@radix-ui/react-toast';
import { type MouseEvent, useEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import { Root, Action, Close } from '@radix-ui/react-toast';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { popoverMixins } from '@/styles/popoverMixins';
import { breakpoints } from '@/styles';
import { Notification, type NotificationProps } from '@/components/Notification';
import { IconButton } from './IconButton';
import { CloseIcon } from '@/icons';
import styled, { keyframes } from 'styled-components';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import { IconName } from './Icon';
type ElementProps = {
isOpen?: boolean;
setIsOpen?: (isOpen: boolean, isClosedFromTimeout?: boolean) => void;
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
actionDescription?: string;
actionAltText?: string;
sensitivity?: 'foreground' | 'background';
@@ -26,13 +21,22 @@ type ElementProps = {
lastUpdated?: number;
};
type StyleProps = {};
type StyleProps = {
className?: string;
};
type ToastProps = NotificationProps & ElementProps & StyleProps;
export const Toast = ({
className,
isOpen = true,
notification,
onClick,
setIsOpen,
slotIcon,
slotTitle,
slotTitleLeft,
slotTitleRight,
slotDescription,
slotCustomContent,
slotAction,
@@ -41,7 +45,7 @@ export const Toast = ({
sensitivity = 'background',
duration = Infinity,
lastUpdated,
}: ElementProps & StyleProps) => {
}: ToastProps) => {
// Timeout
const timeout = useRef<number>();
const [isPaused, setIsPaused] = useState(false);
@@ -58,37 +62,45 @@ export const Toast = ({
return (
<$Root
className={className}
type={sensitivity}
duration={Infinity}
open={isOpen}
onOpenChange={setIsOpen}
onPause={() => setIsPaused(true)}
onResume={() => setIsPaused(false)}
onClick={() => {
setIsOpen?.(false, false);
onClick?.();
}}
>
<div>
<$Container>
<$Header>
{slotIcon && <$Icon>{slotIcon}</$Icon>}
<Close asChild>
<$CloseButton
iconName={IconName.Close}
shape={ButtonShape.Circle}
size={ButtonSize.XSmall}
onClick={(e: MouseEvent) => e.stopPropagation()}
/>
</Close>
<Close asChild>
<$CloseButton
iconComponent={CloseIcon}
shape={ButtonShape.Square}
size={ButtonSize.XSmall}
/>
</Close>
<$Title>{slotTitle}</$Title>
</$Header>
{!slotCustomContent && <$Description>{slotDescription}</$Description>}
{slotCustomContent}
{actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
)}
</$Container>
</div>
{slotCustomContent ?? (
<$Notification
isToast
notification={notification}
slotIcon={slotIcon}
slotTitle={slotTitle}
slotTitleLeft={slotTitleLeft}
slotTitleRight={slotTitleRight}
slotDescription={slotDescription}
slotAction={
actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
)
}
/>
)}
</$Root>
);
};
@@ -114,7 +126,7 @@ const $Root = styled(Root)`
translate: var(--x) var(--y);
will-change: translate, margin-top;
margin-bottom: var(--toasts-gap);
/* margin-bottom: var(--toasts-gap); */
&[data-swipe-direction='right'] {
&[data-state='open'] {
@@ -163,6 +175,10 @@ const $Root = styled(Root)`
cursor: n-resize;
}
&:focus:not([data-swipe='end']) & {
outline: var(--color-accent) 1px solid;
}
&[data-swipe='move'] {
transition-property: opacity;
opacity: 0.98;
@@ -171,6 +187,10 @@ const $Root = styled(Root)`
* {
cursor: inherit;
}
& > * {
opacity: 0.5;
}
}
&[data-state='closed'],
@@ -198,69 +218,48 @@ const $Root = styled(Root)`
}
`;
const $Container = styled.div`
const $Notification = styled(Notification)`
// Params
--toast-icon-size: 1.75em;
// Rules
${popoverMixins.popover}
overflow: visible;
padding: 1rem;
box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
${$Root}:focus:not([data-swipe='end']) & {
outline: var(--color-accent) 1px solid;
}
> * {
transition: opacity 0.2s;
}
${$Root}[data-swipe='move'] & > * {
opacity: 0.5;
}
`;
const $Header = styled.header`
display: block;
`;
const $Icon = styled.div`
${layoutMixins.row}
float: left;
width: 1em;
height: 1em;
margin-right: 0.4em;
line-height: 1;
`;
const $CloseButton = styled(IconButton)`
--button-textColor: var(--color-text-0);
--button-border: none;
--button-icon-size: 0.85em;
display: none;
float: right;
/* float: right; */
margin: -0.42rem -0.42rem -0.42rem 0.42rem;
`;
const $Title = styled(Title)`
flex: 1;
// Absolute
position: absolute;
top: 0;
right: 0;
font: var(--font-base-medium);
color: var(--color-text-2);
border: solid var(--border-width) var(--color-border);
overflow: hidden;
text-overflow: ellipsis;
`;
${$Root}:hover & {
display: block;
z-index: 2;
}
const $Description = styled(Description)`
margin-top: 0.5rem;
color: var(--color-text-0);
font: var(--font-small-book);
@media ${breakpoints.mobile} {
display: block;
z-index: 2;
}
`;
const $Action = styled(Action)`
+7 -2
View File
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import { Provider, Viewport } from '@radix-ui/react-toast';
import styled from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
@@ -12,7 +13,9 @@ type StyleProps = {
className?: string;
};
export const ToastArea = ({ swipeDirection, children, className }: ElementProps & StyleProps) => (
type ToastAreaProps = ElementProps & StyleProps;
export const ToastArea = ({ swipeDirection, children, className }: ToastAreaProps) => (
<$ToastArea className={className}>
<Provider swipeDirection={swipeDirection}>
{children}
@@ -34,6 +37,7 @@ const $ToastArea = styled.aside`
> * {
display: contents;
pointer-events: none;
position: relative;
> ol {
/* display: block; */
@@ -43,6 +47,7 @@ const $ToastArea = styled.aside`
// Desired display order: latest to earliest (¯\_(ツ)_/¯)
display: flex;
flex-direction: column-reverse;
position: relative;
pointer-events: none;
+4
View File
@@ -52,15 +52,19 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.BaseButton = styled(BaseButton)`
--button-toggle-off-backgroundColor: var(--color-layer-3);
--button-toggle-off-textColor: var(--color-text-0);
--button-toggle-off-border: solid var(--border-width) var(--border-color);
--button-toggle-on-backgroundColor: var(--color-layer-1);
--button-toggle-on-textColor: var(--color-text-2);
--button-toggle-on-border: solid var(--border-width) var(--border-color);
--button-backgroundColor: var(--button-toggle-off-backgroundColor);
--button-textColor: var(--button-toggle-off-textColor);
--button-border: var(--button-toggle-off-border);
&[data-state='on'],
&[data-state='active'] {
--button-backgroundColor: var(--button-toggle-on-backgroundColor);
--button-textColor: var(--button-toggle-on-textColor);
--button-border: var(--button-toggle-on-border);
}
`;
+34 -1
View File
@@ -124,6 +124,8 @@ export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPo
export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder;
export type OrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
export const AbacusOrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
const abacusOrderStatuses = [...AbacusOrderStatus.values()] as const;
export type AbacusOrderStatuses = (typeof abacusOrderStatuses)[number];
export type SubaccountFills = Abacus.exchange.dydx.abacus.output.SubaccountFill[];
export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill;
export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment;
@@ -209,11 +211,13 @@ type ReadonlyKeysOf<T> = {
[K in keyof T]: IfEquals<{ [_ in K]: T[K] }, { readonly [_ in K]: T[K] }, K, never>;
}[keyof T];
type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
export type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
export const ORDER_SIDES: Record<KotlinIrEnumValues<typeof AbacusOrderSide>, OrderSide> = {
[AbacusOrderSide.buy.name]: OrderSide.BUY,
[AbacusOrderSide.sell.name]: OrderSide.SELL,
[AbacusOrderSide.buy.rawValue]: OrderSide.BUY,
[AbacusOrderSide.sell.rawValue]: OrderSide.SELL,
};
export const POSITION_SIDES: Record<KotlinIrEnumValues<typeof AbacusPositionSide>, PositionSide> = {
@@ -234,12 +238,25 @@ export const HISTORICAL_PNL_PERIODS: Record<
export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderStatus>, string> = {
[AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.open.rawValue]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.partiallyFilled.name]: STRING_KEYS.PARTIALLY_FILLED,
[AbacusOrderStatus.partiallyFilled.rawValue]: STRING_KEYS.PARTIALLY_FILLED,
[AbacusOrderStatus.filled.name]: STRING_KEYS.ORDER_FILLED,
[AbacusOrderStatus.filled.rawValue]: STRING_KEYS.ORDER_FILLED,
[AbacusOrderStatus.cancelled.name]: STRING_KEYS.CANCELED,
[AbacusOrderStatus.cancelled.rawValue]: STRING_KEYS.CANCELED,
[AbacusOrderStatus.canceling.name]: STRING_KEYS.CANCELING,
[AbacusOrderStatus.canceling.rawValue]: STRING_KEYS.CANCELING,
[AbacusOrderStatus.pending.name]: STRING_KEYS.PENDING,
[AbacusOrderStatus.pending.rawValue]: STRING_KEYS.PENDING,
[AbacusOrderStatus.untriggered.name]: STRING_KEYS.UNTRIGGERED,
[AbacusOrderStatus.untriggered.rawValue]: STRING_KEYS.UNTRIGGERED,
};
export const TRADE_TYPES: Record<
@@ -247,15 +264,31 @@ export const TRADE_TYPES: Record<
Nullable<TradeTypes>
> = {
[AbacusOrderType.limit.name]: TradeTypes.LIMIT,
[AbacusOrderType.limit.rawValue]: TradeTypes.LIMIT,
[AbacusOrderType.market.name]: TradeTypes.MARKET,
[AbacusOrderType.market.rawValue]: TradeTypes.MARKET,
[AbacusOrderType.stopLimit.name]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopLimit.rawValue]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopMarket.name]: TradeTypes.STOP_MARKET,
[AbacusOrderType.stopMarket.rawValue]: TradeTypes.STOP_MARKET,
[AbacusOrderType.takeProfitLimit.name]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitLimit.rawValue]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitMarket.name]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.takeProfitMarket.rawValue]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.liquidated.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.rawValue]: null,
[AbacusOrderType.trailingStop.name]: null,
[AbacusOrderType.trailingStop.rawValue]: null,
};
// Custom types involving Abacus
+2 -1
View File
@@ -10,13 +10,14 @@ export enum DialogTypes {
MobileSignIn = 'MobileSignIn',
Onboarding = 'Onboarding',
OrderDetails = 'OrderDetails',
Preferences = 'Preferences',
RateLimit = 'RateLimit',
Receive = 'Receive',
RestrictedGeo = 'RestrictedGeo',
RestrictedWallet = 'RestrictedWallet',
Trade = 'Trade',
Transfer = 'Transfer',
Withdraw = 'Withdraw',
ManageFunds = 'ManageFunds',
}
export enum TradeBoxDialogTypes {
+4
View File
@@ -13,6 +13,7 @@ export enum LocalStorageKey {
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
TransferNotifications = 'dydx.TransferNotifications',
NotificationPreferences = 'dydx.NotificationPreferences',
// UI State
LastViewedMarket = 'dydx.LastViewedMarket',
@@ -25,5 +26,8 @@ export enum LocalStorageKey {
export const LOCAL_STORAGE_VERSIONS = {
[LocalStorageKey.EvmDerivedAddresses]: 'v2',
[LocalStorageKey.NotificationPreferences]: 'v1',
[LocalStorageKey.TransferNotifications]: 'v1',
[LocalStorageKey.Notifications]: 'v1',
// TODO: version all localStorage keys
};
+8
View File
@@ -6,6 +6,8 @@ import {
LOCALE_DATA,
TOOLTIPS,
WARNINGS_STRING_KEYS,
NOTIFICATIONS_STRING_KEYS,
NOTIFICATIONS,
} from '@dydxprotocol/v4-localization';
import { type LinksConfigs } from '@/hooks/useURLConfigs';
@@ -27,6 +29,7 @@ export enum SupportedLocales {
export const EN_LOCALE_DATA = {
...LOCALE_DATA[SupportedLocales.EN],
...NOTIFICATIONS[SupportedLocales.EN],
TOOLTIPS: TOOLTIPS[SupportedLocales.EN],
};
@@ -34,8 +37,13 @@ export const STRING_KEYS = {
...APP_STRING_KEYS,
...ERRORS_STRING_KEYS,
...WARNINGS_STRING_KEYS,
...NOTIFICATIONS_STRING_KEYS,
};
export const STRING_KEY_VALUES = Object.fromEntries(
Object.values(STRING_KEYS).map((key) => [key, key])
);
export type StringKey = keyof typeof STRING_KEYS;
export type LocaleData = typeof EN_LOCALE_DATA;
+12
View File
@@ -26,3 +26,15 @@ export enum FundingDirection {
ToShort = 'ToShort',
ToLong = 'ToLong',
}
'test'
export const MARKETS_TO_DISPLAY = [
'BTC-USD',
'ETH-USD',
'LINK-USD',
'SOL-USD',
'MATIC-USD',
'ATOM-USD',
'AVAX-USD',
'APE-USD',
];
+1
View File
@@ -26,6 +26,7 @@ export type MenuItem<MenuItemValue, MenuItemTypes = string> = {
tag?: React.ReactNode;
slotAfter?: React.ReactNode;
description?: string;
slotCustomContent?: React.ReactNode;
href?: string;
onSelect?: (key: MenuItemValue) => void;
+25 -8
View File
@@ -2,8 +2,9 @@ import { StatusResponse } from '@0xsquid/sdk';
/** implemented in useNotificationTypes */
export enum NotificationType {
OrderStatusChanged = 'OrderStatusChanged',
AbacusGenerated = 'AbacusGenerated',
SquidTransfer = 'SquidTransfer',
ReleaseUpdates = 'ReleaseUpdates',
}
export enum NotificationComponentType {}
@@ -59,7 +60,7 @@ export enum NotificationStatus {
/** Toast or NotificationsMenu item interacted with or dismissed. "Seen" in NotificationsMenu. */
Seen,
/** Notification marked for deletion. "Archived" in NotificationsMenu. */
/** Notification marked for deletion. Hidden in NotificationsMenu. */
Cleared,
}
@@ -80,14 +81,20 @@ export type Notifications = Record<NotificationId, Notification<any>>;
/** Notification display data derived from app state at runtime. */
export type NotificationDisplayData = {
icon?: React.ReactNode;
title: string; // Title for Toast, Notification, and Push Notification
body?: string; // Description body for Toast, Notification, and Push Notification
title?: string;
slotTitleLeft?: React.ReactNode;
slotTitleRight?: React.ReactNode;
description?: React.ReactNode;
customContent?: React.ReactNode;
customMenuContent?: React.ReactNode;
// Overrides title/body for Notification in NotificationMenu
renderCustomBody?: ({
isToast,
notification,
}: {
isToast?: boolean;
notification: Notification;
}) => React.ReactNode; // Custom Notification
actionDescription?: string;
@@ -122,5 +129,15 @@ export type TransferNotifcation = {
fromChainId?: string;
toAmount?: number;
triggeredAt?: number;
errorCount?: number;
status?: StatusResponse;
};
/**
* @description Struct to store whether a NotificationType should be triggered
*/
export type NotificationPreferences = {
[key in NotificationType]: boolean;
} & { version: string };
export const DEFAULT_TOAST_AUTO_CLOSE_MS = 5000;
+12
View File
@@ -116,3 +116,15 @@ export enum MobilePlaceOrderSteps {
PlacingOrder = 'PlacingOrder',
Confirmation = 'Confirmation',
}
export const CLEARED_TRADE_INPUTS = {
limitPriceInput: '',
triggerPriceInput: '',
trailingPercentInput: '',
};
export const CLEARED_SIZE_INPUTS = {
amountInput: '',
usdAmountInput: '',
leverageInput: '',
};
+2
View File
@@ -22,6 +22,7 @@ import { useShouldShowFooter } from './useShouldShowFooter';
import { useSelectedNetwork } from './useSelectedNetwork';
import { useStringGetter } from './useStringGetter';
import { useSubaccount } from './useSubaccount';
import { useTradeFormInputs } from './useTradeFormInputs';
import { useURLConfigs } from './useURLConfigs';
export {
@@ -49,5 +50,6 @@ export {
useSelectedNetwork,
useStringGetter,
useSubaccount,
useTradeFormInputs,
useURLConfigs,
};
+3 -1
View File
@@ -22,6 +22,8 @@ export const useCurrentMarketId = () => {
const dispatch = useDispatch();
const selectedNetwork = useSelector(getSelectedNetwork);
const marketIds = useSelector(getMarketIds, shallowEqual);
const hasMarketIds = marketIds.length > 0;
const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({
key: LocalStorageKey.LastViewedMarket,
defaultValue: DEFAULT_MARKETID,
@@ -48,5 +50,5 @@ export const useCurrentMarketId = () => {
if (marketIds) {
abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID);
}
}, [selectedNetwork, marketIds, marketId]);
}, [selectedNetwork, hasMarketIds, marketId]);
};
+68 -47
View File
@@ -2,11 +2,11 @@ import { createContext, useContext, useCallback, useEffect, useMemo } from 'reac
import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk';
import { LocalStorageKey } from '@/constants/localStorage';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts';
import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid';
import { useLocalStorage } from './useLocalStorage';
const LocalNotificationsContext = createContext<
@@ -22,16 +22,32 @@ export const LocalNotificationsProvider = ({ ...props }) => (
export const useLocalNotifications = () => useContext(LocalNotificationsContext)!;
const TRANSFER_STATUS_FETCH_INTERVAL = 10_000;
const ERROR_COUNT_THRESHOLD = 3;
const useLocalNotificationsContext = () => {
// transfer notifications
const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{
[key: `dydx${string}`]: TransferNotifcation[];
version: string;
}>({
key: LocalStorageKey.TransferNotifications,
defaultValue: {},
defaultValue: {
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications],
},
});
// Ensure version matches, otherwise wipe
useEffect(() => {
if (
allTransferNotifications?.version !==
LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications]
) {
setAllTransferNotifications({
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications],
});
}
}, [allTransferNotifications]);
const { dydxAddress } = useAccounts();
const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : [];
@@ -54,56 +70,61 @@ const useLocalNotificationsContext = () => {
const squid = useSquid();
const { data: transferStatuses } = useQuery({
queryKey: ['getTransactionStatus', transferNotifications],
useQuery({
queryKey: 'getTransactionStatus',
queryFn: async () => {
const statuses: { [key: string]: StatusResponse } = {};
for (const {
txHash,
toChainId,
fromChainId,
triggeredAt,
status: currentStatus,
} of transferNotifications) {
try {
// skip if error is returned or if the transaction is not ongoing
if (
// @ts-ignore status.errors is not in the type definition but can be returned
currentStatus?.errors ||
currentStatus?.error ||
(currentStatus?.squidTransactionStatus &&
currentStatus?.squidTransactionStatus !== 'ongoing')
) {
continue;
}
const processTransferNotifications = async (transferNotifications: TransferNotifcation[]) => {
const newTransferNotifications = await Promise.all(
transferNotifications.map(async (transferNotification) => {
const {
txHash,
toChainId,
fromChainId,
triggeredAt,
errorCount,
status: currentStatus,
} = transferNotification;
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status;
} catch (error) {
// ignore errors for the first 120s since the status might not be available yet
if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
statuses[txHash] = error;
}
}
}
return statuses;
if (
// @ts-ignore status.errors is not in the type definition but can be returned
!currentStatus?.errors &&
!currentStatus?.error &&
(!currentStatus?.squidTransactionStatus ||
currentStatus?.squidTransactionStatus === 'ongoing')
) {
try {
const status = await squid?.getStatus({
transactionId: txHash,
toChainId,
fromChainId,
});
if (status) {
transferNotification.status = status;
}
} catch (error) {
if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
if (errorCount && errorCount > ERROR_COUNT_THRESHOLD) {
transferNotification.status = error;
} else {
transferNotification.errorCount = errorCount ? errorCount + 1 : 1;
}
}
}
}
return transferNotification;
})
);
return newTransferNotifications;
};
const newTransferNotifications = await processTransferNotifications(transferNotifications);
setTransferNotifications(newTransferNotifications);
},
refetchInterval: TRANSFER_STATUS_FETCH_INTERVAL,
});
useEffect(() => {
if (!transferStatuses) return;
const newTransferNotifications = transferNotifications.map((notification) => {
const status = transferStatuses[notification.txHash];
if (!status) return notification;
return {
...notification,
status,
};
});
setTransferNotifications(newTransferNotifications);
}, [transferStatuses]);
return {
transferNotifications,
addTransferNotification,
+11 -2
View File
@@ -1,7 +1,14 @@
import { useMemo } from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { MarketFilters, MARKET_FILTER_LABELS, type MarketData } from '@/constants/markets';
import {
MarketFilters,
MARKET_FILTER_LABELS,
type MarketData,
MARKETS_TO_DISPLAY,
} from '@/constants/markets';
import { testFlags } from '@/hooks/useTestFlags';
import { getAssets } from '@/state/assetsSelectors';
import { getPerpetualMarkets } from '@/state/perpetualsSelectors';
@@ -42,7 +49,9 @@ export const useMarketsData = (
}, [allPerpetualMarkets, allAssets]);
const filteredMarkets = useMemo(() => {
const filtered = markets.filter(filterFunctions[filter]);
const filtered = markets
.filter(filterFunctions[filter])
.filter(({ id }) => (testFlags.displayAllMarkets ? true : MARKETS_TO_DISPLAY.includes(id)));
if (searchFilter) {
return filtered.filter(
+209 -153
View File
@@ -1,125 +1,148 @@
import { useCallback, useEffect, useMemo } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
import { type ReactNode, useEffect } from 'react';
import styled from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { isEqual, groupBy } from 'lodash';
import { useNavigate } from 'react-router-dom';
import { AlertType } from '@/constants/alerts';
import { AbacusOrderStatus, ORDER_SIDES } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, StringKey } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS } from '@/constants/trade';
import { AppRoute } from '@/constants/routes';
import { DydxChainAsset } from '@/constants/wallets';
import { useSelectedNetwork } from '@/hooks';
import {
STRING_KEYS,
STRING_KEY_VALUES,
type StringGetterFunction,
type StringKey,
} from '@/constants/localization';
import {
type NotificationTypeConfig,
NotificationType,
DEFAULT_TOAST_AUTO_CLOSE_MS,
} from '@/constants/notifications';
import { useSelectedNetwork, useStringGetter } from '@/hooks';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { TransferStatusToast } from '@/views/TransferStatus';
import { TradeNotification } from '@/views/notifications/TradeNotification';
import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { getAbacusNotifications } from '@/state/notificationsSelectors';
import { getMarketIds } from '@/state/perpetualsSelectors';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { formatSeconds } from '@/lib/timeUtils';
import { useStringGetter } from './useStringGetter';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
const parseStringParamsForNotification = ({
stringGetter,
value,
}: {
stringGetter: StringGetterFunction;
value: unknown;
}): ReactNode => {
if (STRING_KEY_VALUES[value as StringKey]) {
return stringGetter({ key: value as StringKey });
}
export const notificationTypes = [
return value as ReactNode;
};
export const notificationTypes: NotificationTypeConfig[] = [
{
type: NotificationType.OrderStatusChanged,
useTrigger: ({ trigger, lastUpdated }) => {
type: NotificationType.AbacusGenerated,
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
const fills = useSelector(getSubaccountFills, shallowEqual) || [];
const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
const orderIds = useMemo(
() => [...Object.keys(ordersByOrderId), ...Object.keys(fillsByOrderId)],
[orders, fills]
);
const abacusNotifications = useSelector(getAbacusNotifications, isEqual);
useEffect(() => {
for (const orderId of orderIds) {
const fills = fillsByOrderId[orderId];
for (const abacusNotif of abacusNotifications) {
const [abacusNotificationType = '', id = ''] = abacusNotif.id.split(':');
const parsedData = abacusNotif.data ? JSON.parse(abacusNotif.data) : {};
const order =
ordersByOrderId[orderId] ??
(fills?.length
? {
...fills[fills.length - 1],
id: orderId,
createdAtMilliseconds: Math.max(
...fills.map((fill) => fill.createdAtMilliseconds)
const params = Object.fromEntries(
Object.entries(parsedData).map(([key, value]) => {
return [key, parseStringParamsForNotification({ stringGetter, value })];
})
);
switch (abacusNotificationType) {
case 'order': {
trigger(
abacusNotif.id,
{
icon: abacusNotif.image && <$Icon src={abacusNotif.image} alt="" />,
title: stringGetter({ key: abacusNotif.title }),
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
toastSensitivity: 'foreground',
renderCustomBody: ({ isToast, notification }) => (
<TradeNotification
isToast={isToast}
data={parsedData}
notification={notification}
/>
),
status: AbacusOrderStatus.filled,
}
: undefined);
if (order)
trigger(
order.id,
{
icon: (
<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />
),
title: `${stringGetter({
key: order.resources.typeStringKey as StringKey,
})} ${
order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0
? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
: stringGetter({ key: order.resources.statusStringKey as StringKey })
}`,
description: `${stringGetter({
key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
})} ${order.size} ${order.marketId} @ $${order.price}`,
actionDescription: 'View Order',
actionAltText: 'View this order in the Orders tab or the Notifications menu.',
toastSensitivity:
order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
toastDuration: 5000,
},
[order.status.name, order.size],
!order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated
);
},
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data],
true
);
break;
}
default:
trigger(
abacusNotif.id,
{
icon: abacusNotif.image && <$Icon src={abacusNotif.image} alt="" />,
title: stringGetter({ key: abacusNotif.title }),
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
toastSensitivity: 'foreground',
},
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data]
);
break;
}
}
}, [orderIds]);
}, [abacusNotifications, stringGetter]);
},
useNotificationAction: () => {
const dispatch = useDispatch();
const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
const ordersById = groupBy(orders, 'id');
const fills = useSelector(getSubaccountFills, shallowEqual) || [];
const fillsById = groupBy(fills, 'id');
const marketIds = useSelector(getMarketIds, shallowEqual);
const navigate = useNavigate();
// useNotificationAction: () => {
// const dispatch = useDispatch();
// const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
// const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
return (notificationId: string) => {
const [abacusNotificationType = '', id = ''] = notificationId.split(':');
// const fills = useSelector(getSubaccountFills, shallowEqual) || [];
// const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
// return (id) => {
// if (ordersByOrderId[id]) {
// dispatch(
// openDialog({
// type: DialogTypes.OrderDetails,
// dialogProps: { orderId: id },
// })
// );
// } else if (fillsByOrderId[id]) {
// const fillId = fillsByOrderId[id][0].id;
// dispatch(
// openDialog({
// type: DialogTypes.FillDetails,
// dialogProps: { fillId },
// })
// );
// }
// };
// },
} as NotificationTypeConfig<string, [string, number]>,
if (ordersById[id]) {
dispatch(
openDialog({
type: DialogTypes.OrderDetails,
dialogProps: { orderId: id },
})
);
} else if (fillsById[id]) {
dispatch(
openDialog({
type: DialogTypes.FillDetails,
dialogProps: { fillId: id },
})
);
} else if (marketIds.includes(id)) {
navigate(`${AppRoute.Trade}/${id}`, {
replace: true,
});
}
};
},
},
{
type: NotificationType.SquidTransfer,
useTrigger: ({ trigger }) => {
@@ -127,81 +150,114 @@ export const notificationTypes = [
const { transferNotifications } = useLocalNotifications();
const { selectedNetwork } = useSelectedNetwork();
const getTitleStringKey = useCallback((type: 'deposit' | 'withdrawal', finished: boolean) => {
if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS;
if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT;
if (type === 'withdrawal' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS;
return STRING_KEYS.WITHDRAW;
}, []);
useEffect(() => {
for (const transfer of transferNotifications) {
const { fromChainId, status, txHash, toAmount } = transfer;
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId ? 'withdrawal' : 'deposit';
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const icon = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />;
// TODO: confirm with design what the description should be
const description = (
<div>
<Styled.TransferText>
{type === 'deposit' ? 'Deposit of ' : 'Withdraw of '}
<Output type={OutputType.Fiat} value={toAmount} />
</Styled.TransferText>
const type =
fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId
? 'withdrawal'
: 'deposit';
{error && (
<Styled.ErrorMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE:
error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
})}
</Styled.ErrorMessage>
)}
</div>
);
const title = stringGetter({
key: {
deposit: isFinished ? STRING_KEYS.DEPOSIT : STRING_KEYS.DEPOSIT_IN_PROGRESS,
withdrawal: isFinished ? STRING_KEYS.WITHDRAW : STRING_KEYS.WITHDRAW_IN_PROGRESS,
}[type],
});
const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0;
const estimatedDuration = formatSeconds(Math.max(toChainEta, 0));
const body = stringGetter({
key: STRING_KEYS.DEPOSIT_STATUS,
params: {
AMOUNT_USD: `${toAmount} ${DydxChainAsset.USDC.toUpperCase()}`,
ESTIMATED_DURATION: estimatedDuration,
},
});
trigger(
txHash,
{
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }),
description: description,
customContent: (
<TransferStatusToast
icon,
title,
body,
renderCustomBody: ({ isToast, notification }) => (
<TransferStatusNotification
isToast={isToast}
slotIcon={icon}
slotTitle={title}
transfer={transfer}
type={type}
toAmount={transfer.toAmount}
triggeredAt={transfer.triggeredAt}
status={transfer.status}
notification={notification}
/>
),
customMenuContent: !finished && (
<div>
{description}
<TransferStatusSteps status={transfer.status} type={type} />
</div>
),
toastSensitivity: 'foreground',
},
[]
[isFinished]
);
}
}, [transferNotifications]);
}, [transferNotifications, stringGetter]);
},
useNotificationAction: () => {
return () => {};
},
},
] satisfies NotificationTypeConfig[];
{
type: NotificationType.ReleaseUpdates,
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
const Styled: Record<string, AnyStyledComponent> = {};
useEffect(() => {
trigger(
'rewards-and-full-trading-live',
{
icon: <AssetIcon symbol="DYDX" />,
title: stringGetter({ key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.TITLE' }),
body: stringGetter({
key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.BODY',
params: {
BLOGPOST: (
<$Link
href="https://www.dydxopsdao.com/blog/deep-dive-full-trading"
target="_blank"
rel="noopener noreferrer"
>
{stringGetter({ key: STRING_KEYS.HERE })}
</$Link>
),
// todo: update localization to flip the two
DOS_BLOGPOST: (
<$Link
href="https://dydx.exchange/blog/v4-full-trading"
target="_blank"
rel="noopener noreferrer"
>
{stringGetter({ key: STRING_KEYS.HERE })}
</$Link>
),
},
}),
toastSensitivity: 'foreground',
},
[]
);
}, [stringGetter]);
},
useNotificationAction: () => {
return () => {};
},
},
];
Styled.TransferText = styled.span`
display: inline-flex;
align-items: center;
gap: 0.5ch;
const $Icon = styled.img`
height: 1.5rem;
width: 1.5rem;
`;
Styled.ErrorMessage = styled.div`
max-width: 13rem;
const $Link = styled.a`
--link-color: var(--color-text-2);
`;
+82 -36
View File
@@ -1,11 +1,13 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { LocalStorageKey } from '@/constants/localStorage';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import {
type Notification,
type NotificationDisplayData,
type NotificationPreferences,
type Notifications,
NotificationStatus,
NotificationType,
} from '@/constants/notifications';
import { useLocalStorage } from './useLocalStorage';
@@ -13,8 +15,10 @@ import { notificationTypes } from './useNotificationTypes';
import { renderSvgToDataUrl } from '../lib/renderSvgToDataUrl';
const NotificationsContext = createContext<ReturnType<typeof useNotificationsContext> | undefined>(
undefined
type NotificationsContextType = ReturnType<typeof useNotificationsContext>;
const NotificationsContext = createContext<NotificationsContextType>(
{} as NotificationsContextType
);
NotificationsContext.displayName = 'Notifications';
@@ -27,6 +31,7 @@ export const useNotifications = () => useContext(NotificationsContext)!;
const useNotificationsContext = () => {
// Local storage
// const [notifications, setNotifications] = useState<Notifications>({});
const [notifications, setNotifications] = useLocalStorage<Notifications>({
key: LocalStorageKey.Notifications,
defaultValue: {},
@@ -37,12 +42,25 @@ const useNotificationsContext = () => {
defaultValue: Date.now(),
});
const [notificationPreferences, setNotificationPreferences] =
useLocalStorage<NotificationPreferences>({
key: LocalStorageKey.NotificationPreferences,
defaultValue: {
[NotificationType.AbacusGenerated]: true,
[NotificationType.SquidTransfer]: true,
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
},
});
useEffect(() => {
setNotificationsLastUpdated(Date.now());
}, [notifications]);
const getKey = <T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
`${notification.type}/${notification.id}`;
const getKey = useCallback(
<T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
`${notification.type}/${notification.id}`,
[]
);
// Display data
const [notificationsDisplayData, setNotificationsDisplayData] = useState(
@@ -54,29 +72,47 @@ const useNotificationsContext = () => {
[notificationsDisplayData]
);
// Check for version changes
useEffect(() => {
if (
notificationPreferences.version !==
LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences]
) {
setNotificationPreferences({
[NotificationType.AbacusGenerated]: true,
[NotificationType.SquidTransfer]: true,
[NotificationType.ReleaseUpdates]: true,
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
});
}
}, []);
// Status changes
const updateStatus = useCallback(
(notification: Notification, status: NotificationStatus) => {
notification.status = status;
notification.timestamps[notification.status] = Date.now();
setNotifications({ ...notifications });
setNotifications({ ...notifications, [getKey(notification)]: notification });
},
[notifications]
[notifications, getKey]
);
const { markUnseen, markSeen, markCleared } = useMemo(
() => ({
markUnseen: (notification: Notification) => {
if (notification.status < NotificationStatus.Unseen)
if (notification.status < NotificationStatus.Unseen) {
updateStatus(notification, NotificationStatus.Unseen);
}
},
markSeen: (notification: Notification) => {
if (notification.status < NotificationStatus.Seen)
if (notification.status < NotificationStatus.Seen) {
updateStatus(notification, NotificationStatus.Seen);
}
},
markCleared: (notification: Notification) => {
if (notification.status < NotificationStatus.Cleared)
if (notification.status < NotificationStatus.Cleared) {
updateStatus(notification, NotificationStatus.Cleared);
}
},
}),
[updateStatus]
@@ -97,33 +133,37 @@ const useNotificationsContext = () => {
const notification = notifications[key];
// New unique key - create new notification
if (!notification) {
const notification = (notifications[key] = {
id,
type,
timestamps: {},
updateKey,
} as Notification);
// Filter out notifications that are not enabled
if (notificationPreferences[type] !== false) {
// New unique key - create new notification
if (!notification) {
const notification = (notifications[key] = {
id,
type,
timestamps: {},
updateKey,
} as Notification);
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
} else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
// updateKey changed - update existing notification
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
}
const notification = notifications[key];
// updateKey changed - update existing notification
else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
const notification = notifications[key];
notification.updateKey = updateKey;
updateStatus(notification, NotificationStatus.Updated);
notification.updateKey = updateKey;
updateStatus(notification, NotificationStatus.Updated);
}
} else {
// Notification is disabled - remove it
delete notifications[key];
}
notificationsDisplayData[key] = displayData;
setNotificationsDisplayData({ ...notificationsDisplayData });
},
[notifications, updateStatus]
[notifications, updateStatus, notificationPreferences[type]]
),
lastUpdated: notificationsLastUpdated,
@@ -136,10 +176,8 @@ const useNotificationsContext = () => {
)
);
const onNotificationAction = useCallback(
async (notification: Notification) => await actions[notification.type]?.(notification.id),
[actions]
);
const onNotificationAction = async (notification: Notification) =>
await actions[notification.type]?.(notification.id);
// Push notifications
const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({
@@ -186,7 +224,7 @@ const useNotificationsContext = () => {
renotify: true,
tag: getKey(notification),
data: notification,
body: displayData.description,
description: displayData.body,
icon: iconUrl ?? '/favicon.svg',
badge: iconUrl ?? '/favicon.svg',
image: iconUrl ?? '/favicon.svg',
@@ -208,7 +246,7 @@ const useNotificationsContext = () => {
setPushNotificationsLastUpdated(Date.now());
})();
}, [hasEnabledPush, notifications, onNotificationAction, markSeen]);
}, [hasEnabledPush, notifications, markSeen]);
// Menu state
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -239,5 +277,13 @@ const useNotificationsContext = () => {
// Menu state
isMenuOpen,
setIsMenuOpen,
// Notification Preferences
notificationPreferences,
setNotificationPreferences,
getNotificationPreferenceForType: useCallback(
(type: NotificationType) => notificationPreferences[type],
[notificationPreferences]
),
};
};
+7
View File
@@ -16,6 +16,13 @@ class TestFlags {
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
/**
* @description We are temporarily displaying a subset of ACTIVE markets, this flag will allow you to see all ACTIVE markets
*/
get displayAllMarkets() {
return !!this.queryParams.displayAllMarkets;
}
}
export const testFlags = new TestFlags();
+33
View File
@@ -0,0 +1,33 @@
import { getTradeFormInputs } from '@/state/inputsSelectors';
import { useEffect } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { TradeInputField } from '@/constants/abacus';
import abacusStateManager from '@/lib/abacus';
export const useTradeFormInputs = () => {
const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual);
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
useEffect(() => {
const floatValue = parseFloat(triggerPriceInput);
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.triggerPrice,
});
}, [triggerPriceInput]);
useEffect(() => {
const floatValue = parseFloat(limitPriceInput);
abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice });
}, [limitPriceInput]);
useEffect(() => {
const floatValue = parseFloat(trailingPercentInput);
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.trailingPercent,
});
}, [trailingPercentInput]);
};
+6
View File
@@ -0,0 +1,6 @@
<svg width="27" height="28" viewBox="0 0 27 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Vector" d="M9.5 15L12.5 18L17.5 11M25.5 14C25.5 15.5759 25.1896 17.1363 24.5866 18.5922C23.9835 20.0481 23.0996 21.371 21.9853 22.4853C20.871 23.5996 19.5481 24.4835 18.0922 25.0866C16.6363 25.6896 15.0759 26 13.5 26C11.9241 26 10.3637 25.6896 8.9078 25.0866C7.45189 24.4835 6.12902 23.5996 5.01472 22.4853C3.90042 21.371 3.0165 20.0481 2.41345 18.5922C1.81039 17.1363 1.5 15.5759 1.5 14C1.5 10.8174 2.76428 7.76516 5.01472 5.51472C7.26516 3.26428 10.3174 2 13.5 2C16.6826 2 19.7348 3.26428 21.9853 5.51472C24.2357 7.76516 25.5 10.8174 25.5 14Z" stroke="#3ED9A4" stroke-opacity="0.5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M9.5 15L12.5 18L17.5 11" stroke="#3ED9A4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 914 B

+1 -1
View File
@@ -1,4 +1,4 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="3" stroke-linecap="round"
<path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 235 B

+1
View File
@@ -10,6 +10,7 @@ export { default as CautionCircleIcon } from './caution-circle.svg';
export { default as CautionCircleStrokeIcon } from './caution-circle-stroke.svg';
export { default as ChatIcon } from './chat-bubble.svg';
export { default as CheckIcon } from './check.svg';
export { default as CheckCircleIcon } from './check-circle.svg';
export { default as ChevronLeftIcon } from './chevron-left.svg';
export { default as ChevronRightIcon } from './chevron-right.svg';
export { default as ClockIcon } from './clock.svg';
+12 -4
View File
@@ -1,5 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g fill="#FFFFFF">
<path d="M461.2 128H80c-8.84 0-16-7.16-16-16s7.16-16 16-16h384c8.84 0 16-7.16 16-16 0-26.51-21.49-48-48-48H64C28.65 32 0 60.65 0 96v320c0 35.35 28.65 64 64 64h397.2c28.02 0 50.8-21.53 50.8-48V176c0-26.47-22.78-48-50.8-48zM416 336c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32z"/>
</g>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Wallet" clip-path="url(#clip0_14122_34311)">
<g id="Group 151">
<path id="Exclude" fill-rule="evenodd" clip-rule="evenodd" d="M11.4979 10.9976C11.1764 11.3193 10.7404 11.5 10.2857 11.5L1.71429 11.5C1.25963 11.5 0.823594 11.3193 0.502102 10.9976C0.180612 10.6758 -4.76837e-07 10.2395 -4.76837e-07 9.78455L-4.76837e-07 2.06502C-4.76837e-07 2.51999 0.180612 2.95632 0.502102 3.27803C0.823594 3.59974 1.25963 3.78047 1.71429 3.78047L10.2857 3.78047C10.7404 3.78047 11.1764 3.96121 11.4979 4.28292C11.8194 4.60462 12 5.04096 12 5.49592V9.78455C12 10.2395 11.8194 10.6758 11.4979 10.9976ZM9.64294 8.7126C10.2347 8.7126 10.7144 8.23258 10.7144 7.64044C10.7144 7.0483 10.2347 6.56828 9.64294 6.56828C9.05121 6.56828 8.57151 7.0483 8.57151 7.64044C8.57151 8.23258 9.05121 8.7126 9.64294 8.7126Z" fill="#C8C7D8"/>
<path id="Vector" d="M8.57031 2.92186V2.06413C8.57031 1.60917 8.3897 1.17284 8.06821 0.851128C7.74672 0.529419 7.31068 0.348684 6.85603 0.348684L2.14174 0.348684C1.80075 0.348684 1.47372 0.484235 1.2326 0.725517C0.991486 0.966799 0.856028 1.29405 0.856028 1.63527C0.856028 1.9765 0.991486 2.30374 1.2326 2.54503C1.47372 2.78631 1.80075 2.92186 2.14174 2.92186L8.57031 2.92186Z" fill="#C8C7D8"/>
</g>
</g>
<defs>
<clipPath id="clip0_14122_34311">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 1.4 KiB

+4 -2
View File
@@ -15,13 +15,14 @@ import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog';
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
import { PreferencesDialog } from '@/views/dialogs/PreferencesDialog';
import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog';
import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog';
import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog';
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
import { ManageFundsDialog } from '@/views/dialogs/ManageFundsDialog';
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
@@ -56,12 +57,13 @@ export const DialogManager = () => {
[DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />,
[DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />,
[DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />,
[DialogTypes.Preferences]: <PreferencesDialog {...modalProps} />,
[DialogTypes.RateLimit]: <RateLimitDialog {...modalProps} />,
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
[DialogTypes.RestrictedGeo]: <RestrictedGeoDialog {...modalProps} />,
[DialogTypes.RestrictedWallet]: <RestrictedWalletDialog {...modalProps} />,
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
[DialogTypes.ManageFunds]: <ManageFundsDialog {...modalProps} />,
}[type];
};
+4 -2
View File
@@ -1,7 +1,7 @@
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { AbacusApiStatus } from '@/constants/abacus';
import { ButtonSize } from '@/constants/buttons';
import { ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP, isDev } from '@/constants/networks';
@@ -55,10 +55,12 @@ export const FooterDesktop = () => {
}
>
<Styled.FooterButton
type={statusPage ? ButtonType.Link : ButtonType.Button}
slotLeft={<Styled.StatusDot exchangeStatus={exchangeStatus} />}
slotRight={statusPage && <LinkOutIcon />}
size={ButtonSize.XSmall}
state={{ isDisabled: !statusPage }}
href={statusPage}
>
{label}
</Styled.FooterButton>
@@ -132,7 +134,7 @@ Styled.StatusDot = styled.div<{ exchangeStatus: ExchangeStatus }>`
Styled.FooterButton = styled(Button)`
--button-height: 1.5rem;
--button-radius: 0.25rem;
--button-backgroundColor: var(--color-layer-2);
--button-backgroundColor: transparent;
--button-border: none;
--button-textColor: var(--color-text-0);
+29 -13
View File
@@ -1,7 +1,6 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { DEFAULT_MARKETID } from '@/constants/markets';
@@ -11,8 +10,8 @@ import { useShouldShowFooter, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { NavigationMenu } from '@/components/NavigationMenu';
import { Icon } from '@/components/Icon';
import { BellIcon, MarketsIcon, PlayIcon, PortfolioIcon, ProfileIcon, TradeIcon } from '@/icons';
import { Icon, IconName } from '@/components/Icon';
import { BellIcon, MarketsIcon, PortfolioIcon, ProfileIcon } from '@/icons';
import { IconButton } from '@/components/IconButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
@@ -42,11 +41,9 @@ export const FooterMobile = () => {
value: 'trade',
label: stringGetter({ key: STRING_KEYS.TRADE }),
slotBefore: (
<Styled.IconButton
iconComponent={TradeIcon}
size={ButtonSize.Large}
action={ButtonAction.Primary}
/>
<Styled.StartIcon>
<Icon iconName={IconName.Trade} />
</Styled.StartIcon>
),
href: `${AppRoute.Trade}/${marketId ?? DEFAULT_MARKETID}`,
}
@@ -54,11 +51,9 @@ export const FooterMobile = () => {
value: 'onboarding',
label: stringGetter({ key: STRING_KEYS.ONBOARDING }),
slotBefore: (
<Styled.IconButton
iconComponent={PlayIcon}
size={ButtonSize.Large}
action={ButtonAction.Primary}
/>
<Styled.StartIcon>
<Icon iconName={IconName.Play} />
</Styled.StartIcon>
),
onClick: () => dispatch(openDialog({ type: DialogTypes.Onboarding })),
},
@@ -168,3 +163,24 @@ Styled.IconButton = styled(IconButton)`
Styled.Icon = styled(Icon)`
font-size: 1.5rem;
`;
Styled.StartIcon = styled.div`
display: inline-flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-top: -0.25rem;
width: 3.5rem;
height: 3.5rem;
color: var(--color-text-2);
background-color: var(--color-accent);
border: solid var(--border-width) var(--color-border-white);
border-radius: 50%;
svg {
width: 1.5rem;
height: 1.5rem;
}
`;
+130 -40
View File
@@ -1,20 +1,27 @@
import { NotificationStatus } from '@/constants/notifications';
import { ButtonSize } from '@/constants/buttons';
import { useMemo, useState } from 'react';
import styled, { css } from 'styled-components';
import { NotificationStatus } from '@/constants/notifications';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { useNotifications } from '@/hooks/useNotifications';
import { useBreakpoints } from '@/hooks/useBreakpoints';
import { ChevronLeftIcon } from '@/icons';
import { breakpoints } from '@/styles';
import { ToastArea } from '@/components/ToastArea';
import { Toast } from '@/components/Toast';
import { Button } from '@/components/Button';
import styled from 'styled-components';
import { Toast } from '@/components/Toast';
import { ToastArea } from '@/components/ToastArea';
import { ToggleButton } from '@/components/ToggleButton';
type StyleProps = {
className?: string;
};
const MAX_TOASTS = 10;
export const NotificationsToastArea = ({ className }: StyleProps) => {
const [shouldStackNotifications, setshouldStackNotifications] = useState(true);
const {
notifications,
getKey,
@@ -26,10 +33,9 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
} = useNotifications();
const { isMobile } = useBreakpoints();
return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{Object.values(notifications)
const notificationMap = useMemo(() => {
return (
Object.values(notifications)
// Sort by time of first trigger
.sort(
(n1, n2) =>
@@ -41,45 +47,129 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
key: getKey(notification),
displayData: getDisplayData(notification),
}))
.filter(({ displayData }) => displayData)
.map(({ notification, key, displayData }) => (
<Toast
key={key}
isOpen={notification.status < NotificationStatus.Unseen}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.description}
slotCustomContent={displayData.customContent}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
</Button>
}
actionDescription={displayData.actionDescription}
actionAltText={displayData.actionAltText}
duration={isMenuOpen ? Infinity : displayData.toastDuration}
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen, isClosedFromTimeout) => {
if (!isOpen)
if (isClosedFromTimeout)
// Toast timer expired without user interaction
markUnseen(notification);
// Toast interacted with or dismissed
else markSeen(notification);
}}
lastUpdated={notification.timestamps[notification.status]}
/>
))}
.filter(
({ displayData, notification }) =>
displayData && notification.status < NotificationStatus.Unseen
)
.slice(-MAX_TOASTS)
);
}, [notifications, getKey, getDisplayData]);
if (isMenuOpen) return null;
const hasMultipleToasts = notificationMap.length > 1;
return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{hasMultipleToasts && (
<StyledToggleButton
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
isPressed={shouldStackNotifications}
onPressedChange={() => setshouldStackNotifications(!shouldStackNotifications)}
>
<ChevronLeftIcon />
</StyledToggleButton>
)}
{notificationMap.map(({ notification, key, displayData }, idx) => (
<StyledToast
key={key}
isStacked={shouldStackNotifications}
isOpen={notification.status < NotificationStatus.Unseen}
layer={notificationMap.length - 1 - idx}
notification={notification}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotTitleLeft={displayData.slotTitleLeft}
slotTitleRight={displayData.slotTitleRight}
slotDescription={displayData.body}
slotCustomContent={displayData.renderCustomBody?.({ isToast: true, notification })}
onClick={() => onNotificationAction(notification)}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
</Button>
}
actionDescription={displayData.actionDescription}
actionAltText={displayData.actionAltText}
duration={displayData.toastDuration ?? Infinity}
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen: boolean, isClosedFromTimeout?: boolean) => {
if (!isOpen)
if (isClosedFromTimeout)
// Toast timer expired without user interaction
markUnseen(notification);
// Toast interacted with or dismissed
else markSeen(notification);
}}
lastUpdated={notification.timestamps[notification.status]}
/>
))}
</StyledToastArea>
);
};
const StyledToastArea = styled(ToastArea)`
position: absolute;
width: min(16rem, 100%);
width: min(17.5rem, 100%);
inset: 0 0 0 auto;
padding: 0.75rem 0.75rem 0.75rem 0;
mask-image: linear-gradient(to left, transparent, white 0.5rem);
@media ${breakpoints.mobile} {
width: 100%;
position: fixed;
}
`;
const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>`
// Stacked toast
${({ isStacked, layer }) =>
isStacked
? css`
position: absolute;
left: 0;
top: calc(${layer} * 2px);
right: calc(${layer} * -2px);
`
: css`
margin-bottom: 0.75rem;
`}
`;
const StyledToggleButton = styled(ToggleButton)<{ isPressed: boolean }>`
z-index: 2;
pointer-events: auto;
display: none;
position: absolute;
top: 4px;
left: calc(50% - 0.75rem);
--button-width: 2rem;
--button-height: 1rem;
${StyledToastArea}:hover & {
display: flex;
}
> svg {
width: 4px;
margin-top: 1px;
transform: rotate(-90deg);
${({ isPressed }) =>
!isPressed &&
css`
transform: rotate(90deg);
`}
}
@media ${breakpoints.mobile} {
display: flex;
left: calc(50% - 2rem);
--button-width: 4rem;
--button-height: 2rem;
}
`;
+12 -7
View File
@@ -1,6 +1,7 @@
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import { encodeJson } from '@dydxprotocol/v4-client-js';
import {
CompositeClient,
@@ -195,8 +196,9 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
throw new StatefulOrderError('Stateful order has failed to commit.', tx);
}
const parsedTx = this.parseToPrimitives(tx);
const hash = parsedTx?.hash;
const encodedTx = encodeJson(tx);
const parsedTx = JSON.parse(encodedTx);
const hash = parsedTx.hash.toUpperCase();
if (isTestnet) {
console.log(
@@ -206,7 +208,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
);
} else console.log(`txHash: ${hash}`);
return JSON.stringify(parsedTx);
return encodedTx;
} catch (error) {
if (error?.name !== 'BroadcastError') {
log('DydxChainTransactions/placeOrderTransaction', error);
@@ -236,9 +238,14 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
goodTilBlockTime || undefined
);
const parsedTx = this.parseToPrimitives(tx);
const encodedTx = encodeJson(tx);
return JSON.stringify(parsedTx);
if (import.meta.env.MODE === 'development') {
const parsedTx = JSON.parse(encodedTx);
console.log(parsedTx, parsedTx.hash.toUpperCase());
}
return encodedTx;
} catch (error) {
log('DydxChainTransactions/cancelOrderTransaction', error);
@@ -434,8 +441,6 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parseDelegations = this.parseToPrimitives(delegations);
callback(JSON.stringify(parseDelegations));
break;
// Do not implement Transfers (yet)
case QueryType.Transfers:
default:
break;
}
+8 -3
View File
@@ -27,11 +27,11 @@ import {
import { DEFAULT_MARKETID } from '@/constants/markets';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks';
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
import type { RootStore } from '@/state/_store';
import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { setTradeFormInputs } from '@/state/inputs';
import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors';
import AbacusRest from './rest';
import AbacusAnalytics from './analytics';
@@ -43,6 +43,7 @@ import AbacusFormatter from './formatter';
import AbacusThreading from './threading';
import AbacusFileSystem from './filesystem';
import { LocaleSeparators } from '../numbers';
class AbacusStateManager {
private store: RootStore | undefined;
private currentMarket: string | undefined;
@@ -132,6 +133,8 @@ class AbacusStateManager {
this.setTradeValue({ value: null, field: TradeInputField.limitPrice });
}
this.store?.dispatch(setTradeFormInputs(CLEARED_TRADE_INPUTS));
if (shouldResetSize) {
this.setTradeValue({ value: null, field: TradeInputField.size });
this.setTradeValue({ value: null, field: TradeInputField.usdcSize });
@@ -139,6 +142,8 @@ class AbacusStateManager {
if (needsLeverage) {
this.setTradeValue({ value: null, field: TradeInputField.leverage });
}
this.store?.dispatch(setTradeFormInputs(CLEARED_SIZE_INPUTS));
}
};
+2
View File
@@ -35,6 +35,7 @@ import { setApiState } from '@/state/app';
import { setAssets } from '@/state/assets';
import { setConfigs } from '@/state/configs';
import { setInputs } from '@/state/inputs';
import { updateNotifications } from '@/state/notifications';
import { setHistoricalFundings, setLiveTrades, setMarkets, setOrderbook } from '@/state/perpetuals';
import { isTruthy } from '../isTruthy';
@@ -50,6 +51,7 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
}
notificationsChanged(notifications: kollections.List<AbacusNotification>): void {
this.store?.dispatch(updateNotifications(notifications.toArray()));
return;
}
+100 -34
View File
@@ -2,12 +2,16 @@ import styled, { AnyStyledComponent, css } from 'styled-components';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useEnsName } from 'wagmi';
import { useNavigate } from 'react-router-dom';
import { ButtonSize } from '@/constants/buttons';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { layoutMixins } from '@/styles/layoutMixins';
import { Details } from '@/components/Details';
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { IconName } from '@/components/Icon';
import { Icon, IconName } from '@/components/Icon';
import { IconButton, type IconButtonProps } from '@/components/IconButton';
import { Panel } from '@/components/Panel';
import { Toolbar } from '@/components/Toolbar';
@@ -16,22 +20,22 @@ import { OnboardingState } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { AppRoute, PortfolioRoute, HistoryRoute } from '@/constants/routes';
import { wallets, WalletType } from '@/constants/wallets';
import { wallets } from '@/constants/wallets';
import { useAccounts, useStringGetter, useTokenConfigs } from '@/hooks';
import { getOnboardingState } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import abacusStateManager from '@/lib/abacus';
import { isTruthy } from '@/lib/isTruthy';
import { truncateAddress } from '@/lib/wallet';
import { layoutMixins } from '@/styles/layoutMixins';
const ENS_CHAIN_ID = 1; // Ethereum
const Profile = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const navigate = useNavigate();
const onboardingState = useSelector(getOnboardingState);
const isConnected = onboardingState !== OnboardingState.Disconnected;
@@ -46,16 +50,50 @@ const Profile = () => {
const actions = [
{
key: 'settings',
label: stringGetter({ key: STRING_KEYS.SETTINGS }),
icon: { iconName: IconName.Gear },
href: AppRoute.Settings,
key: 'deposit',
label: stringGetter({ key: STRING_KEYS.DEPOSIT }),
icon: { iconName: IconName.Deposit },
onClick: () => {
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.deposit.rawValue,
},
})
);
},
},
{
key: 'withdraw',
label: stringGetter({ key: STRING_KEYS.WITHDRAW }),
icon: { iconName: IconName.Withdraw },
onClick: () => {
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.withdrawal.rawValue,
},
})
);
},
},
{
key: 'transfer',
label: stringGetter({ key: STRING_KEYS.TRANSFER }),
icon: { iconName: IconName.Send },
onClick: () => {
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.transferOut.rawValue,
},
})
);
},
},
// {
// key: 'tutorials',
// label: stringGetter({ key: STRING_KEYS.TUTORIALS }),
// icon: { iconName: IconName.Comment },
// },
isConnected
? {
key: 'sign-out',
@@ -66,9 +104,9 @@ const Profile = () => {
},
}
: {
key: 'wallet',
label: stringGetter({ key: STRING_KEYS.WALLET }),
icon: { iconComponent: wallets[walletType || WalletType.OtherWallet].icon },
key: 'connect',
label: stringGetter({ key: STRING_KEYS.CONNECT }),
icon: { iconName: IconName.Transfer },
onClick: () => {
dispatch(openDialog({ type: DialogTypes.Onboarding }));
},
@@ -104,12 +142,7 @@ const Profile = () => {
{actions.map(({ key, label, href, icon, onClick }) => {
const action = (
<>
<Styled.ActionButton
{...icon}
size={ButtonSize.Large}
onClick={onClick}
isCloseIcon={icon.iconName === IconName.Close}
/>
<Styled.ActionButton {...icon} size={ButtonSize.Large} onClick={onClick} />
<span>{label}</span>
</>
);
@@ -122,6 +155,26 @@ const Profile = () => {
);
})}
</Styled.Actions>
<Styled.EqualGrid>
<Styled.PanelButton
slotHeader={
<Styled.InlineRow>
<Icon iconName={IconName.Gear} />
{stringGetter({ key: STRING_KEYS.SETTINGS })}
</Styled.InlineRow>
}
onClick={() => navigate(AppRoute.Settings)}
/>
<Styled.PanelButton
slotHeader={
<Styled.InlineRow>
<Icon iconName={IconName.HelpCircle} />
{stringGetter({ key: STRING_KEYS.HELP })}
</Styled.InlineRow>
}
onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))}
/>
</Styled.EqualGrid>
<Styled.EqualGrid>
<Panel
@@ -180,7 +233,7 @@ Styled.MobileProfileLayout = styled.div`
${layoutMixins.contentContainerPage}
gap: 1rem;
padding: 1.25rem;
padding: 1.25rem 0.9rem;
`;
Styled.Header = styled.header`
@@ -245,19 +298,21 @@ Styled.Actions = styled(Toolbar)`
}
`;
Styled.ActionButton = styled(IconButton)<{ isCloseIcon?: boolean }>`
Styled.ActionButton = styled(IconButton)<{ iconName?: IconName }>`
margin-bottom: 0.5rem;
${({ isCloseIcon }) =>
isCloseIcon &&
css`
--button-textColor: var(--color-negative);
svg {
width: 0.875em;
height: 0.875em;
}
`}
${({ iconName }) =>
iconName === IconName.Close
? css`
--button-textColor: var(--color-negative);
--button-icon-size: 0.75em;
`
: iconName === IconName.Transfer &&
css`
--button-icon-size: 1.25em;
--button-textColor: var(--color-text-2);
--button-backgroundColor: var(--color-accent);
`}
`;
Styled.EqualGrid = styled.div`
@@ -306,3 +361,14 @@ Styled.TablePanel = styled(Panel)`
}
}
`;
Styled.InlineRow = styled.div`
${layoutMixins.inlineRow}
padding: 1rem;
gap: 0.5rem;
`;
Styled.PanelButton = styled(Panel)`
--panel-paddingY: 0
--panel-paddingX:0;
`;
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -39,7 +39,7 @@ export const DYDXBalancePanel = () => {
<AssetIcon symbol={chainTokenLabel} />
{chainTokenLabel}
</Styled.Title>
<Styled.ReceiveAndTransferButtons>
<Styled.ActionButtons>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Small} />
) : (
@@ -52,7 +52,7 @@ export const DYDXBalancePanel = () => {
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
)}
</Styled.ReceiveAndTransferButtons>
</Styled.ActionButtons>
</Styled.Header>
}
>
@@ -134,7 +134,7 @@ Styled.Title = styled.h3`
}
`;
Styled.ReceiveAndTransferButtons = styled(Toolbar)`
Styled.ActionButtons = styled(Toolbar)`
${layoutMixins.inlineRow}
--stickyArea-topHeight: max-content;
gap: 0.5rem;
+7 -1
View File
@@ -7,7 +7,12 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { TradeLayouts } from '@/constants/layout';
import { useBreakpoints, useCurrentMarketId, usePageTitlePriceUpdates } from '@/hooks';
import {
useBreakpoints,
useCurrentMarketId,
usePageTitlePriceUpdates,
useTradeFormInputs,
} from '@/hooks';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSelectedTradeLayout } from '@/state/layoutSelectors';
@@ -37,6 +42,7 @@ const TradePage = () => {
const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true);
usePageTitlePriceUpdates();
useTradeFormInputs();
return isTablet ? (
<Styled.TradeLayoutMobile>
+3
View File
@@ -1,6 +1,7 @@
import { configureStore } from '@reduxjs/toolkit';
import abacusStateManager from '@/lib/abacus';
import appMiddleware from './appMiddleware';
import localizationMiddleware from './localizationMiddleware';
@@ -13,6 +14,7 @@ import { inputsSlice } from './inputs';
import { layoutSlice } from './layout';
import { localizationSlice } from './localization';
import { perpetualsSlice } from './perpetuals';
import { notificationsSlice } from './notifications';
export const commandMenuSlices = [layoutSlice, localizationSlice];
@@ -26,6 +28,7 @@ export const store = configureStore({
inputs: inputsSlice.reducer,
layout: layoutSlice.reducer,
localization: localizationSlice.reducer,
notifications: notificationsSlice.reducer,
perpetuals: perpetualsSlice.reducer,
},
+15 -1
View File
@@ -1,4 +1,5 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import assign from 'lodash/assign';
import type {
InputError,
@@ -9,9 +10,14 @@ import type {
TransferInputs,
} from '@/constants/abacus';
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
type TradeFormInputs = typeof CLEARED_TRADE_INPUTS & typeof CLEARED_SIZE_INPUTS;
export interface InputsState {
current?: Nullable<string>;
inputErrors?: Nullable<InputError[]>;
tradeFormInputs: TradeFormInputs;
tradeInputs?: Nullable<TradeInputs>;
closePositionInputs?: Nullable<ClosePositionInputs>;
transferInputs?: Nullable<TransferInputs>;
@@ -20,6 +26,10 @@ export interface InputsState {
const initialState: InputsState = {
current: undefined,
inputErrors: undefined,
tradeFormInputs: {
...CLEARED_TRADE_INPUTS,
...CLEARED_SIZE_INPUTS,
},
tradeInputs: undefined,
transferInputs: undefined,
};
@@ -40,7 +50,11 @@ export const inputsSlice = createSlice({
transferInputs: transfer,
};
},
setTradeFormInputs: (state, action: PayloadAction<Partial<TradeFormInputs>>) => {
state.tradeFormInputs = assign({}, state.tradeFormInputs, action.payload);
},
},
});
export const { setInputs } = inputsSlice.actions;
export const { setInputs, setTradeFormInputs } = inputsSlice.actions;
+5
View File
@@ -120,3 +120,8 @@ export const useTradeFormData = () => {
shallowEqual
);
};
/**
* @returns Tradeform Input states for display. Abacus inputs should track these values.
*/
export const getTradeFormInputs = (state: RootState) => state.inputs.tradeFormInputs;
+9 -4
View File
@@ -1,5 +1,10 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { LOCALE_DATA, SupportedLocale, TOOLTIPS } from '@dydxprotocol/v4-localization';
import {
LOCALE_DATA,
NOTIFICATIONS,
SupportedLocale,
TOOLTIPS,
} from '@dydxprotocol/v4-localization';
import {
type LocaleData,
@@ -26,6 +31,7 @@ const getNewLocaleData = ({
const newLocaleData = {
...LOCALE_DATA[locale],
...NOTIFICATIONS[locale],
TOOLTIPS: TOOLTIPS[locale],
};
@@ -51,9 +57,8 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
} else if (globalThis.navigator?.language) {
const browserLanguageBaseTag = globalThis.navigator.language.split('-')[0].toLowerCase();
const locale = SUPPORTED_BASE_TAGS_LOCALE_MAPPING[
browserLanguageBaseTag
] as SupportedLocales;
const locale = (SUPPORTED_BASE_TAGS_LOCALE_MAPPING[browserLanguageBaseTag] ??
SUPPORTED_BASE_TAGS_LOCALE_MAPPING[SupportedLocales.EN]) as SupportedLocales;
if (locale) {
store.dispatch(setSelectedLocale({ locale, isAutoDetect: true }));
+24
View File
@@ -0,0 +1,24 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { AbacusNotification } from '@/constants/abacus';
export interface NotificationsState {
abacusNotifications: AbacusNotification[];
}
const initialState: NotificationsState = {
abacusNotifications: [],
};
export const notificationsSlice = createSlice({
name: 'Notifications',
initialState,
reducers: {
updateNotifications: (state, action: PayloadAction<AbacusNotification[]>) => {
state.abacusNotifications = action.payload;
},
},
});
export const { updateNotifications } = notificationsSlice.actions;
+3
View File
@@ -0,0 +1,3 @@
import { RootState } from './_store';
export const getAbacusNotifications = (state: RootState) => state.notifications.abacusNotifications;
+31
View File
@@ -107,12 +107,17 @@ export const formMixins: Record<
`,
inputLabel: css`
--label-textColor: var(--color-text-1);
position: relative;
height: 100%;
width: 100%;
gap: 0;
border-radius: inherit;
@media ${breakpoints.tablet} {
gap: 0.25rem;
}
`,
inputToggleGroup: css`
@@ -130,4 +135,30 @@ export const formMixins: Record<
--button-toggle-off-textColor: var(--color-text-1);
}
`,
footer: css`
${layoutMixins.stickyFooter}
${layoutMixins.withStickyFooterBackdrop}
margin-top: auto;
`,
transfersForm: css`
${() => formMixins.inputsColumn}
--form-input-gap: 1.25rem;
--form-input-height: 3.5rem;
--form-input-height-mobile: 4rem;
--form-input-paddingY: 0.5rem;
--form-input-paddingX: 1rem;
height: 100%;
label {
--label-textColor: var(--color-text-0);
}
@media ${breakpoints.tablet} {
--form-input-gap: 1rem;
}
`,
};
+2 -2
View File
@@ -12,8 +12,8 @@ export const headerMixins = {
--trigger-open-textColor: var(--color-text-2);
&:hover:not(:disabled) {
background-color: var(--trigger-hover-backgroundColor);
color: var(--trigger-hover-textColor);
--trigger-backgroundColor: var(--trigger-hover-backgroundColor);
--trigger-textColor: var(--trigger-hover-textColor);
}
`,
+4 -1
View File
@@ -2,7 +2,9 @@ import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { STRING_KEYS } from '@/constants/localization';
import { MARKETS_TO_DISPLAY } from '@/constants/markets';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { testFlags } from '@/hooks/useTestFlags';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -32,6 +34,7 @@ export const ExchangeBillboards: React.FC<ExchangeBillboardsProps> = ({
Object.values(perpetualMarkets)
.filter(Boolean)
.filter(({ id }) => (testFlags.displayAllMarkets ? true : MARKETS_TO_DISPLAY.includes(id)))
.forEach(({ oraclePrice, perpetual }) => {
const { volume24H, trades24H, openInterest = 0 } = perpetual || {};
volume24HUSDC += volume24H ?? 0;
@@ -60,7 +63,7 @@ export const ExchangeBillboards: React.FC<ExchangeBillboardsProps> = ({
},
{
key: 'trades',
labelKey: STRING_KEYS.TRADES,
labelKey: isTablet ? STRING_KEYS.TRADES_24H : STRING_KEYS.TRADES,
value: totalTrades24H || undefined,
type: isTablet ? OutputType.CompactNumber : OutputType.Number,
subLabelKey: !isTablet && STRING_KEYS.TRADES_LABEL,
+49 -40
View File
@@ -1,6 +1,7 @@
import { type OrderStatus, AbacusOrderStatus } from '@/constants/abacus';
import styled from 'styled-components';
import { AbacusOrderStatus } from '@/constants/abacus';
import { Icon } from '@/components/Icon';
import {
OrderCanceledIcon,
OrderFilledIcon,
@@ -9,49 +10,57 @@ import {
OrderPendingIcon,
} from '@/icons';
import styled from 'styled-components';
import { Icon } from '@/components/Icon';
type ElementProps = {
status: OrderStatus;
status: string;
totalFilled: number;
};
export const OrderStatusIcon = ({ status, totalFilled }: ElementProps) => (
<$Icon
{...{
[AbacusOrderStatus.open.name]:
totalFilled > 0
? {
iconComponent: OrderPartiallyFilledIcon,
color: 'var(--color-warning)',
}
: {
iconComponent: OrderOpenIcon,
color: 'var(--color-text-2)',
},
[AbacusOrderStatus.filled.name]: {
iconComponent: OrderFilledIcon,
color: 'var(--color-success)',
},
[AbacusOrderStatus.cancelled.name]: {
iconComponent: OrderCanceledIcon,
color: 'var(--color-error)',
},
[AbacusOrderStatus.canceling.name]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-error)',
},
[AbacusOrderStatus.pending.name]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-text-2)',
},
[AbacusOrderStatus.untriggered.name]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-text-2)',
},
}[status.name]}
/>
);
type StyleProps = {
className?: string;
};
export const OrderStatusIcon = ({ className, status, totalFilled }: ElementProps & StyleProps) => {
const { iconComponent, color } = {
[AbacusOrderStatus.open.rawValue]:
totalFilled > 0
? {
iconComponent: OrderPartiallyFilledIcon,
color: 'var(--color-warning)',
}
: {
iconComponent: OrderOpenIcon,
color: 'var(--color-text-2)',
},
[AbacusOrderStatus.partiallyFilled.rawValue]: {
iconComponent: OrderPartiallyFilledIcon,
color: 'var(--color-warning)',
},
[AbacusOrderStatus.filled.rawValue]: {
iconComponent: OrderFilledIcon,
color: 'var(--color-success)',
},
[AbacusOrderStatus.cancelled.rawValue]: {
iconComponent: OrderCanceledIcon,
color: 'var(--color-error)',
},
[AbacusOrderStatus.canceling.rawValue]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-error)',
},
[AbacusOrderStatus.pending.rawValue]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-text-2)',
},
[AbacusOrderStatus.untriggered.rawValue]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-text-2)',
},
}[status];
return <$Icon className={className} iconComponent={iconComponent} color={color} />;
};
const $Icon = styled(Icon)<{ color: string }>`
color: ${({ color }) => color};
+3 -2
View File
@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import { TradeInputField } from '@/constants/abacus';
import { TradeTypes } from '@/constants/trade';
import { STRING_KEYS } from '@/constants/localization';
import { STRING_KEYS, StringKey } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -30,7 +30,8 @@ const useTradeTypeOptions = () => {
const allTradeTypeItems = typeOptions?.toArray()?.map(({ type, stringKey }) => ({
value: type,
label: stringGetter({
key: type === TradeTypes.TAKE_PROFIT ? STRING_KEYS.TAKE_PROFIT_LIMIT : stringKey,
key:
type === TradeTypes.TAKE_PROFIT ? STRING_KEYS.TAKE_PROFIT_LIMIT : (stringKey as StringKey),
}),
}));
-185
View File
@@ -1,185 +0,0 @@
import { useCallback, useState, useMemo } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { Root, Trigger, Content } from '@radix-ui/react-collapsible';
import { StatusResponse } from '@0xsquid/sdk';
import { useInterval, useStringGetter } from '@/hooks';
import { STRING_KEYS } from '@/constants/localization';
import { AlertType } from '@/constants/alerts';
import { formatSeconds } from '@/lib/timeUtils';
import { AlertMessage } from '@/components/AlertMessage';
import { Output, OutputType } from '@/components/Output';
import { WithReceipt } from '@/components/WithReceipt';
import { Icon, IconName } from '@/components/Icon';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { LoadingDots } from '@/components/Loading/LoadingDots';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
type: 'withdrawal' | 'deposit';
toAmount?: number;
triggeredAt?: number;
status?: StatusResponse;
};
export const TransferStatusToast = ({
type,
toAmount,
triggeredAt = Date.now(),
status,
}: ElementProps) => {
const stringGetter = useStringGetter();
const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number>(0);
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000;
setSecondsLeft(Math.floor((triggeredAt + fromChainEta + toChainEta - Date.now()) / 1000));
}, [status]);
useInterval({ callback: updateSecondsLeft });
if (!status) return <LoadingDots size={3} />;
const inProgressStatusString =
type === 'deposit'
? secondsLeft > 0
? STRING_KEYS.DEPOSIT_STATUS
: STRING_KEYS.DEPOSIT_STATUS_SHORTLY
: secondsLeft > 0
? STRING_KEYS.WITHDRAW_STATUS
: STRING_KEYS.WITHDRAW_STATUS_SHORTLY;
const statusString =
type === 'deposit'
? status?.squidTransactionStatus === 'success'
? STRING_KEYS.DEPOSIT_COMPLETE
: inProgressStatusString
: status?.squidTransactionStatus === 'success'
? STRING_KEYS.WITHDRAW_COMPLETE
: inProgressStatusString;
return (
<Styled.Root open={open} onOpenChange={setOpen}>
<WithReceipt
hideReceipt={!open}
side="bottom"
slotReceipt={
<Styled.Receipt>
<TransferStatusSteps status={status} type={type} />
</Styled.Receipt>
}
>
<Styled.BridgingStatus>
<Styled.Status>
{stringGetter({
key: statusString,
params: {
AMOUNT_USD: <Styled.InlineOutput type={OutputType.Fiat} value={toAmount} />,
ESTIMATED_DURATION: (
<Styled.InlineOutput
type={OutputType.Text}
value={formatSeconds(Math.max(secondsLeft || 0, 0))}
/>
),
},
})}
</Styled.Status>
{error && (
<AlertMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
})}
</AlertMessage>
)}
<Styled.Trigger>
<Styled.TriggerIcon>
<Icon iconName={IconName.Caret} />
</Styled.TriggerIcon>
{stringGetter({ key: open ? STRING_KEYS.HIDE_DETAILS : STRING_KEYS.VIEW_DETAILS })}
</Styled.Trigger>
</Styled.BridgingStatus>
</WithReceipt>
</Styled.Root>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Root = styled(Root)`
margin: 0.5rem -1rem -1rem -1rem;
color: var(--color-text-0);
font: var(--font-small-book);
`;
Styled.Receipt = styled.div`
padding: 0 1rem;
`;
Styled.BridgingStatus = styled.div`
${layoutMixins.flexColumn};
background-color: var(--color-layer-3);
gap: 0.5rem;
padding: 0 1rem 1rem 1rem;
border-radius: 0.5rem;
`;
Styled.Status = styled.div`
color: var(--color-text-0);
font-size: 0.875rem;
`;
Styled.InlineOutput = styled(Output)`
display: inline-block;
color: var(--color-text-1);
`;
Styled.Content = styled(Content)`
${layoutMixins.flexColumn};
gap: 0.5rem;
padding: 1rem;
`;
Styled.Step = styled.div`
${layoutMixins.row};
gap: 0.5rem;
`;
Styled.Trigger = styled(Trigger)`
display: flex;
align-items: center;
gap: 0.5em;
color: var(--color-accent);
user-select: none;
&:focus {
outline: none;
}
`;
Styled.TriggerIcon = styled.span`
width: 0.75em;
display: inline-flex;
transition: rotate 0.3s var(--ease-out-expo);
${Styled.Trigger}[data-state='open'] & {
rotate: -0.5turn;
}
`;
+2 -46
View File
@@ -1,23 +1,15 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { isMainnet } from '@/constants/networks';
import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog';
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import { layoutMixins } from '@/styles/layoutMixins';
import { DepositDialogContent } from './DepositDialog/DepositDialogContent';
type ElementProps = {
setIsOpen?: (open: boolean) => void;
};
export const DepositDialog = ({ setIsOpen }: ElementProps) => {
const [showFaucet, setShowFaucet] = useState(!isMainnet);
const stringGetter = useStringGetter();
const { isMobile } = useBreakpoints();
@@ -28,45 +20,9 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
isOpen
setIsOpen={setIsOpen}
title={stringGetter({ key: STRING_KEYS.DEPOSIT })}
description={showFaucet && 'Test funds will be sent directly to your dYdX account.'}
placement={isMobile ? DialogPlacement.FullScreen : DialogPlacement.Default}
>
<Styled.Content>
{isMainnet || !showFaucet ? (
<DepositForm />
) : (
<TestnetDepositForm onDeposit={closeDialog} />
)}
{!isMainnet && (
<Styled.TextToggle onClick={() => setShowFaucet(!showFaucet)}>
{showFaucet ? 'Show deposit form' : 'Show test faucet'}
</Styled.TextToggle>
)}
</Styled.Content>
<DepositDialogContent onDeposit={closeDialog} />
</Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.TextToggle = styled.div`
${layoutMixins.stickyFooter}
color: var(--color-accent);
cursor: pointer;
margin-top: auto;
&:hover {
text-decoration: underline;
}
`;
Styled.Content = styled.div`
${layoutMixins.stickyArea0}
--stickyArea0-bottomHeight: 2rem;
--stickyArea0-bottomGap: 1rem;
--stickyArea0-totalInsetBottom: 0.5rem;
${layoutMixins.flexColumn}
gap: 1rem;
`;
@@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { isMainnet } from '@/constants/networks';
import { layoutMixins } from '@/styles/layoutMixins';
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import abacusStateManager from '@/lib/abacus';
type ElementProps = {
onDeposit?: () => void;
};
export const DepositDialogContent = ({ onDeposit }: ElementProps) => {
const [showFaucet, setShowFaucet] = useState(!isMainnet);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: TransferType.deposit.rawValue,
});
return () => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: null,
});
};
}, []);
return (
<Styled.Content>
{isMainnet || !showFaucet ? (
<DepositForm onDeposit={onDeposit} />
) : (
<TestnetDepositForm onDeposit={onDeposit} />
)}
{!isMainnet && (
<Styled.TextToggle onClick={() => setShowFaucet(!showFaucet)}>
{showFaucet ? 'Show deposit form' : 'Show test faucet'}
</Styled.TextToggle>
)}
</Styled.Content>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Content = styled.div`
${layoutMixins.flexColumn}
gap: 1rem;
form {
flex: 1;
}
`;
Styled.TextToggle = styled.div`
${layoutMixins.stickyFooter}
--stickyArea-bottomHeight: 0;
color: var(--color-accent);
cursor: pointer;
margin-top: auto;
&:hover {
text-decoration: underline;
}
`;
+87
View File
@@ -0,0 +1,87 @@
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { ButtonSize } from '@/constants/buttons';
import { useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog';
import { ToggleGroup } from '@/components/ToggleGroup';
import { TransferForm } from '@/views/forms/TransferForm';
import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm';
import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { DepositDialogContent } from './DepositDialog/DepositDialogContent';
type ElementProps = {
selectedTransferType?: string;
setIsOpen?: (open: boolean) => void;
};
export const ManageFundsDialog = ({ setIsOpen, selectedTransferType }: ElementProps) => {
const stringGetter = useStringGetter();
const { type } = useSelector(getTransferInputs, shallowEqual) || {};
const currentType = type?.rawValue ?? selectedTransferType ?? TransferType.deposit.rawValue;
const closeDialog = () => setIsOpen?.(false);
const transferTypeConfig = {
[TransferType.deposit.rawValue]: {
value: TransferType.deposit.rawValue,
label: stringGetter({ key: STRING_KEYS.DEPOSIT }),
component: <DepositDialogContent />,
},
[TransferType.withdrawal.rawValue]: {
value: TransferType.withdrawal.rawValue,
label: stringGetter({ key: STRING_KEYS.WITHDRAW }),
component: <WithdrawForm />,
},
[TransferType.transferOut.rawValue]: {
value: TransferType.transferOut.rawValue,
label: stringGetter({ key: STRING_KEYS.TRANSFER }),
component: <TransferForm onDone={closeDialog} />,
},
};
return (
<Styled.Dialog
isOpen
setIsOpen={setIsOpen}
placement={DialogPlacement.FullScreen}
title={
<Styled.ToggleGroup
items={Object.values(transferTypeConfig)}
value={currentType}
size={ButtonSize.Medium}
onValueChange={(value: string) =>
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value,
})
}
/>
}
hasHeaderBorder
>
{transferTypeConfig[currentType].component}
</Styled.Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)`
--dialog-content-paddingTop: 1.5rem;
`;
Styled.ToggleGroup = styled(ToggleGroup)`
overflow-x: auto;
button {
--button-toggle-off-border: none;
--button-padding: 0 1rem;
}
`;
+7 -11
View File
@@ -5,7 +5,7 @@ import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { useAccounts, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles';
import breakpoints from '@/styles/breakpoints';
import { layoutMixins } from '@/styles/layoutMixins';
import { AlertMessage } from '@/components/AlertMessage';
@@ -54,11 +54,14 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => {
<Styled.WithReceipt
slotReceipt={
<Styled.CheckboxContainer>
<Styled.Checkbox
<Checkbox
checked={hasAcknowledged}
onClick={() => setHasAcknowledged(!hasAcknowledged)}
onCheckedChange={setHasAcknowledged}
id="acknowledge-secret-phase-risk"
label={stringGetter({
key: STRING_KEYS.SECRET_PHRASE_RISK_ACK,
})}
/>
<span>{stringGetter({ key: STRING_KEYS.SECRET_PHRASE_RISK_ACK })}</span>
</Styled.CheckboxContainer>
}
>
@@ -162,17 +165,10 @@ Styled.WithReceipt = styled(WithReceipt)`
`;
Styled.CheckboxContainer = styled.div`
${layoutMixins.spacedRow};
padding: 1rem;
gap: 0.75rem;
color: var(--color-text-0);
`;
Styled.Checkbox = styled(Checkbox)`
--checkbox-backgroundColor: var(--color-layer-3);
`;
Styled.AlertMessage = styled(AlertMessage)`
font: var(--font-base-book);
margin: 0;
@@ -7,6 +7,7 @@ import { STRING_KEYS } from '@/constants/localization';
import { ButtonAction } from '@/constants/buttons';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import { Button } from '@/components/Button';
import { Link } from '@/components/Link';
@@ -54,14 +55,14 @@ export const AcknowledgeTerms = ({ onClose, onContinue }: ElementProps) => {
<li>{stringGetter({ key: STRING_KEYS.TOS_LINE5 })}</li>
</ul>
</Styled.TOS>
<Styled.Row>
<Styled.Footer>
<Styled.Button onClick={onClose} action={ButtonAction.Base}>
{stringGetter({ key: STRING_KEYS.CLOSE })}
</Styled.Button>
<Styled.Button onClick={onAcknowledgement} action={ButtonAction.Primary}>
{stringGetter({ key: STRING_KEYS.I_AGREE })}
</Styled.Button>
</Styled.Row>
</Styled.Footer>
</>
);
};
@@ -87,8 +88,9 @@ Styled.TOS = styled.section`
}
`;
Styled.Row = styled.div`
${layoutMixins.row};
Styled.Footer = styled.div`
${formMixins.footer};
${layoutMixins.row}
gap: 1rem;
`;
@@ -8,7 +8,7 @@ import { STRING_KEYS } from '@/constants/localization';
import { Button } from '@/components/Button';
import { openDialog } from '@/state/dialogs';
import { forceOpenDialog } from '@/state/dialogs';
import { getOnboardingState } from '@/state/accountSelectors';
import { OnboardingState } from '@/constants/account';
@@ -26,7 +26,7 @@ export const OnboardingTriggerButton = ({ size = ButtonSize.Small }: StyleProps)
<Button
action={ButtonAction.Primary}
size={size}
onClick={() => dispatch(openDialog({ type: DialogTypes.Onboarding }))}
onClick={() => dispatch(forceOpenDialog({ type: DialogTypes.Onboarding }))}
>
{
{
+98
View File
@@ -0,0 +1,98 @@
import { useEffect, useMemo, useState } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { NotificationType } from '@/constants/notifications';
import { useStringGetter } from '@/hooks';
import { useNotifications } from '@/hooks/useNotifications';
import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu';
import { Switch } from '@/components/Switch';
export const usePreferenceMenu = () => {
const stringGetter = useStringGetter();
// Notifications
const { notificationPreferences, setNotificationPreferences } = useNotifications();
const [enabledNotifs, setEnabledNotifs] = useState(notificationPreferences);
const toggleNotifPreference = (type: NotificationType) =>
setEnabledNotifs((prev) => ({ ...prev, [type]: !prev[type] }));
useEffect(() => {
setNotificationPreferences(enabledNotifs);
}, [enabledNotifs]);
const notificationSection = useMemo(
() => ({
group: 'Notifications',
groupLabel: stringGetter({ key: STRING_KEYS.NOTIFICATIONS }),
items: [
{
value: NotificationType.AbacusGenerated,
label: stringGetter({ key: STRING_KEYS.TRADING }),
slotAfter: (
<Switch
name={NotificationType.AbacusGenerated}
checked={enabledNotifs[NotificationType.AbacusGenerated]}
onCheckedChange={(enabled: boolean) => null}
/>
),
onSelect: () => toggleNotifPreference(NotificationType.AbacusGenerated),
},
{
value: NotificationType.SquidTransfer,
label: stringGetter({ key: STRING_KEYS.TRANSFERS }),
slotAfter: (
<Switch
name={NotificationType.SquidTransfer}
checked={enabledNotifs[NotificationType.SquidTransfer]}
onCheckedChange={(enabled: boolean) => null}
/>
),
onSelect: () => toggleNotifPreference(NotificationType.SquidTransfer),
},
{
value: NotificationType.ReleaseUpdates,
label: "Release Updates",
slotAfter: (
<Switch
name={NotificationType.ReleaseUpdates}
checked={enabledNotifs[NotificationType.ReleaseUpdates]}
onCheckedChange={(enabled: boolean) => null}
/>
),
onSelect: () => toggleNotifPreference(NotificationType.ReleaseUpdates),
}
],
}),
[stringGetter, enabledNotifs]
);
return [notificationSection];
};
type ElementProps = {
setIsOpen: (open: boolean) => void;
};
export const PreferencesDialog = ({ setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const preferenceItems = usePreferenceMenu();
return (
<Styled.ComboboxDialogMenu
isOpen
title={stringGetter({ key: STRING_KEYS.PREFERENCES })}
items={preferenceItems}
setIsOpen={setIsOpen}
/>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.ComboboxDialogMenu = styled(ComboboxDialogMenu)`
--dialog-content-paddingBottom: 0.5rem;
`;
-124
View File
@@ -1,124 +0,0 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { DydxChainAsset } from '@/constants/wallets';
import { formMixins } from '@/styles/formMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import { useAccounts, useTokenConfigs, useStringGetter } from '@/hooks';
import { AssetIcon } from '@/components/AssetIcon';
import { CopyButton } from '@/components/CopyButton';
import { Dialog } from '@/components/Dialog';
import { QrCode } from '@/components/QrCode';
import { SelectItem, SelectMenu } from '@/components/SelectMenu';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { truncateAddress } from '@/lib/wallet';
import { OnboardingTriggerButton } from './OnboardingTriggerButton';
type ElementProps = {
selectedAsset?: DydxChainAsset;
setIsOpen: (open: boolean) => void;
};
export const ReceiveDialog = ({ selectedAsset = DydxChainAsset.CHAINTOKEN, setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const { dydxAddress } = useAccounts();
const { chainTokenLabel, usdcLabel } = useTokenConfigs();
const [asset, setAsset] = useState(selectedAsset);
const assetOptions = [
{
value: DydxChainAsset.USDC,
label: (
<Styled.InlineRow>
<AssetIcon symbol="USDC" /> {usdcLabel}
</Styled.InlineRow>
),
},
{
value: DydxChainAsset.CHAINTOKEN,
label: (
<Styled.InlineRow>
{/* TODO: conditionally display the native token assetIcon */}
{/* <AssetIcon symbol="DYDX" /> */}
{chainTokenLabel}
</Styled.InlineRow>
),
},
];
return (
<Styled.Dialog isOpen setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.RECEIVE })}>
<Styled.Content>
<Styled.SelectMenu
label={stringGetter({ key: STRING_KEYS.ASSET })}
value={asset}
onValueChange={setAsset}
>
{assetOptions.map(({ value, label }) => (
<Styled.SelectItem key={value} value={value} label={label} />
))}
</Styled.SelectMenu>
{!dydxAddress ? (
<OnboardingTriggerButton />
) : (
<>
<Styled.WithDetailsReceipt
side="bottom"
detailItems={[
{
key: 'address',
label: stringGetter({ key: STRING_KEYS.DYDX_CHAIN_ADDRESS }),
value: truncateAddress(dydxAddress),
},
]}
>
<QrCode hasLogo value={dydxAddress!} />
</Styled.WithDetailsReceipt>
<CopyButton value={dydxAddress} />
</>
)}
</Styled.Content>
</Styled.Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)`
--dialog-content-paddingTop: var(--default-border-width);
--dialog-width: 20rem;
`;
Styled.Content = styled.div`
${formMixins.inputsColumn}
gap: 1.25rem;
`;
Styled.SelectMenu = styled(SelectMenu)`
${formMixins.inputSelectMenu}
--form-input-height: 3.5rem;
`;
Styled.SelectItem = styled(SelectItem)`
${formMixins.inputSelectMenuItem}
`;
Styled.InlineRow = styled.span`
${layoutMixins.inlineRow}
height: 100%;
img {
font-size: 1.1em;
}
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
border-radius: 0.75em;
`;
-2
View File
@@ -112,8 +112,6 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>`
--dialog-backgroundColor: var(--color-layer-2);
--dialog-header-height: 1rem;
--dialog-header-paddingTop: 1.5rem;
--dialog-header-paddingBottom: 1rem;
--dialog-content-paddingTop: 0;
--dialog-content-paddingBottom: 0;
--dialog-content-paddingLeft: 0;
@@ -2,14 +2,16 @@ import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react
import styled, { type AnyStyledComponent } from 'styled-components';
import { type NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import { parseUnits } from 'viem';
import { Abi, parseUnits } from 'viem';
import erc20 from '@/abi/erc20.json';
import erc20_usdt from '@/abi/erc20_usdt.json';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NotificationStatus } from '@/constants/notifications';
import { NumberSign } from '@/constants/numbers';
import type { EvmAddress } from '@/constants/wallets';
@@ -190,15 +192,23 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals);
if (sourceAmountBN > (allowance as bigint)) {
const { request } = await publicClientWagmi.simulateContract({
account: evmAddress,
address: sourceToken.address as EvmAddress,
abi: erc20,
functionName: 'approve',
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
});
const simulateApprove = async (abi: Abi) =>
publicClientWagmi.simulateContract({
account: evmAddress,
address: sourceToken.address as EvmAddress,
abi,
functionName: 'approve',
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
});
const approveTx = await signerWagmi.writeContract(request);
let result;
try {
result = await simulateApprove(erc20 as Abi);
} catch (e) {
result = await simulateApprove(erc20_usdt as Abi);
}
const approveTx = await signerWagmi.writeContract(result.request);
await publicClientWagmi.waitForTransactionReceipt({
hash: approveTx,
});
@@ -244,6 +254,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
});
abacusStateManager.clearTransferInputValues();
setFromAmount('');
@@ -351,14 +362,17 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
/>
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
/>
</Styled.Footer>
</Styled.Form>
);
};
@@ -366,13 +380,12 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
--form-input-height: 3.5rem;
${formMixins.transfersForm}
`;
${layoutMixins.flexColumn}
gap: 1.5rem;
${layoutMixins.stickyArea1}
min-height: calc(100% - var(--stickyArea0-bottomHeight));
Styled.Footer = styled.footer`
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
@@ -28,7 +28,9 @@ import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccountBuyingPower, getSubaccountEquity } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
@@ -64,6 +66,8 @@ export const DepositButtonAndReceipt = ({
const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
const stringGetter = useStringGetter();
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const {
matchNetwork: switchNetwork,
isSwitchingNetwork,
@@ -210,7 +214,9 @@ export const DepositButtonAndReceipt = ({
}
slotError={slotError}
>
{!isMatchingNetwork ? (
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : !isMatchingNetwork ? (
<Button
action={ButtonAction.Primary}
onClick={switchNetwork}
@@ -247,8 +253,7 @@ Styled.WithReceipt = styled(WithReceipt)`
Styled.CollapsibleDetails = styled.div`
${layoutMixins.column}
padding: 0.375rem 1rem 0.5rem;
gap: 0.5rem;
padding: var(--form-input-paddingY) var(--form-input-paddingX);
`;
Styled.Details = styled(Details)`
@@ -2,14 +2,16 @@ import { useState, type FormEvent, useEffect } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonAction, ButtonType } from '@/constants/buttons';
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { useAccounts, useStringGetter, useSubaccount } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { Button } from '@/components/Button';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors';
import { getSelectedNetwork } from '@/state/appSelectors';
@@ -26,6 +28,7 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
const { requestFaucetFunds } = useSubaccount();
const subAccount = useSelector(getSubaccount, shallowEqual);
const selectedNetwork = useSelector(getSelectedNetwork);
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const [isLoading, setIsLoading] = useState(false);
@@ -71,13 +74,15 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
},
})}
</p>
<Styled.SubmitButton
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{ isLoading }}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Styled.SubmitButton>
<Styled.Footer>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : (
<Button action={ButtonAction.Primary} type={ButtonType.Submit} state={{ isLoading }}>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Button>
)}
</Styled.Footer>
</Styled.Form>
);
};
@@ -85,10 +90,14 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
${layoutMixins.column}
gap: 1rem;
${formMixins.transfersForm}
`;
Styled.SubmitButton = styled(Button)`
${layoutMixins.stickyFooter}
Styled.Footer = styled.footer`
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
button {
--button-width: 100%;
}
`;
@@ -10,6 +10,7 @@ import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NotificationStatus } from '@/constants/notifications';
import { NumberSign } from '@/constants/numbers';
import {
@@ -171,6 +172,7 @@ export const WithdrawForm = () => {
toChainId: chainIdStr || undefined,
toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
});
abacusStateManager.clearTransferInputValues();
setWithdrawAmount('');
@@ -369,14 +371,16 @@ export const WithdrawForm = () => {
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<WithdrawButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
setSlippage={onSetSlippage}
slippage={slippage}
withdrawChain={chainIdStr || undefined}
withdrawToken={toToken || undefined}
/>
<Styled.Footer>
<WithdrawButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
setSlippage={onSetSlippage}
slippage={slippage}
withdrawChain={chainIdStr || undefined}
withdrawToken={toToken || undefined}
/>
</Styled.Footer>
</Styled.Form>
);
};
@@ -388,14 +392,12 @@ Styled.DiffOutput = styled(DiffOutput)`
`;
Styled.Form = styled.form`
--form-input-height: 3.5rem;
${formMixins.transfersForm}
`;
min-height: calc(100% - var(--stickyArea0-bottomHeight));
${layoutMixins.flexColumn}
gap: 1.25rem;
${layoutMixins.stickyArea1}
Styled.Footer = styled.footer`
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
`;
Styled.DestinationRow = styled.div`
@@ -24,7 +24,9 @@ import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
@@ -57,6 +59,7 @@ export const WithdrawButtonAndReceipt = ({
const { leverage } = useSelector(getSubaccount, shallowEqual) || {};
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const toAmount =
summary?.toAmount &&
@@ -192,16 +195,20 @@ export const WithdrawButtonAndReceipt = ({
</Styled.CollapsibleDetails>
}
>
<Button
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{
isDisabled: !isFormValid,
isLoading: (isFormValid && !requestPayload) || isLoading,
}}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : (
<Button
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{
isDisabled: !isFormValid,
isLoading: (isFormValid && !requestPayload) || isLoading,
}}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
)}
</Styled.WithReceipt>
);
};
@@ -223,8 +230,7 @@ Styled.WithReceipt = styled(WithReceipt)`
Styled.CollapsibleDetails = styled.div`
${layoutMixins.column}
padding: 0.375rem 1rem 0.5rem;
gap: 0.5rem;
padding: var(--form-input-paddingY) var(--form-input-paddingX);
`;
Styled.Details = styled(Details)`
+21 -22
View File
@@ -1,6 +1,6 @@
import { type FormEvent, useState, Ref, useCallback } from 'react';
import { type FormEvent, useState, Ref, useCallback, useEffect } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import type { NumberFormatValues, SourceInfo } from 'react-number-format';
import { AlertType } from '@/constants/alerts';
@@ -37,7 +37,8 @@ import { WithTooltip } from '@/components/WithTooltip';
import { Orderbook } from '@/views/tables/Orderbook';
import { getCurrentInput, useTradeFormData } from '@/state/inputsSelectors';
import { setTradeFormInputs } from '@/state/inputs';
import { getCurrentInput, getTradeFormInputs, useTradeFormData } from '@/state/inputsSelectors';
import { getCurrentMarketConfig } from '@/state/perpetualsSelectors';
import abacusStateManager from '@/lib/abacus';
@@ -80,6 +81,7 @@ export const TradeForm = ({
const [placeOrderError, setPlaceOrderError] = useState<string>();
const [showOrderbook, setShowOrderbook] = useState(false);
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { placeOrder } = useSubaccount();
@@ -98,18 +100,21 @@ export const TradeForm = ({
tradeErrors,
} = useTradeFormData();
const { limitPrice, triggerPrice, trailingPercent } = price || {};
const currentInput = useSelector(getCurrentInput);
const { tickSizeDecimals, stepSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) || {};
const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual);
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
const needsAdvancedOptions =
needsGoodUntil || timeInForceOptions || executionOptions || needsPostOnly || needsReduceOnly;
const tradeFormInputs: TradeBoxInputConfig[] = [];
const isInputFilled =
Object.values(price || {}).some((val) => val != null) ||
Object.values(tradeFormInputValues).some((val) => val !== '') ||
Object.values(price || {}).some((val) => !!val) ||
[size?.size, size?.usdcSize, size?.leverage].some((val) => val != null);
const hasInputErrors =
@@ -197,13 +202,10 @@ export const TradeForm = ({
<Tag>USD</Tag>
</>
),
onChange: ({ floatValue }: NumberFormatValues) => {
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.triggerPrice,
});
onChange: ({ value }: NumberFormatValues) => {
dispatch(setTradeFormInputs({ triggerPriceInput: value }));
},
value: triggerPrice ?? '',
value: triggerPriceInput ?? '',
decimals: tickSizeDecimals || USD_DECIMALS,
});
}
@@ -220,10 +222,10 @@ export const TradeForm = ({
<Tag>USD</Tag>
</>
),
onChange: ({ floatValue }: NumberFormatValues) => {
abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice });
onChange: ({ value }: NumberFormatValues) => {
dispatch(setTradeFormInputs({ limitPriceInput: value }));
},
value: limitPrice ?? '',
value: limitPriceInput,
decimals: tickSizeDecimals || USD_DECIMALS,
});
}
@@ -237,13 +239,10 @@ export const TradeForm = ({
{stringGetter({ key: STRING_KEYS.TRAILING_PERCENT })}
</WithTooltip>
),
onChange: ({ floatValue }: NumberFormatValues) => {
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.trailingPercent,
});
onChange: ({ value }: NumberFormatValues) => {
dispatch(setTradeFormInputs({ trailingPercentInput: value }));
},
value: trailingPercent ?? '',
value: trailingPercentInput ?? '',
});
}
@@ -460,10 +459,10 @@ Styled.ButtonRow = styled.div`
`;
Styled.Footer = styled.footer`
${layoutMixins.stickyFooter}
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--tradeBox-content-paddingBottom);
backdrop-filter: none;
${layoutMixins.column}
${layoutMixins.noPointerEvents}
margin-top: auto;
`;
@@ -130,40 +130,38 @@ export const AdvancedTradeOptions = () => {
</Styled.SelectMenu>
)}
{needsPostOnly && (
<Styled.CheckboxField>
<Checkbox
checked={postOnly || false}
onClick={() =>
abacusStateManager.setTradeValue({
value: !postOnly,
field: TradeInputField.postOnly,
})
}
/>
<Styled.CheckboxLabel>
<Checkbox
checked={postOnly || false}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
field: TradeInputField.postOnly,
})
}
id="post-only"
label={
<WithTooltip tooltip="post-only" side="right">
{stringGetter({ key: STRING_KEYS.POST_ONLY })}
</WithTooltip>
</Styled.CheckboxLabel>
</Styled.CheckboxField>
}
/>
)}
{needsReduceOnly && (
<Styled.CheckboxField>
<Checkbox
checked={reduceOnly || false}
onClick={() =>
abacusStateManager.setTradeValue({
value: !reduceOnly,
field: TradeInputField.reduceOnly,
})
}
/>
<Styled.CheckboxLabel>
<Checkbox
checked={reduceOnly || false}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
field: TradeInputField.reduceOnly,
})
}
id="reduce-only"
label={
<WithTooltip tooltip="reduce-only" side="right">
{stringGetter({ key: STRING_KEYS.REDUCE_ONLY })}
</WithTooltip>
</Styled.CheckboxLabel>
</Styled.CheckboxField>
}
/>
)}
</>
)}
@@ -174,15 +172,6 @@ export const AdvancedTradeOptions = () => {
const Styled: Record<string, AnyStyledComponent> = {};
Styled.CheckboxField = styled.div`
${layoutMixins.row}
gap: 0.5rem;
`;
Styled.CheckboxLabel = styled.div`
font-size: 0.875em;
`;
Styled.Collapsible = styled(Collapsible)`
--trigger-backgroundColor: transparent;
--trigger-open-backgroundColor: transparent;
@@ -30,8 +30,8 @@ import { getSelectedOrderSide, hasPositionSideChanged } from '@/lib/tradeData';
import { LeverageSlider } from './LeverageSlider';
type ElementProps = {
leverageInputValue: Nullable<number>;
setLeverageInputValue: Dispatch<SetStateAction<Nullable<number>>>;
leverageInputValue: string;
setLeverageInputValue: (value: string) => void;
};
export const MarketLeverageInput = ({
@@ -66,7 +66,7 @@ export const MarketLeverageInput = ({
const newLeverage = MustBigNumber(floatValue).toFixed();
if (value === '' || newLeverage === 'NaN' || !floatValue) {
setLeverageInputValue(null);
setLeverageInputValue('');
abacusStateManager.setTradeValue({
value: null,
field: TradeInputField.leverage,
@@ -84,7 +84,7 @@ export const MarketLeverageInput = ({
? newLeverageBN.abs().negated()
: newLeverageBN.abs();
setLeverageInputValue(newLeverageSignedBN.toNumber());
setLeverageInputValue(newLeverageSignedBN.toString());
abacusStateManager.setTradeValue({
value: newLeverageSignedBN.toFixed(LEVERAGE_DECIMALS),
@@ -98,11 +98,11 @@ export const MarketLeverageInput = ({
if (leveragePosition === PositionSide.None) return;
const inputValue = leverageInputValue || currentLeverage;
const newInputValue = MustBigNumber(inputValue).negated().toNumber();
const newInputValue = MustBigNumber(inputValue).negated().toFixed(LEVERAGE_DECIMALS);
setLeverageInputValue(newInputValue);
abacusStateManager.setTradeValue({
value: newInputValue.toFixed(LEVERAGE_DECIMALS),
value: newInputValue,
field: TradeInputField.leverage,
});
};
@@ -9,6 +9,7 @@ import { TRADE_TYPE_STRINGS, MobilePlaceOrderSteps } from '@/constants/trade';
import { useStringGetter, useTokenConfigs } from '@/hooks';
import { AssetIcon } from '@/components/AssetIcon';
import { Button } from '@/components/Button';
import { Output, OutputType, ShowSign } from '@/components/Output';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
@@ -77,7 +78,11 @@ export const PlaceOrderButtonAndReceipt = ({
},
{
key: 'fee',
label: stringGetter({ key: STRING_KEYS.FEE }),
label: (
<WithTooltip tooltip="fee" side="right">
{stringGetter({ key: STRING_KEYS.FEE })}
</WithTooltip>
),
value: <Output type={OutputType.Fiat} value={fee} useGrouping />,
},
{
@@ -85,7 +90,7 @@ export const PlaceOrderButtonAndReceipt = ({
label: (
<>
{stringGetter({ key: STRING_KEYS.MAXIMUM_REWARDS })}
{/* <AssetIcon symbol="DYDX" /> */}
<AssetIcon symbol={chainTokenLabel} />
</>
),
value: (
@@ -56,7 +56,8 @@ const ToggleContainer = styled(ToggleGroup)<{ value: OrderSide }>`
position: relative;
> button {
--button-border: none;
--button-toggle-on-border: none;
--button-toggle-off-border: none;
--button-toggle-off-backgroundColor: transparent;
--button-toggle-on-backgroundColor: transparent;
--button-toggle-on-textColor: var(--toggle-color);
+24 -18
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import styled, { AnyStyledComponent } from 'styled-components';
import { TradeInputField } from '@/constants/abacus';
@@ -20,8 +20,13 @@ import { Icon, IconName } from '@/components/Icon';
import { ToggleButton } from '@/components/ToggleButton';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { setTradeFormInputs } from '@/state/inputs';
import { getInputTradeSizeData, getInputTradeOptions } from '@/state/inputsSelectors';
import {
getInputTradeSizeData,
getInputTradeOptions,
getTradeFormInputs,
} from '@/state/inputsSelectors';
import { getCurrentMarketConfig } from '@/state/perpetualsSelectors';
@@ -31,12 +36,8 @@ import { MustBigNumber } from '@/lib/numbers';
import { MarketLeverageInput } from './MarketLeverageInput';
export const TradeSizeInputs = () => {
const [sizeInputValue, setSizeInputValue] = useState<number | null>();
const [usdcInputValue, setUsdcInputValue] = useState<number | null>();
const [leverageInputValue, setLeverageInputValue] = useState<number | null>();
const [showUSDCInputOnTablet, setShowUSDCInputOnTablet] = useState(false);
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { isTablet } = useBreakpoints();
@@ -50,24 +51,27 @@ export const TradeSizeInputs = () => {
const { needsLeverage } = currentTradeInputOptions || {};
const decimals = stepSizeDecimals || TOKEN_DECIMALS;
const { amountInput, usdAmountInput, leverageInput } = useSelector(
getTradeFormInputs,
shallowEqual
);
// Update State variables if their inputs are not being source of calculations
// Or if they have been reset to null
useEffect(() => {
if (lastEditedInput !== TradeSizeInput.Size || size == null) {
// Abacus size already respects step size
// using .toFixed to prevent trailing zeros and exponential notation
setSizeInputValue(size);
dispatch(setTradeFormInputs({ amountInput: size ? size.toString() : '' }));
}
if (lastEditedInput !== TradeSizeInput.Usdc || usdcSize == null) {
setUsdcInputValue(usdcSize);
dispatch(setTradeFormInputs({ usdAmountInput: usdcSize ? usdcSize.toString() : '' }));
}
if (lastEditedInput !== TradeSizeInput.Leverage || leverage == null) {
setLeverageInputValue(leverage);
dispatch(setTradeFormInputs({ leverageInput: leverage ? leverage.toString() : '' }));
}
}, [size, usdcSize, leverage, lastEditedInput]);
const onSizeInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
setSizeInputValue(floatValue);
dispatch(setTradeFormInputs({ amountInput: value }));
const newAmount = MustBigNumber(floatValue).toFixed(decimals);
abacusStateManager.setTradeValue({
@@ -77,7 +81,7 @@ export const TradeSizeInputs = () => {
};
const onUSDCInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
setUsdcInputValue(floatValue);
dispatch(setTradeFormInputs({ usdAmountInput: value }));
const newUsdcAmount = MustBigNumber(floatValue).toFixed();
abacusStateManager.setTradeValue({
@@ -113,7 +117,7 @@ export const TradeSizeInputs = () => {
}
slotRight={isTablet && inputToggleButton}
type={InputType.Number}
value={sizeInputValue || ''}
value={amountInput || ''}
/>
);
@@ -122,7 +126,7 @@ export const TradeSizeInputs = () => {
id="trade-usdc"
onInput={onUSDCInput}
type={InputType.Currency}
value={usdcInputValue || ''}
value={usdAmountInput || ''}
decimals={tickSizeDecimals || USD_DECIMALS}
label={
<>
@@ -153,8 +157,10 @@ export const TradeSizeInputs = () => {
{needsLeverage && (
<MarketLeverageInput
leverageInputValue={leverageInputValue}
setLeverageInputValue={setLeverageInputValue}
leverageInputValue={leverageInput}
setLeverageInputValue={(value: string) =>
dispatch(setTradeFormInputs({ leverageInput: value }))
}
/>
)}
</Styled.Column>
+15 -17
View File
@@ -119,6 +119,10 @@ export const TransferForm = ({
return () => {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: null,
});
};
}, []);
@@ -300,7 +304,7 @@ export const TransferForm = ({
}}
>
<Styled.Row>
<Styled.FormInput
<FormInput
id="destination"
onInput={(e: SyntheticInputEvent) => onChangeAddress(e.target?.value)}
label={
@@ -353,8 +357,8 @@ export const TransferForm = ({
))}
</Styled.SelectMenu>
<WithDetailsReceipt side="bottom" detailItems={amountDetailItems}>
<Styled.FormInput
<Styled.WithDetailsReceipt side="bottom" detailItems={amountDetailItems}>
<FormInput
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
type={InputType.Number}
onChange={({ floatValue }: NumberFormatValues) => onChangeAmount(floatValue)}
@@ -371,7 +375,7 @@ export const TransferForm = ({
}
disabled={isLoading}
/>
</WithDetailsReceipt>
</Styled.WithDetailsReceipt>
{showNotEnoughGasWarning && (
<AlertMessage type={AlertType.Warning}>
@@ -399,17 +403,12 @@ export const TransferForm = ({
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
${formMixins.inputsColumn}
gap: 1.25rem;
`;
Styled.FormInput = styled(FormInput)`
--form-input-height: 3.5rem;
${formMixins.transfersForm}
`;
Styled.Footer = styled.footer`
${layoutMixins.stickyFooter}
margin-top: auto;
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
`;
Styled.Row = styled.div`
@@ -419,7 +418,6 @@ Styled.Row = styled.div`
Styled.SelectMenu = styled(SelectMenu)`
${formMixins.inputSelectMenu}
--form-input-height: 3.5rem;
`;
Styled.SelectItem = styled(SelectItem)`
@@ -430,6 +428,10 @@ Styled.NetworkSelectMenu = styled(Styled.SelectMenu)`
pointer-events: none;
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.InlineRow = styled.span`
${layoutMixins.inlineRow}
height: 100%;
@@ -444,10 +446,6 @@ Styled.DestinationInputLabel = styled.span`
svg {
color: var(--color-positive);
path {
stroke-width: 2;
}
}
`;
@@ -1,4 +1,5 @@
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
@@ -12,9 +13,9 @@ import { DiffOutput } from '@/components/DiffOutput';
import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
@@ -102,7 +103,7 @@ export const TransferButtonAndReceipt = ({
].filter(isTruthy);
return (
<WithDetailsReceipt detailItems={transferDetailItems}>
<Styled.WithDetailsReceipt detailItems={transferDetailItems}>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : (
@@ -114,6 +115,16 @@ export const TransferButtonAndReceipt = ({
{stringGetter({ key: STRING_KEYS.CONFIRM_TRANSFER })}
</Button>
)}
</WithDetailsReceipt>
</Styled.WithDetailsReceipt>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
dl {
padding: var(--form-input-paddingY) var(--form-input-paddingX);
}
`;
+66 -51
View File
@@ -1,12 +1,13 @@
import { memo } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import type { Dispatch } from '@reduxjs/toolkit';
import { OnboardingState } from '@/constants/account';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, TOOLTIP_STRING_KEYS } from '@/constants/localization';
import { STRING_KEYS, StringGetterFunction, TOOLTIP_STRING_KEYS } from '@/constants/localization';
import { isMainnet } from '@/constants/networks';
import { DydxChainAsset, wallets } from '@/constants/wallets';
import {
@@ -21,6 +22,7 @@ import {
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { AssetIcon } from '@/components/AssetIcon';
import { CopyButton } from '@/components/CopyButton';
import { DropdownMenu } from '@/components/DropdownMenu';
import { Output, OutputType } from '@/components/Output';
import { Icon, IconName } from '@/components/Icon';
@@ -87,19 +89,16 @@ export const AccountMenu = () => {
</WithTooltip>
<Styled.Address>{truncateAddress(dydxAddress)}</Styled.Address>
</Styled.Column>
<IconButton
action={ButtonAction.Base}
iconName={IconName.Copy}
onClick={() => dydxAddress && navigator.clipboard.writeText(dydxAddress)}
shape={ButtonShape.Square}
/>
<IconButton
action={ButtonAction.Base}
href={`${mintscanBase}/account/${dydxAddress}`}
iconName={IconName.LinkOut}
shape={ButtonShape.Square}
type={ButtonType.Link}
/>
<Styled.CopyButton buttonType="icon" value={dydxAddress} shape={ButtonShape.Square} />
<WithTooltip tooltipString={stringGetter({ key: STRING_KEYS.MINTSCAN })}>
<Styled.IconButton
action={ButtonAction.Base}
href={`${mintscanBase}/account/${dydxAddress}`}
iconName={IconName.LinkOut}
shape={ButtonShape.Square}
type={ButtonType.Link}
/>
</WithTooltip>
</Styled.AddressRow>
<Styled.AddressRow>
{walletType && (
@@ -121,7 +120,7 @@ export const AccountMenu = () => {
key: STRING_KEYS.ASSET_BALANCE,
params: { ASSET: chainTokenLabel },
})}
{/* <AssetIcon symbol="DYDX" /> */}
<AssetIcon symbol={chainTokenLabel} />
</Styled.label>
<Styled.BalanceOutput type={OutputType.Asset} value={nativeTokenBalance} />
</div>
@@ -129,6 +128,7 @@ export const AccountMenu = () => {
asset={DydxChainAsset.CHAINTOKEN}
dispatch={dispatch}
hasBalance={nativeTokenBalance.gt(0)}
stringGetter={stringGetter}
/>
</div>
<div>
@@ -150,6 +150,7 @@ export const AccountMenu = () => {
asset={DydxChainAsset.USDC}
dispatch={dispatch}
hasBalance={MustBigNumber(usdcBalance).gt(0)}
stringGetter={stringGetter}
withOnboarding
/>
</div>
@@ -169,22 +170,15 @@ export const AccountMenu = () => {
onSelect: onRecoverKeys,
separator: true,
},
dydxAddress && {
value: 'CopyAddress',
icon: <Icon iconName={IconName.Copy} />,
label: stringGetter({ key: STRING_KEYS.COPY_ADDRESS }),
onSelect: async () => await navigator.clipboard.writeText(dydxAddress),
{
value: 'Preferences',
icon: <Icon iconName={IconName.Gear} />,
label: stringGetter({ key: STRING_KEYS.PREFERENCES }),
onSelect: () => dispatch(openDialog({ type: DialogTypes.Preferences })),
},
// {
// value: 'ViewInExplorer',
// icon: <Icon iconName={IconName.Etherscan} />,
// label: stringGetter({ key: STRING_KEYS.OPEN_IN_ETHERSCAN }),
// onSelect: () => globalThis.open(`${mintscanBase}/address/${address}`),
// separator: true,
// },
...(onboardingState === OnboardingState.AccountConnected && hdKey
? [
{
!isMainnet && {
value: 'MobileQrSignIn',
icon: <Icon iconName={IconName.Qr} />,
label: stringGetter({ key: STRING_KEYS.TITLE_SIGN_INTO_MOBILE }),
@@ -192,16 +186,12 @@ export const AccountMenu = () => {
},
{
value: 'MnemonicExport',
icon: (
<span>
<Icon iconName={IconName.ExportKeys} />
</span>
),
icon: <Icon iconName={IconName.ExportKeys} />,
label: <span>{stringGetter({ key: STRING_KEYS.EXPORT_SECRET_PHRASE })}</span>,
highlightColor: 'negative',
onSelect: () => dispatch(openDialog({ type: DialogTypes.MnemonicExport })),
},
]
].filter(isTruthy)
: []),
{
value: 'Disconnect',
@@ -230,38 +220,48 @@ const AssetActions = memo(
dispatch,
withOnboarding,
hasBalance,
stringGetter,
}: {
asset: DydxChainAsset;
dispatch: Dispatch;
withOnboarding?: boolean;
hasBalance?: boolean;
stringGetter: StringGetterFunction;
}) => (
<Styled.InlineRow>
{[
withOnboarding && {
dialogType: DialogTypes.Deposit,
iconName: IconName.Deposit,
tooltipStringKey: STRING_KEYS.DEPOSIT,
},
withOnboarding &&
hasBalance && {
dialogType: DialogTypes.Withdraw,
iconName: IconName.Withdraw,
tooltipStringKey: STRING_KEYS.WITHDRAW,
},
withOnboarding && {
dialogType: DialogTypes.Deposit,
iconName: IconName.Deposit,
},
hasBalance && {
dialogType: DialogTypes.Transfer,
dialogProps: { selectedAsset: asset },
iconName: IconName.Send,
tooltipStringKey: STRING_KEYS.TRANSFER,
},
]
.filter(isTruthy)
.map(({ iconName, dialogType, dialogProps }) => (
<Styled.IconButton
key={dialogType}
action={ButtonAction.Base}
shape={ButtonShape.Square}
iconName={iconName}
onClick={() => dispatch(openDialog({ type: dialogType, dialogProps }))}
/>
.map(({ iconName, tooltipStringKey, dialogType, dialogProps }) => (
<Styled.WithTooltip
key={tooltipStringKey}
tooltipString={stringGetter({ key: tooltipStringKey })}
>
<Styled.IconButton
key={dialogType}
action={ButtonAction.Base}
shape={ButtonShape.Square}
iconName={iconName}
onClick={() => dispatch(openDialog({ type: dialogType, dialogProps }))}
/>
</Styled.WithTooltip>
))}
</Styled.InlineRow>
)
@@ -333,13 +333,12 @@ Styled.label = styled.div`
Styled.Balances = styled.div`
${layoutMixins.flexColumn}
gap: 2px;
> div {
${layoutMixins.spacedRow}
box-shadow: 0 0 0 1px var(--color-border);
gap: 0.5rem;
padding: 0.5rem 1rem;
padding: 0.625rem 1rem;
background-color: var(--color-layer-4);
@@ -384,6 +383,22 @@ Styled.ConnectToChain = styled(Styled.Column)`
}
`;
Styled.IconButton = styled(IconButton)`
Styled.IconButton = styled(IconButton)<{ iconName: IconName }>`
--button-padding: 0 0.25rem;
--button-border: solid var(--border-width) var(--color-layer-6);
${({ iconName }) =>
[IconName.Withdraw, IconName.Deposit].includes(iconName) &&
css`
--button-icon-size: 1.375em;
`}
`;
Styled.CopyButton = styled(CopyButton)`
--button-padding: 0 0.25rem;
--button-border: solid var(--border-width) var(--color-layer-6);
`;
Styled.WithTooltip = styled(WithTooltip)`
--tooltip-backgroundColor: var(--color-layer-5);
`;
+37 -60
View File
@@ -3,18 +3,17 @@ import styled from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
import { groupBy } from 'lodash';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { type Notification, NotificationStatus } from '@/constants/notifications';
import { useBreakpoints } from '@/hooks';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { useNotifications } from '@/hooks/useNotifications';
import { CloseIcon } from '@/icons';
import { Button } from '@/components/Button';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu';
import { DialogPlacement } from '@/components/Dialog';
import { Output, OutputType } from '@/components/Output';
import { IconButton } from '@/components/IconButton';
import { Notification as NotificationCard } from '@/components/Notification';
import { Toolbar } from '@/components/Toolbar';
type ElementProps = {
@@ -27,13 +26,14 @@ export const NotificationsMenu = ({
placement = DialogPlacement.Sidebar,
}: ElementProps) => {
const { isTablet } = useBreakpoints();
const stringGetter = useStringGetter();
const {
notifications,
getDisplayData,
getKey,
markSeen,
markCleared,
markAllCleared,
onNotificationAction,
@@ -65,14 +65,12 @@ export const NotificationsMenu = ({
const items: Parameters<typeof ComboboxDialogMenu>[0]['items'] = useMemo(
() =>
(Object.entries(notificationsByStatus) as unknown as [NotificationStatus, Notification[]][])
// .filter(([status]) => status !== NotificationStatus.Cleared)
.filter(([status]) => status < NotificationStatus.Cleared)
.map(([status, notifications]) => ({
group: status,
groupLabel: {
[NotificationStatus.Triggered]: 'New',
// [NotificationStatus.Updated]: 'Updates',
[NotificationStatus.Triggered]: stringGetter({ key: STRING_KEYS.NEW }),
[NotificationStatus.Seen]: 'Seen',
[NotificationStatus.Cleared]: 'Archived',
}[status as number],
items: notifications
@@ -83,59 +81,40 @@ export const NotificationsMenu = ({
)
.map((notification) => ({
notification,
key: getKey(notification),
displayData: getDisplayData(notification),
key: getKey?.(notification),
displayData: getDisplayData?.(notification),
}))
.map(({ notification, key, displayData }) => ({
value: key,
label: displayData.title ?? '',
description: displayData.customMenuContent || displayData.description,
slotBefore: !displayData.customMenuContent && displayData.icon,
slotAfter: !displayData.customMenuContent && (
<>
<$Output
type={OutputType.RelativeTime}
value={
notification.timestamps[NotificationStatus.Updated] ||
notification.timestamps[NotificationStatus.Triggered]
}
/>
{notification.status < NotificationStatus.Seen ? <$UnreadIndicator /> : null}
{notification.status < NotificationStatus.Cleared ? (
<$IconButton
iconComponent={CloseIcon}
onClick={(e) => {
e.stopPropagation();
/*if (notification.status < NotificationStatus.Seen) {
markSeen(notification);
} else*/ if (notification.status < NotificationStatus.Cleared) {
markCleared(notification);
}
}}
/>
) : null}
</>
label: displayData.title,
description: displayData.body,
slotCustomContent: displayData.renderCustomBody?.({ notification }) ?? (
<NotificationCard
isToast={false}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.body}
notification={notification}
/>
),
disabled: notification.status === NotificationStatus.Cleared,
onSelect: () => {
onNotificationAction(notification);
markSeen(notification);
onNotificationAction?.(notification);
markSeen?.(notification);
},
})),
}))
.filter(({ items }) => items.length),
[notificationsByStatus, getDisplayData, onNotificationAction, markSeen]
[notificationsByStatus, getDisplayData, onNotificationAction, markSeen, stringGetter]
);
return (
<ComboboxDialogMenu
<$ComboboxDialogMenu
withItemBorders
isOpen={isMenuOpen || placement === DialogPlacement.Inline}
setIsOpen={setIsMenuOpen}
items={items}
title="Notifications"
title={stringGetter({ key: STRING_KEYS.NOTIFICATIONS })}
slotTrigger={
<$TriggerContainer>
{slotTrigger}
@@ -150,11 +129,11 @@ export const NotificationsMenu = ({
state={{ isLoading: isEnablingPush }}
onClick={enablePush}
>
Enable Push Notifications
{stringGetter({ key: STRING_KEYS.ENABLE_PUSH_NOTIFICATIONS })}
</Button>
) : (
<Button size={ButtonSize.Small} action={ButtonAction.Secondary} onClick={disablePush}>
Disable Push Notifications
{stringGetter({ key: STRING_KEYS.DISABLE_PUSH_NOTIFICATIONS })}
</Button>
)}
@@ -168,7 +147,7 @@ export const NotificationsMenu = ({
),
}}
>
Clear All
{stringGetter({ key: STRING_KEYS.CLEAR_ALL })}
</Button>
</$FooterToolbar>
}
@@ -178,6 +157,14 @@ export const NotificationsMenu = ({
);
};
const $ComboboxDialogMenu = styled(ComboboxDialogMenu)`
--comboboxDialogMenu-item-padding: 0;
[cmdk-list] > [cmdk-list-sizer] > * {
box-shadow: none;
}
`;
const $UnreadIndicator = styled.div`
width: 0.5rem;
height: 0.5rem;
@@ -198,16 +185,6 @@ const $TriggerUnreadIndicator = styled($UnreadIndicator)`
top: -0.325rem;
`;
const $Output = styled(Output)`
color: var(--color-text-0);
`;
const $IconButton = styled(IconButton)`
--button-border: none;
--button-textColor: var(--color-text-0);
--button-hover-textColor: var(--color-text-1);
`;
const $FooterToolbar = styled(Toolbar)`
display: flex;
flex-wrap: wrap;
@@ -0,0 +1,127 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { shallowEqual, useSelector } from 'react-redux';
import {
AbacusOrderStatus,
AbacusOrderType,
KotlinIrEnumValues,
ORDER_STATUS_STRINGS,
TRADE_TYPES,
} from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade';
import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { AssetIcon } from '@/components/AssetIcon';
import { Details } from '@/components/Details';
import { Notification, NotificationProps } from '@/components/Notification';
import { OrderSideTag } from '@/components/OrderSideTag';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { Output, OutputType } from '@/components/Output';
import { getMarketData } from '@/state/perpetualsSelectors';
type ElementProps = {
data: {
AMOUNT: string;
ASSET: string;
AVERAGE_PRICE: string;
FILLED_AMOUNT: string;
MARKET: string;
ORDER_STATUS: string;
ORDER_TYPE: string;
ORDER_TYPE_TEXT: string;
PRICE: string;
SIDE: string;
};
};
export type TradeNotificationProps = NotificationProps & ElementProps;
export const TradeNotification = ({ isToast, data, notification }: TradeNotificationProps) => {
const stringGetter = useStringGetter();
const { AVERAGE_PRICE, FILLED_AMOUNT, MARKET, ORDER_TYPE, ORDER_STATUS, SIDE } = data;
const marketData = useSelector(getMarketData(MARKET), shallowEqual);
const { assetId } = marketData ?? {};
const orderType = ORDER_TYPE as KotlinIrEnumValues<typeof AbacusOrderType>;
const tradeType = TRADE_TYPES[orderType];
const titleKey = tradeType && TRADE_TYPE_STRINGS[tradeType]?.tradeTypeKey;
const orderStatus = ORDER_STATUS as KotlinIrEnumValues<typeof AbacusOrderStatus>;
return (
<Notification
isToast={isToast}
notification={notification}
slotIcon={<AssetIcon symbol={assetId} />}
slotTitle={titleKey && stringGetter({ key: titleKey })}
slotTitleRight={
<Styled.OrderStatus>
{stringGetter({ key: ORDER_STATUS_STRINGS[orderStatus] })}
<Styled.OrderStatusIcon status={orderStatus} totalFilled={parseFloat(FILLED_AMOUNT)} />
</Styled.OrderStatus>
}
slotCustomContent={
<Styled.Details
items={[
{
key: 'size',
label: (
<Styled.Label>
{stringGetter({ key: STRING_KEYS.SIZE })}
<OrderSideTag
orderSide={SIDE === STRING_KEYS.BUY ? OrderSide.BUY : OrderSide.SELL}
/>
</Styled.Label>
),
value: (
<Output type={OutputType.Asset} value={FILLED_AMOUNT} tag={marketData?.assetId} />
),
},
{
key: 'price',
label: stringGetter({ key: STRING_KEYS.PRICE }),
value:
ORDER_TYPE === TradeTypes.MARKET ? (
<span>{stringGetter({ key: STRING_KEYS.MARKET_ORDER_SHORT })}</span>
) : (
<Output
type={OutputType.Fiat}
value={AVERAGE_PRICE}
fractionDigits={marketData?.configs?.displayTickSizeDecimals}
/>
),
},
]}
/>
}
/>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Label = styled.span`
${layoutMixins.row}
gap: 0.5ch;
`;
Styled.OrderStatus = styled(Styled.Label)`
color: var(--color-text-0);
font: var(--font-small-book);
`;
Styled.OrderStatusIcon = styled(OrderStatusIcon)`
width: 0.9375rem;
height: 0.9375rem;
`;
Styled.Details = styled(Details)`
--details-item-height: 1.5rem;
dd {
color: var(--color-text-2);
}
`;
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import { StatusResponse } from '@0xsquid/sdk';
import { useStringGetter, useSelectedNetwork, useURLConfigs } from '@/hooks';
@@ -18,6 +18,10 @@ type ElementProps = {
type: 'withdrawal' | 'deposit';
};
type StyleProps = {
className?: string;
};
enum TransferStatusStep {
FromChain,
Bridge,
@@ -25,7 +29,7 @@ enum TransferStatusStep {
Complete,
}
export const TransferStatusSteps = ({ status, type }: ElementProps) => {
export const TransferStatusSteps = ({ className, status, type }: ElementProps & StyleProps) => {
const stringGetter = useStringGetter();
const { selectedNetwork } = useSelectedNetwork();
const { mintscan: mintscanTxUrl } = useURLConfigs();
@@ -101,7 +105,7 @@ export const TransferStatusSteps = ({ status, type }: ElementProps) => {
if (!status) return <LoadingDots size={3} />;
return (
<Styled.BridgingStatus>
<Styled.BridgingStatus className={className}>
{steps.map((step) => (
<Styled.Step key={step.step}>
<Styled.row>
@@ -0,0 +1,225 @@
import { useCallback, useState, useMemo, MouseEvent } from 'react';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import { useInterval, useStringGetter } from '@/hooks';
import { AlertType } from '@/constants/alerts';
import { STRING_KEYS } from '@/constants/localization';
import { TransferNotifcation } from '@/constants/notifications';
import { formatSeconds } from '@/lib/timeUtils';
import { AlertMessage } from '@/components/AlertMessage';
import { Collapsible } from '@/components/Collapsible';
import { Icon, IconName } from '@/components/Icon';
import { LoadingDots } from '@/components/Loading/LoadingDots';
import { Notification, NotificationProps } from '@/components/Notification';
import { Output, OutputType } from '@/components/Output';
import { WithReceipt } from '@/components/WithReceipt';
import { layoutMixins } from '@/styles/layoutMixins';
import { TransferStatusSteps } from './TransferStatusSteps';
type ElementProps = {
type: 'withdrawal' | 'deposit';
transfer: TransferNotifcation;
triggeredAt?: number;
};
export const TransferStatusNotification = ({
isToast,
notification,
slotIcon,
slotTitle,
slotDescription,
transfer,
type,
triggeredAt = Date.now(),
}: ElementProps & NotificationProps) => {
const stringGetter = useStringGetter();
const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number>(0);
const { fromChainId, status, txHash, toAmount } = transfer;
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000;
setSecondsLeft(Math.floor((triggeredAt + fromChainEta + toChainEta - Date.now()) / 1000));
}, [status]);
useInterval({ callback: updateSecondsLeft });
const inProgressStatusString =
type === 'deposit'
? secondsLeft > 0
? STRING_KEYS.DEPOSIT_STATUS
: STRING_KEYS.DEPOSIT_STATUS_SHORTLY
: secondsLeft > 0
? STRING_KEYS.WITHDRAW_STATUS
: STRING_KEYS.WITHDRAW_STATUS_SHORTLY;
const statusString =
type === 'deposit'
? status?.squidTransactionStatus === 'success'
? STRING_KEYS.DEPOSIT_COMPLETE
: inProgressStatusString
: status?.squidTransactionStatus === 'success'
? STRING_KEYS.WITHDRAW_COMPLETE
: inProgressStatusString;
const content = (
<>
<Styled.Status>
{stringGetter({
key: statusString,
params: {
AMOUNT_USD: <Styled.InlineOutput type={OutputType.Fiat} value={toAmount} />,
ESTIMATED_DURATION: (
<Styled.InlineOutput
type={OutputType.Text}
value={formatSeconds(Math.max(secondsLeft || 0, 0))}
/>
),
},
})}
</Styled.Status>
{error && (
<AlertMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
})}
</AlertMessage>
)}
</>
);
const transferNotif = (
<Notification
isToast={isToast}
notification={notification}
slotIcon={isToast && slotIcon}
slotTitle={slotTitle}
slotCustomContent={
!status ? (
<LoadingDots size={3} />
) : (
<Styled.BridgingStatus>
{content}
{!isToast && status?.squidTransactionStatus !== 'success' && (
<Styled.TransferStatusSteps status={status} type={type} />
)}
</Styled.BridgingStatus>
)
}
slotAction={
isToast &&
status &&
!error && (
<Styled.Trigger
isOpen={open}
onClick={(e: MouseEvent) => {
e.stopPropagation();
setOpen(!open);
}}
>
<Icon iconName={IconName.Caret} />{' '}
{stringGetter({
key: open ? STRING_KEYS.HIDE_DETAILS : STRING_KEYS.VIEW_DETAILS,
})}
</Styled.Trigger>
)
}
withClose={false}
/>
);
return isToast ? (
<WithReceipt
hideReceipt={!open}
side="bottom"
slotReceipt={
<Collapsible open={open} onOpenChange={setOpen} label="" withTrigger={false}>
<Styled.Receipt>
<TransferStatusSteps status={status} type={type} />
</Styled.Receipt>
</Collapsible>
}
>
{transferNotif}
</WithReceipt>
) : (
transferNotif
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.BridgingStatus = styled.div`
${layoutMixins.flexColumn};
gap: 0.5rem;
`;
Styled.Status = styled.div<{ withMarginBottom?: boolean }>`
color: var(--color-text-0);
font-size: 0.875rem;
${({ withMarginBottom }) =>
withMarginBottom &&
css`
margin-bottom: 0.5rem;
`}
`;
Styled.InlineOutput = styled(Output)`
display: inline-block;
color: var(--color-text-1);
`;
Styled.Step = styled.div`
${layoutMixins.row};
gap: 0.5rem;
`;
Styled.TransferStatusSteps = styled(TransferStatusSteps)`
padding: 0.5rem 0 0;
`;
Styled.Trigger = styled.button<{ isOpen?: boolean }>`
display: flex;
align-items: center;
gap: 0.5em;
color: var(--color-accent);
user-select: none;
cursor: pointer;
&:focus-visible {
outline: none;
text-decoration: underline;
}
svg {
transition: rotate 0.3s var(--ease-out-expo);
}
${({ isOpen }) =>
isOpen &&
css`
& > svg {
rotate: -0.5turn;
}
`}
`;
Styled.Receipt = styled.div`
padding: 0 1rem;
`;
+19 -13
View File
@@ -67,7 +67,8 @@ export const MarketsTable = ({ className }: { className?: string }) => {
<Styled.Output
type={OutputType.Percent}
value={MustBigNumber(priceChange24HPercent).abs()}
isNegative={MustBigNumber(priceChange24H).isNegative()}
isPositive={MustBigNumber(priceChange24HPercent).gt(0)}
isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
/>
</>
)}
@@ -96,19 +97,20 @@ export const MarketsTable = ({ className }: { className?: string }) => {
),
},
{
columnKey: 'priceChange24H',
getCellValue: (row) => row.priceChange24H,
columnKey: 'priceChange24HPercent',
getCellValue: (row) => row.priceChange24HPercent,
label: stringGetter({ key: STRING_KEYS.CHANGE_24H }),
renderCell: ({ priceChange24H, priceChange24HPercent, tickSizeDecimals }) => (
<TableCell stacked>
<Styled.InlineRow>
{!priceChange24H ? (
{!priceChange24HPercent ? (
<Output type={OutputType.Text} value={null} />
) : (
<Styled.Output
type={OutputType.Percent}
value={MustBigNumber(priceChange24HPercent).abs()}
isNegative={MustBigNumber(priceChange24H).isNegative()}
isPositive={MustBigNumber(priceChange24HPercent).gt(0)}
isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
/>
)}
</Styled.InlineRow>
@@ -128,26 +130,25 @@ export const MarketsTable = ({ className }: { className?: string }) => {
<Styled.Output
type={OutputType.SmallPercent}
value={row.nextFundingRate}
isPositive={MustBigNumber(row.nextFundingRate).gt(0)}
isNegative={MustBigNumber(row.nextFundingRate).isNegative()}
/>
),
},
{
columnKey: 'openInterest',
getCellValue: (row) => row.openInterest,
getCellValue: (row) => row.openInterestUSDC,
label: stringGetter({ key: STRING_KEYS.OPEN_INTEREST }),
renderCell: (row) => (
<TableCell stacked>
<Output type={OutputType.Fiat} value={row.openInterestUSDC} />
<Output
fractionDigits={LARGE_TOKEN_DECIMALS}
type={OutputType.Number}
value={row.openInterest}
tag={row.asset?.id}
/>
<Output
type={OutputType.Fiat}
value={MustBigNumber(row.openInterest).times(MustBigNumber(row.oraclePrice))}
/>
</TableCell>
),
},
@@ -155,7 +156,7 @@ export const MarketsTable = ({ className }: { className?: string }) => {
columnKey: 'volume24H',
getCellValue: (row) => row.volume24H,
label: stringGetter({ key: STRING_KEYS.VOLUME_24H }),
renderCell: (row) => <Output type={OutputType.Number} value={row.volume24H} />,
renderCell: (row) => <Output type={OutputType.Fiat} value={row.volume24H} />,
},
{
columnKey: 'trades24H',
@@ -241,6 +242,11 @@ Styled.TabletPriceChange = styled(Styled.InlineRow)`
font: var(--font-small-book);
`;
Styled.Output = styled(Output)<{ isNegative?: boolean }>`
color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)};
Styled.Output = styled(Output)<{ isNegative?: boolean; isPositive?: boolean }>`
color: ${({ isNegative, isPositive }) =>
isNegative
? `var(--color-negative)`
: isPositive
? `var(--color-positive)`
: `var(--color-text-1)`};
`;
+22 -20
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client-js';
@@ -9,13 +9,13 @@ import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { calculateCanViewAccount } from '@/state/accountCalculators';
import { setTradeFormInputs } from '@/state/inputs';
import { getSubaccountOpenOrdersBySideAndPrice } from '@/state/accountSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getCurrentMarketConfig, getCurrentMarketOrderbook } from '@/state/perpetualsSelectors';
import { getCurrentInput } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { Details } from '@/components/Details';
@@ -208,7 +208,6 @@ const OrderbookTable = ({
// Style props
histogramSide={histogramSide}
hideHeader={hideHeader}
withFocusStickyRows
style={{
'--histogram-range': histogramRange,
}}
@@ -223,11 +222,12 @@ export const Orderbook = ({
maxRowsPerSide = ORDERBOOK_MAX_ROWS_PER_SIDE,
hideHeader = false,
}: ElementProps & StyleProps) => {
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { isTablet } = useBreakpoints();
const currentInput = useSelector(getCurrentInput);
const { symbol = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {};
const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {};
const { stepSizeDecimals, tickSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) ?? {};
@@ -274,18 +274,16 @@ export const Orderbook = ({
const onRowAction = useCallback(
(key: string, row: RowData) => {
if (currentInput === 'trade' && key !== 'spread' && row?.price)
abacusStateManager.setTradeValue({
value: row?.price,
field: TradeInputField.limitPrice,
});
if (currentInput === 'trade' && key !== 'spread' && row?.price) {
dispatch(setTradeFormInputs({ limitPriceInput: row?.price?.toString() }));
}
},
[currentInput]
);
const orderbookTableProps = {
showMineColumn,
symbol,
symbol: id,
stepSizeDecimals,
tickSizeDecimals,
histogramRange,
@@ -510,17 +508,21 @@ Styled.OrderbookTable = styled(OrderbookTradesTable)<StyleProps>`
content: '';
}
&:not(:active):is(:focus-visible, :focus-within) {
${orderbookMixins.scrollSnapItem}
z-index: 2;
${({ withFocusStickyRows }) =>
withFocusStickyRows &&
css`
&:not(:active):is(:focus-visible, :focus-within) {
${orderbookMixins.scrollSnapItem}
z-index: 2;
&[data-side='bid'] {
top: calc(var(--stickyArea-totalInsetTop) + var(--orderbook-spreadRowHeight));
}
&[data-side='ask'] {
bottom: calc(var(--stickyArea-totalInsetBottom) + var(--orderbook-spreadRowHeight));
}
}
&[data-side='bid'] {
top: calc(var(--stickyArea-totalInsetTop) + var(--orderbook-spreadRowHeight));
}
&[data-side='ask'] {
bottom: calc(var(--stickyArea-totalInsetBottom) + var(--orderbook-spreadRowHeight));
}
}
`}
}
${Styled.HorizontalLayout} & {
+2 -2
View File
@@ -81,10 +81,10 @@ const getTransferHistoryTableColumnDef = ({
),
renderCell: ({ fromAddress, toAddress }) => (
<TableCell stacked>
<CopyButton shownAsText value={fromAddress ?? undefined}>
<CopyButton buttonType="text" value={fromAddress ?? undefined}>
{fromAddress ? truncateAddress(fromAddress) : '-'}
</CopyButton>{' '}
<CopyButton shownAsText value={toAddress ?? undefined}>
<CopyButton buttonType="text" value={toAddress ?? undefined}>
{toAddress ? truncateAddress(toAddress) : '-'}
</CopyButton>
</TableCell>