Compare commits

..
Author SHA1 Message Date
Bill He 514a2d3ab3 use decimals: 2023-11-06 17:35:57 -08:00
Bill He c58d94e477 Fix decimal overflow bug 2023-11-06 17:26:27 -08:00
105 changed files with 1456 additions and 3719 deletions
-5
View File
@@ -98,11 +98,6 @@ For the "Build & Development Settings", we recommend the following:
- Framework Preset: `Vite` - Framework Preset: `Vite`
- Build Command (override): `pnpm run build` - Build Command (override): `pnpm run build`
By default, the dev server runs in development mode and the build command runs in production mode. To override the default mode, you can pass in the `--mode` option flag. For example, if you want to build your app for testnet:
```
pnpm run build --mode testnet
```
If you wish to incorporate analytics via Amplitude and Bugsnag, you can use our scripts: If you wish to incorporate analytics via Amplitude and Bugsnag, you can use our scripts:
`pnpm run build:inject-amplitude` and `pnpm run build:inject-bugsnag`. You will need to provide your own API keys for these services. In the Environment Variables section, name the variables as `AMPLITUDE_API_KEY` and `BUGSNAG_API_KEY` and provide the respective keys as their values. `pnpm run build:inject-amplitude` and `pnpm run build:inject-bugsnag`. You will need to provide your own API keys for these services. In the Environment Variables section, name the variables as `AMPLITUDE_API_KEY` and `BUGSNAG_API_KEY` and provide the respective keys as their values.
+3 -6
View File
@@ -39,16 +39,14 @@
"@cosmjs/proto-signing": "^0.31.0", "@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0", "@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.0.24", "@dydxprotocol/v4-abacus": "^1.0.10",
"@dydxprotocol/v4-client-js": "^1.0.0", "@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.5", "@dydxprotocol/v4-localization": "^1.0.0",
"@ethersproject/providers": "^5.7.2", "@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3", "@js-joda/core": "^5.5.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5", "@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-navigation-menu": "^1.1.3",
"@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.1.3",
@@ -98,7 +96,6 @@
"long": "^5.2.3", "long": "^5.2.3",
"luxon": "^3.3.0", "luxon": "^3.3.0",
"qr-code-styling": "1.6.0-rc.1", "qr-code-styling": "1.6.0-rc.1",
"query-string": "^8.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-aria": "^3.25.0", "react-aria": "^3.25.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -111,7 +108,7 @@
"styled-components": "^5.3.11", "styled-components": "^5.3.11",
"use-latest": "^1.2.1", "use-latest": "^1.2.1",
"viem": "^1.12.2", "viem": "^1.12.2",
"wagmi": "^1.4.5" "wagmi": "^1.3.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.5", "@babel/core": "^7.22.5",
+85 -125
View File
@@ -1,5 +1,9 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies: dependencies:
'@0xsquid/sdk': '@0xsquid/sdk':
specifier: ^1.10.0 specifier: ^1.10.0
@@ -23,23 +27,20 @@ dependencies:
specifier: ^0.31.0 specifier: ^0.31.0
version: 0.31.0 version: 0.31.0
'@dydxprotocol/v4-abacus': '@dydxprotocol/v4-abacus':
specifier: ^1.0.24 specifier: ^1.0.10
version: 1.0.24 version: 1.0.10
'@dydxprotocol/v4-client-js': '@dydxprotocol/v4-client-js':
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
'@dydxprotocol/v4-localization': '@dydxprotocol/v4-localization':
specifier: ^1.0.5 specifier: ^1.0.0
version: 1.0.5 version: 1.0.0
'@ethersproject/providers': '@ethersproject/providers':
specifier: ^5.7.2 specifier: ^5.7.2
version: 5.7.2 version: 5.7.2
'@js-joda/core': '@js-joda/core':
specifier: ^5.5.3 specifier: ^5.5.3
version: 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': '@radix-ui/react-collapsible':
specifier: ^1.0.3 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) version: 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -49,9 +50,6 @@ dependencies:
'@radix-ui/react-dropdown-menu': '@radix-ui/react-dropdown-menu':
specifier: ^2.0.5 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) 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': '@radix-ui/react-navigation-menu':
specifier: ^1.1.3 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) version: 1.1.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -199,9 +197,6 @@ dependencies:
qr-code-styling: qr-code-styling:
specifier: 1.6.0-rc.1 specifier: 1.6.0-rc.1
version: 1.6.0-rc.1 version: 1.6.0-rc.1
query-string:
specifier: ^8.1.0
version: 8.1.0
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
@@ -239,8 +234,8 @@ dependencies:
specifier: ^1.12.2 specifier: ^1.12.2
version: 1.12.2(typescript@5.1.3) version: 1.12.2(typescript@5.1.3)
wagmi: wagmi:
specifier: ^1.4.5 specifier: ^1.3.7
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) 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)
devDependencies: devDependencies:
'@babel/core': '@babel/core':
@@ -984,8 +979,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true dev: true
/@dydxprotocol/v4-abacus@1.0.24: /@dydxprotocol/v4-abacus@1.0.10:
resolution: {integrity: sha512-wDGSjkrc3Se6Ev7UTjPgJV7PiyzZSz2mJwOTZikLwH7W3k1iPYGQmaCd7TudFk8h2aSdlEwNsQBCf0sLoyvHaQ==} resolution: {integrity: sha512-SdxI/JcyxM1ydtR9ImWujeurea3gbxj37qYnw1wz829BQiZ05uNEtll3xJCI0MjpG+01ARsxVys7SHbpjSxwOg==}
dev: false dev: false
/@dydxprotocol/v4-client-js@1.0.0: /@dydxprotocol/v4-client-js@1.0.0:
@@ -1016,8 +1011,8 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@dydxprotocol/v4-localization@1.0.5: /@dydxprotocol/v4-localization@1.0.0:
resolution: {integrity: sha512-zZm3GFp/ORz9YjigQFVBlcWmaxYY+XEdIw8AWN5nEbsuiA2Jay+/OA7lO9+nV1J1p1T+TgLo9iczIHJsAo3IsA==} resolution: {integrity: sha512-U7QjOlzpfSAVEchkNDB3xYRx7okzdeYi83XsnHwI+EHa2quFm/2Y2dhrBkT5fqpZmhcwxb/vG8AZmFSToOf9hA==}
dev: false dev: false
/@dydxprotocol/v4-proto@0.4.1: /@dydxprotocol/v4-proto@0.4.1:
@@ -2183,34 +2178,6 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false 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): /@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==} resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies: peerDependencies:
@@ -2512,14 +2479,6 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false 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): /@radix-ui/react-id@1.0.0(react@18.2.0):
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==} resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
peerDependencies: peerDependencies:
@@ -5975,12 +5934,26 @@ packages:
pretty-format: 27.5.1 pretty-format: 27.5.1
dev: true dev: true
/@wagmi/connectors@3.1.3(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2): /@wagmi/chains@1.4.0(typescript@5.1.3):
resolution: {integrity: sha512-UgwsQKQDFObJVJMf9pDfFoXTv710o4zrTHyhIWKBTMMkLpCMsMxN5+ZaDhBYt/BgoRinfRYQo8uwuwLhxE6Log==} resolution: {integrity: sha512-9HwJrhcZ1TxyrCbE10y7s1eSiSiyfGam7AHIOLYExaOX+vpOZ+MNTt4orFEDbEpz1fxwJDPPI38lanAUix1OSA==}
peerDependencies: peerDependencies:
typescript: '>=5.0.4' 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' viem: '>=0.3.35'
peerDependenciesMeta: peerDependenciesMeta:
'@wagmi/chains':
optional: true
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
@@ -5988,17 +5961,17 @@ packages:
'@ledgerhq/connect-kit-loader': 1.1.2 '@ledgerhq/connect-kit-loader': 1.1.2
'@safe-global/safe-apps-provider': 0.17.1(typescript@5.1.3) '@safe-global/safe-apps-provider': 0.17.1(typescript@5.1.3)
'@safe-global/safe-apps-sdk': 8.1.0(typescript@5.1.3) '@safe-global/safe-apps-sdk': 8.1.0(typescript@5.1.3)
'@walletconnect/ethereum-provider': 2.10.2(@walletconnect/modal@2.6.2) '@wagmi/chains': 1.4.0(typescript@5.1.3)
'@walletconnect/ethereum-provider': 2.8.6(@walletconnect/modal@2.5.9)
'@walletconnect/legacy-provider': 2.0.0 '@walletconnect/legacy-provider': 2.0.0
'@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0) '@walletconnect/modal': 2.5.9(react@18.2.0)
'@walletconnect/utils': 2.10.2 '@walletconnect/utils': 2.8.6
abitype: 0.8.7(typescript@5.1.3) abitype: 0.8.7(typescript@5.1.3)
eventemitter3: 4.0.7 eventemitter3: 4.0.7
typescript: 5.1.3 typescript: 5.1.3
viem: 1.12.2(typescript@5.1.3) viem: 1.12.2(typescript@5.1.3)
transitivePeerDependencies: transitivePeerDependencies:
- '@react-native-async-storage/async-storage' - '@react-native-async-storage/async-storage'
- '@types/react'
- bufferutil - bufferutil
- encoding - encoding
- lokijs - lokijs
@@ -6008,8 +5981,8 @@ packages:
- zod - zod
dev: false dev: false
/@wagmi/core@1.4.5(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2): /@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-N9luRb1Uk4tBN9kaYcQSWKE9AsRt/rvZaFt5IZech4JPzNN2sQlfhKd9GEjOXYRDqEPHdDvos7qyBKiDNTz4GA==} resolution: {integrity: sha512-TXv9ZlRR5aySfERFuWMuo+lKXC/CoqtxVJZVHPqhK1jY+nldMx3AvrWzzF4CccRaMYcVdvPFepvmxzq2A2VvWg==}
peerDependencies: peerDependencies:
typescript: '>=5.0.4' typescript: '>=5.0.4'
viem: '>=0.3.35' viem: '>=0.3.35'
@@ -6017,7 +5990,8 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@wagmi/connectors': 3.1.3(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2) '@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)
abitype: 0.8.7(typescript@5.1.3) abitype: 0.8.7(typescript@5.1.3)
eventemitter3: 4.0.7 eventemitter3: 4.0.7
typescript: 5.1.3 typescript: 5.1.3
@@ -6062,8 +6036,8 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@walletconnect/core@2.10.2: /@walletconnect/core@2.8.6:
resolution: {integrity: sha512-JQz/xp3SLEpTeRQctdck2ugSBVEpMxoSE+lFi2voJkZop1hv6P+uqr6E4PzjFluAjeAnKlT1xvra0aFWjPWVcw==} resolution: {integrity: sha512-rnSqm1KJLcww/v6+UH8JeibQkJ3EKgyUDPfEK0stSEkrIUIcXaFlq3Et8S+vgV8bPhI0MVUhAhFL5OJZ3t2ryg==}
dependencies: dependencies:
'@walletconnect/heartbeat': 1.2.1 '@walletconnect/heartbeat': 1.2.1
'@walletconnect/jsonrpc-provider': 1.0.13 '@walletconnect/jsonrpc-provider': 1.0.13
@@ -6076,8 +6050,8 @@ packages:
'@walletconnect/relay-auth': 1.0.4 '@walletconnect/relay-auth': 1.0.4
'@walletconnect/safe-json': 1.0.2 '@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2 '@walletconnect/time': 1.0.2
'@walletconnect/types': 2.10.2 '@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.10.2 '@walletconnect/utils': 2.8.6
events: 3.3.0 events: 3.3.0
lodash.isequal: 4.5.0 lodash.isequal: 4.5.0
uint8arrays: 3.1.1 uint8arrays: 3.1.1
@@ -6113,8 +6087,8 @@ packages:
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@walletconnect/ethereum-provider@2.10.2(@walletconnect/modal@2.6.2): /@walletconnect/ethereum-provider@2.8.6(@walletconnect/modal@2.5.9):
resolution: {integrity: sha512-QMYFZ6+rVq2CJLdIPdKK0j1Qm66UA27oQU5V2SrL8EVwl7wFfm0Bq7fnL+qAWeDpn612dNeNErpk/ROa1zWlWg==} resolution: {integrity: sha512-wUvJEsXTLmMihrOhQxAs1k9hrWEOT03QBn54P9r9GpJbJ1zEfIjQaXFfi8uup6gldhH+vN38PsbOiLyv/6d3qQ==}
peerDependencies: peerDependencies:
'@walletconnect/modal': '>=2' '@walletconnect/modal': '>=2'
peerDependenciesMeta: peerDependenciesMeta:
@@ -6125,11 +6099,11 @@ packages:
'@walletconnect/jsonrpc-provider': 1.0.13 '@walletconnect/jsonrpc-provider': 1.0.13
'@walletconnect/jsonrpc-types': 1.0.3 '@walletconnect/jsonrpc-types': 1.0.3
'@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0) '@walletconnect/modal': 2.5.9(react@18.2.0)
'@walletconnect/sign-client': 2.10.2 '@walletconnect/sign-client': 2.8.6
'@walletconnect/types': 2.10.2 '@walletconnect/types': 2.8.6
'@walletconnect/universal-provider': 2.10.2 '@walletconnect/universal-provider': 2.8.6
'@walletconnect/utils': 2.10.2 '@walletconnect/utils': 2.8.6
events: 3.3.0 events: 3.3.0
transitivePeerDependencies: transitivePeerDependencies:
- '@react-native-async-storage/async-storage' - '@react-native-async-storage/async-storage'
@@ -6280,34 +6254,32 @@ packages:
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@walletconnect/modal-core@2.6.2(@types/react@18.2.14)(react@18.2.0): /@walletconnect/modal-core@2.5.9(react@18.2.0):
resolution: {integrity: sha512-cv8ibvdOJQv2B+nyxP9IIFdxvQznMz8OOr/oR/AaUZym4hjXNL/l1a2UlSQBXrVjo3xxbouMxLb3kBsHoYP2CA==} resolution: {integrity: sha512-isIebwF9hOknGouhS/Ob4YJ9Sa/tqNYG2v6Ua9EkCqIoLimepkG5eC53tslUWW29SLSfQ9qqBNG2+iE7yQXqgw==}
dependencies: dependencies:
valtio: 1.11.2(@types/react@18.2.14)(react@18.2.0) buffer: 6.0.3
valtio: 1.10.6(react@18.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react'
- react - react
dev: false dev: false
/@walletconnect/modal-ui@2.6.2(@types/react@18.2.14)(react@18.2.0): /@walletconnect/modal-ui@2.5.9(react@18.2.0):
resolution: {integrity: sha512-rbdstM1HPGvr7jprQkyPggX7rP4XiCG85ZA+zWBEX0dVQg8PpAgRUqpeub4xQKDgY7pY/xLRXSiCVdWGqvG2HA==} resolution: {integrity: sha512-nfBaAT9Ls7RZTBBgAq+Nt/3AoUcinIJ9bcq5UHXTV3lOPu/qCKmUC/0HY3GvUK8ykabUAsjr0OAGmcqkB91qug==}
dependencies: dependencies:
'@walletconnect/modal-core': 2.6.2(@types/react@18.2.14)(react@18.2.0) '@walletconnect/modal-core': 2.5.9(react@18.2.0)
lit: 2.8.0 lit: 2.7.5
motion: 10.16.2 motion: 10.16.2
qrcode: 1.5.3 qrcode: 1.5.3
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react'
- react - react
dev: false dev: false
/@walletconnect/modal@2.6.2(@types/react@18.2.14)(react@18.2.0): /@walletconnect/modal@2.5.9(react@18.2.0):
resolution: {integrity: sha512-eFopgKi8AjKf/0U4SemvcYw9zlLpx9njVN8sf6DAkowC2Md0gPU/UNEbH1Wwj407pEKnEds98pKWib1NN1ACoA==} resolution: {integrity: sha512-Zs2RvPwbBNRdBhb50FuJCxi3FJltt1KSpI7odjU/x9GTpTOcSOkmR66PBCy2JvNA0+ztnS1Xs0LVEr3lu7/Jzw==}
dependencies: dependencies:
'@walletconnect/modal-core': 2.6.2(@types/react@18.2.14)(react@18.2.0) '@walletconnect/modal-core': 2.5.9(react@18.2.0)
'@walletconnect/modal-ui': 2.6.2(@types/react@18.2.14)(react@18.2.0) '@walletconnect/modal-ui': 2.5.9(react@18.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react'
- react - react
dev: false dev: false
@@ -6363,17 +6335,17 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@walletconnect/sign-client@2.10.2: /@walletconnect/sign-client@2.8.6:
resolution: {integrity: sha512-vviSLV3f92I0bReX+OLr1HmbH0uIzYEQQFd1MzIfDk9PkfFT/LLAHhUnDaIAMkIdippqDcJia+5QEtT4JihL3Q==} resolution: {integrity: sha512-rOFTKTHP7oJfXgYHX7+SdB8VbcsEE3ZFG/bMdmZboWaBim1mrY3vUyDdKrNr0VgI3AwBiEQezQDfKxBX0pMSQQ==}
dependencies: dependencies:
'@walletconnect/core': 2.10.2 '@walletconnect/core': 2.8.6
'@walletconnect/events': 1.0.1 '@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.1 '@walletconnect/heartbeat': 1.2.1
'@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 2.0.1 '@walletconnect/logger': 2.0.1
'@walletconnect/time': 1.0.2 '@walletconnect/time': 1.0.2
'@walletconnect/types': 2.10.2 '@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.10.2 '@walletconnect/utils': 2.8.6
events: 3.3.0 events: 3.3.0
transitivePeerDependencies: transitivePeerDependencies:
- '@react-native-async-storage/async-storage' - '@react-native-async-storage/async-storage'
@@ -6402,8 +6374,8 @@ packages:
- lokijs - lokijs
dev: false dev: false
/@walletconnect/types@2.10.2: /@walletconnect/types@2.8.6:
resolution: {integrity: sha512-luNV+07Wdla4STi9AejseCQY31tzWKQ5a7C3zZZaRK/di+rFaAAb7YW04OP4klE7tw/mJRGPTlekZElmHxO8kQ==} resolution: {integrity: sha512-Z/PFa3W1XdxeTcCtdR6lUsFgZfU/69wWJBPyclPwn7cu1+eriuCr6XZXQpJjib3flU+HnwHiXeUuqZaheehPxw==}
dependencies: dependencies:
'@walletconnect/events': 1.0.1 '@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.1 '@walletconnect/heartbeat': 1.2.1
@@ -6416,17 +6388,17 @@ packages:
- lokijs - lokijs
dev: false dev: false
/@walletconnect/universal-provider@2.10.2: /@walletconnect/universal-provider@2.8.6:
resolution: {integrity: sha512-wFgI0LbQ3D56sgaUMsgOHCM5m8WLxiC71BGuCKQfApgsbNMVKugYVy2zWHyUyi8sqTQHI+uSaVpDev4UHq9LEw==} resolution: {integrity: sha512-ln1RVv8+oHu9enOJ/oVkjiarneB+4vJCk16znOklIN2JtDHwB8iObDHlQH3UE6ynNTw1iRvaGuPR4g+YdIfB6w==}
dependencies: dependencies:
'@walletconnect/jsonrpc-http-connection': 1.0.7 '@walletconnect/jsonrpc-http-connection': 1.0.7
'@walletconnect/jsonrpc-provider': 1.0.13 '@walletconnect/jsonrpc-provider': 1.0.13
'@walletconnect/jsonrpc-types': 1.0.3 '@walletconnect/jsonrpc-types': 1.0.3
'@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 2.0.1 '@walletconnect/logger': 2.0.1
'@walletconnect/sign-client': 2.10.2 '@walletconnect/sign-client': 2.8.6
'@walletconnect/types': 2.10.2 '@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.10.2 '@walletconnect/utils': 2.8.6
events: 3.3.0 events: 3.3.0
transitivePeerDependencies: transitivePeerDependencies:
- '@react-native-async-storage/async-storage' - '@react-native-async-storage/async-storage'
@@ -6458,8 +6430,8 @@ packages:
- lokijs - lokijs
dev: false dev: false
/@walletconnect/utils@2.10.2: /@walletconnect/utils@2.8.6:
resolution: {integrity: sha512-syxXRpc2yhSknMu3IfiBGobxOY7fLfLTJuw+ppKaeO6WUdZpIit3wfuGOcc0Ms3ZPFCrGfyGOoZsCvgdXtptRg==} resolution: {integrity: sha512-wcy6e5+COYo7tfNnW8YqidnATdJDIW6vDiWWE7A1F78Sl/VflkaevB9cIgyn8eLdxC1SxXgGoeC2oLP90nnHJg==}
dependencies: dependencies:
'@stablelib/chacha20poly1305': 1.0.1 '@stablelib/chacha20poly1305': 1.0.1
'@stablelib/hkdf': 1.0.1 '@stablelib/hkdf': 1.0.1
@@ -6469,7 +6441,7 @@ packages:
'@walletconnect/relay-api': 1.0.9 '@walletconnect/relay-api': 1.0.9
'@walletconnect/safe-json': 1.0.2 '@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2 '@walletconnect/time': 1.0.2
'@walletconnect/types': 2.10.2 '@walletconnect/types': 2.8.6
'@walletconnect/window-getters': 1.0.1 '@walletconnect/window-getters': 1.0.1
'@walletconnect/window-metadata': 1.0.1 '@walletconnect/window-metadata': 1.0.1
detect-browser: 5.3.0 detect-browser: 5.3.0
@@ -7903,6 +7875,7 @@ packages:
/decode-uri-component@0.4.1: /decode-uri-component@0.4.1:
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dev: true
/deep-eql@4.1.3: /deep-eql@4.1.3:
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
@@ -8972,6 +8945,7 @@ packages:
/filter-obj@5.1.0: /filter-obj@5.1.0:
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dev: true
/find-cache-dir@3.3.2: /find-cache-dir@3.3.2:
resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
@@ -10398,14 +10372,6 @@ packages:
lit-html: 2.8.0 lit-html: 2.8.0
dev: false 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: /loader-runner@4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'} engines: {node: '>=6.11.5'}
@@ -11910,6 +11876,7 @@ packages:
decode-uri-component: 0.4.1 decode-uri-component: 0.4.1
filter-obj: 5.1.0 filter-obj: 5.1.0
split-on-first: 3.0.0 split-on-first: 3.0.0
dev: true
/queue-microtask@1.2.3: /queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -12734,6 +12701,7 @@ packages:
/split-on-first@3.0.0: /split-on-first@3.0.0:
resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true
/split2@4.2.0: /split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
@@ -13539,19 +13507,15 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0)
dev: false dev: false
/valtio@1.11.2(@types/react@18.2.14)(react@18.2.0): /valtio@1.10.6(react@18.2.0):
resolution: {integrity: sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw==} resolution: {integrity: sha512-SxN1bHUmdhW6V8qsQTpCgJEwp7uHbntuH0S9cdLQtiohuevwBksbpXjwj5uDMA7bLwg1WKyq9sEpZrx3TIMrkA==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
peerDependencies: peerDependencies:
'@types/react': '>=16.8'
react: '>=16.8' react: '>=16.8'
peerDependenciesMeta: peerDependenciesMeta:
'@types/react':
optional: true
react: react:
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.14
proxy-compare: 2.5.1 proxy-compare: 2.5.1
react: 18.2.0 react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0)
@@ -13774,8 +13738,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/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): /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-Ph62E6cO5n2Z8Z5LTyZrkaNprxTsbC4w0qZJT4OJdXrEELziI8z/b4FO6amVFXdu2rDp/wpvF56e4mhKC8/Kdw==} resolution: {integrity: sha512-n4Qwby0QB8/JSGjc0gGn4MWZ0BT229agEg6saPGS6VswmPjJLdkLq9qxKn1JptXxPREmb/ZvwHxU++3Z+gQJVA==}
peerDependencies: peerDependencies:
react: '>=17.0.0' react: '>=17.0.0'
typescript: '>=5.0.4' typescript: '>=5.0.4'
@@ -13787,7 +13751,7 @@ packages:
'@tanstack/query-sync-storage-persister': 4.32.6 '@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': 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) '@tanstack/react-query-persist-client': 4.32.6(@tanstack/react-query@4.32.6)
'@wagmi/core': 1.4.5(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2) '@wagmi/core': 1.3.6(@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) abitype: 0.8.7(typescript@5.1.3)
react: 18.2.0 react: 18.2.0
typescript: 5.1.3 typescript: 5.1.3
@@ -14136,7 +14100,3 @@ packages:
/zwitch@2.0.4: /zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+2 -74
View File
@@ -1,6 +1,6 @@
{ {
"apps": { "apps": {
"ios": { "iOS": {
"scheme": "dydxv4" "scheme": "dydxv4"
} }
}, },
@@ -400,77 +400,6 @@
"signTypedDataDomainName": "dYdX Chain" "signTypedDataDomainName": "dYdX Chain"
} }
}, },
"dydxprotocol-staging-forced-update": {
"name": "v4 Staging Forced Update",
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"isMainNet": false,
"tokens": {
"chain": {
"name": "Dv4TNT",
"denom": "adv4tnt",
"decimals": 18,
"image": "/currencies/dydx.png"
},
"usdc": {
"name": "USDC",
"denom": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5",
"gasDenom": "uusdc",
"decimals": 6,
"image": "/currencies/usdc.png"
}
},
"endpoints": {
"indexers": [
{
"api": "https://indexer.v4staging.dydx.exchange",
"socket": "wss://indexer.v4staging.dydx.exchange"
}
],
"faucet": "https://faucet.v4staging.dydx.exchange",
"validators": [
"https://validator.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
"privacy": "https://dydx.exchange/privacy",
"mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}",
"mintscanBase": "https://testnet.mintscan.io/dydx-testnet",
"documentation": "https://v4-teacher.vercel.app/",
"community": "https://discord.com/invite/dydx",
"feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform",
"blogs": "https://www.dydx.foundation/blog"
},
"wallets": {
"walletconnect": {
"client": {
"name": "dYdX v4",
"description": "dYdX v4 App",
"iconUrl": "/logos/dydx-x.png"
},
"v2": {
"projectId": "47559b2ec96c09aed9ff2cb54a31ab0e"
}
},
"walletSegue": {
"callbackUrl": "/walletsegue"
},
"images": "/wallets/",
"signTypedDataAction": "dYdX Chain Onboarding",
"signTypedDataDomainName": "dYdX Chain"
},
"apps": {
"ios": {
"minimalVersion": "1.0",
"build":40000,
"url": "https://apps.apple.com/app/dydx/id1564787350"
}
}
},
"dydxprotocol-staging-west": { "dydxprotocol-staging-west": {
"name": "v4 Staging West", "name": "v4 Staging West",
"ethereumChainId": "5", "ethereumChainId": "5",
@@ -964,7 +893,6 @@
"privacy": "https://dydx.exchange/privacy", "privacy": "https://dydx.exchange/privacy",
"statusPage": "https://status.v4testnet.dydx.exchange/", "statusPage": "https://status.v4testnet.dydx.exchange/",
"mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}", "mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}",
"mintscanBase": "https://testnet.mintscan.io/dydx-testnet",
"documentation": "https://docs.dydx.exchange/", "documentation": "https://docs.dydx.exchange/",
"community": "https://discord.com/invite/dydx", "community": "https://discord.com/invite/dydx",
"feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform", "feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform",
@@ -1105,7 +1033,7 @@
"tos": "[HTTP link to TOS]", "tos": "[HTTP link to TOS]",
"privacy": "[HTTP link to Privacy Policy]", "privacy": "[HTTP link to Privacy Policy]",
"mintscan": "[HTTP link to Mintscan, with {tx_hash} placeholder]", "mintscan": "[HTTP link to Mintscan, with {tx_hash} placeholder]",
"mintscanBase": "[HTTP link to TOS mintscan base url]", "mintscanBase": "[HTTP link to TOS mintsacn base url]",
"feedback": "[HTTP link to feedback form, can be null]", "feedback": "[HTTP link to feedback form, can be null]",
"blogs": "[HTTP link to blogs, can be null]", "blogs": "[HTTP link to blogs, can be null]",
"foundation": "[HTTP link to foundation, can be null]", "foundation": "[HTTP link to foundation, can be null]",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

After

Width:  |  Height:  |  Size: 817 B

-2
View File
@@ -23,7 +23,6 @@ import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications';
import { RestrictionProvider } from '@/hooks/useRestrictions'; import { RestrictionProvider } from '@/hooks/useRestrictions';
import { SubaccountProvider } from '@/hooks/useSubaccount'; import { SubaccountProvider } from '@/hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquid'; import { SquidProvider } from '@/hooks/useSquid';
import { TestFlagsProvider } from '@/hooks/useTestFlags';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute'; import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
@@ -124,7 +123,6 @@ const providers = [
wrapProvider(QueryClientProvider, { client: queryClient }), wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider), wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }), wrapProvider(WagmiConfig, { config }),
wrapProvider(TestFlagsProvider),
wrapProvider(LocaleProvider), wrapProvider(LocaleProvider),
wrapProvider(RestrictionProvider), wrapProvider(RestrictionProvider),
wrapProvider(DydxProvider), wrapProvider(DydxProvider),
+1 -3
View File
@@ -13,9 +13,7 @@ export const Checkboxes: Story<CheckboxProps> = (args) => {
<Checkbox <Checkbox
{...args} {...args}
checked={isChecked} checked={isChecked}
onCheckedChange={setIsChecked} onClick={(e: React.ChangeEvent) => setIsChecked((e.target as HTMLInputElement).checked)}
id="story-checkbox"
label="example label"
/> />
</StoryWrapper> </StoryWrapper>
); );
+73 -46
View File
@@ -1,14 +1,10 @@
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { Root, Indicator } from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import { layoutMixins } from '@/styles/layoutMixins'; import { breakpoints } from '@/styles';
type ElementProps = { type ElementProps = {
checked: boolean; checked: boolean;
onCheckedChange: (checked: boolean) => void; onClick: (e: React.ChangeEvent) => void;
id?: string;
label?: React.ReactNode;
}; };
type StyleProps = { type StyleProps = {
@@ -17,54 +13,85 @@ type StyleProps = {
export type CheckboxProps = ElementProps & StyleProps; export type CheckboxProps = ElementProps & StyleProps;
export const Checkbox: React.FC<CheckboxProps> = ({ export const Checkbox: React.FC<CheckboxProps> = ({ checked, className, onClick }) => {
checked, return (
className, <Styled.CheckboxWrapper className={className}>
onCheckedChange, <Styled.Checkbox
id, type="checkbox"
label, checked={checked}
}) => ( onChange={onClick}
<Styled.Container> />
<Styled.Root className={className} checked={checked} onCheckedChange={onCheckedChange} id={id}> <Styled.CustomCheckbox />
<Styled.Indicator> </Styled.CheckboxWrapper>
<CheckIcon /> );
</Styled.Indicator> };
</Styled.Root>
{label && <label htmlFor={id}>{label}</label>}
</Styled.Container>
);
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.Container = styled.div` Styled.CustomCheckbox = styled.span`
${layoutMixins.row} width: 1.25em;
gap: 1ch; height: 1.25em;
font: var(--font-small-book);
label { position: absolute;
cursor: pointer; left: 0;
} right: 0;
`; top: 0;
bottom: 0;
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); background-color: var(--checkbox-backgroundColor);
&[data-state='checked'] { &::after {
--checkbox-backgroundColor: var(--color-accent); 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;
}
} }
`; `;
Styled.Indicator = styled(Indicator)` Styled.CheckboxWrapper = styled.div`
display: flex; --checkbox-backgroundColor: var(--color-layer-1);
align-items: center; --checkbox-checkColor: var(--color-text-1);
justify-content: center;
color: var(--color-text-2); display: flex;
border-radius: 0.25em;
overflow: hidden;
position: relative;
cursor: pointer;
> 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;
}
`; `;
+6 -5
View File
@@ -1,13 +1,16 @@
import type { Story } from '@ladle/react'; import type { Story } from '@ladle/react';
import { Collapsible, type CollapsibleProps } from '@/components/Collapsible'; import { Collapsible } from '@/components/Collapsible';
import { StoryWrapper } from '.ladle/components'; import { StoryWrapper } from '.ladle/components';
import { IconName } from './Icon'; import { IconName } from './Icon';
export const CollapsibleStory: Story<CollapsibleProps> = (args) => ( export const CollapsibleStory: Story<Parameters<typeof Collapsible>> = (args) => (
<StoryWrapper> <StoryWrapper>
<Collapsible {...args}> <Collapsible
label="Collapsible List of Items"
{...args}
>
<ul> <ul>
<li>Collapsible Item 1</li> <li>Collapsible Item 1</li>
<li>Collapsible Item 2</li> <li>Collapsible Item 2</li>
@@ -20,8 +23,6 @@ export const CollapsibleStory: Story<CollapsibleProps> = (args) => (
CollapsibleStory.args = { CollapsibleStory.args = {
disabled: false, disabled: false,
withTrigger: true,
label: 'Collapsible List of Items',
}; };
CollapsibleStory.argTypes = { CollapsibleStory.argTypes = {
+18 -24
View File
@@ -14,7 +14,6 @@ type ElementProps = {
label: React.ReactNode; label: React.ReactNode;
triggerIcon?: IconName; triggerIcon?: IconName;
children: React.ReactNode; children: React.ReactNode;
withTrigger?: boolean;
}; };
type StyleProps = { type StyleProps = {
@@ -24,8 +23,6 @@ type StyleProps = {
fullWidth?: boolean; fullWidth?: boolean;
}; };
export type CollapsibleProps = ElementProps & StyleProps;
export const Collapsible = ({ export const Collapsible = ({
defaultOpen = false, defaultOpen = false,
disabled, disabled,
@@ -38,29 +35,26 @@ export const Collapsible = ({
triggerIconSide = 'left', triggerIconSide = 'left',
fullWidth, fullWidth,
className, className,
withTrigger = true, }: ElementProps & StyleProps) => (
}: CollapsibleProps) => (
<Styled.Root defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}> <Styled.Root defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}>
{withTrigger && ( <Styled.Trigger className={className} disabled={disabled}>
<Styled.Trigger className={className} disabled={disabled}> {triggerIconSide === 'right' && (
{triggerIconSide === 'right' && ( <>
<> {label}
{label} {fullWidth && <HorizontalSeparatorFiller />}
{fullWidth && <HorizontalSeparatorFiller />} </>
</> )}
)}
<Styled.TriggerIcon> <Styled.TriggerIcon>
<Icon iconName={triggerIcon} /> <Icon iconName={triggerIcon} />
</Styled.TriggerIcon> </Styled.TriggerIcon>
{triggerIconSide === 'left' && ( {triggerIconSide === 'left' && (
<> <>
{fullWidth && <HorizontalSeparatorFiller />} {fullWidth && <HorizontalSeparatorFiller />}
{label} {label}
</> </>
)} )}
</Styled.Trigger> </Styled.Trigger>
)}
<Styled.Content $transitionDuration={transitionDuration}>{children}</Styled.Content> <Styled.Content $transitionDuration={transitionDuration}>{children}</Styled.Content>
</Styled.Root> </Styled.Root>
); );
+2 -12
View File
@@ -22,12 +22,7 @@ type PickComboxMenuProps<
MenuGroupValue extends string | number MenuGroupValue extends string | number
> = Pick< > = Pick<
ComboboxMenuProps<MenuItemValue, MenuGroupValue>, ComboboxMenuProps<MenuItemValue, MenuGroupValue>,
| 'inputPlaceholder' 'inputPlaceholder' | 'onItemSelected' | 'slotEmpty' | 'withSearch' | 'withStickyLayout'
| 'onItemSelected'
| 'slotEmpty'
| 'withItemBorders'
| 'withSearch'
| 'withStickyLayout'
>; >;
type PickDialogProps = Pick< type PickDialogProps = Pick<
@@ -58,7 +53,6 @@ export const ComboboxDialogMenu = <
onItemSelected, onItemSelected,
inputPlaceholder, inputPlaceholder,
slotEmpty, slotEmpty,
withItemBorders,
withSearch, withSearch,
withStickyLayout = true, withStickyLayout = true,
children, children,
@@ -89,7 +83,6 @@ export const ComboboxDialogMenu = <
title={title} title={title}
inputPlaceholder={inputPlaceholder} inputPlaceholder={inputPlaceholder}
slotEmpty={slotEmpty} slotEmpty={slotEmpty}
withItemBorders={withItemBorders}
withSearch={withSearch} withSearch={withSearch}
withStickyLayout={withStickyLayout} withStickyLayout={withStickyLayout}
/> />
@@ -102,8 +95,6 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)` Styled.Dialog = styled(Dialog)`
/* Params */ /* Params */
--comboboxDialogMenu-backgroundColor: var(--color-layer-2); --comboboxDialogMenu-backgroundColor: var(--color-layer-2);
--comboboxDialogMenu-item-gap: 0.5rem;
--comboboxDialogMenu-item-padding: 0.5rem 1rem;
/* Overrides */ /* Overrides */
& { & {
@@ -127,6 +118,5 @@ Styled.Dialog = styled(Dialog)`
Styled.ComboboxMenu = styled(ComboboxMenu)` Styled.ComboboxMenu = styled(ComboboxMenu)`
--comboboxMenu-backgroundColor: var(--comboboxDialogMenu-backgroundColor); --comboboxMenu-backgroundColor: var(--comboboxDialogMenu-backgroundColor);
--comboboxMenu-item-gap: var(--comboboxDialogMenu-item-gap); --comboboxMenu-item-gap: var(--comboxDialogMenu-item-gap, 0.5rem);
--comboboxMenu-item-padding: var(--comboboxDialogMenu-item-padding);
`; `;
+17 -50
View File
@@ -2,7 +2,7 @@ import { Fragment, type ReactNode, useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components'; import styled, { type AnyStyledComponent, css } from 'styled-components';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { MenuItem, type MenuConfig } from '@/constants/menus'; import { type MenuConfig } from '@/constants/menus';
import { popoverMixins } from '@/styles/popoverMixins'; import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -20,7 +20,6 @@ type ElementProps<MenuItemValue extends string | number, MenuGroupValue extends
type StyleProps = { type StyleProps = {
className?: string; className?: string;
withItemBorders?: boolean;
withStickyLayout?: boolean; withStickyLayout?: boolean;
}; };
@@ -39,7 +38,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
withSearch = true, withSearch = true,
className, className,
withItemBorders,
withStickyLayout, withStickyLayout,
}: ComboboxMenuProps<MenuItemValue, MenuGroupValue>) => { }: ComboboxMenuProps<MenuItemValue, MenuGroupValue>) => {
const [highlightedCommand, setHighlightedCommand] = useState<MenuItemValue>(); const [highlightedCommand, setHighlightedCommand] = useState<MenuItemValue>();
@@ -82,7 +80,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
<Styled.Group <Styled.Group
key={group.group} key={group.group}
heading={group.groupLabel} heading={group.groupLabel}
$withItemBorders={withItemBorders}
$withStickyLayout={withStickyLayout} $withStickyLayout={withStickyLayout}
> >
{group.items.map((item) => ( {group.items.map((item) => (
@@ -102,31 +99,22 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
} }
}} }}
disabled={item.disabled} disabled={item.disabled}
$withItemBorders={withItemBorders}
> >
{ {item.slotBefore}
<> <Styled.ItemLabel>
{item.slotBefore} <span>
{item.slotCustomContent ?? ( {`${item.label}${item.subitems?.length ? '…' : ''}`}
<Styled.ItemLabel> {item.tag && (
<span> <>
{typeof item.label === 'string' {' '}
? `${item.label}${item.subitems?.length ? '…' : ''}` <Tag>{item.tag}</Tag>
: item.label} </>
{item.tag && (
<>
{' '}
<Tag>{item.tag}</Tag>
</>
)}
</span>
{item.description && <span>{item.description}</span>}
</Styled.ItemLabel>
)} )}
{item.slotAfter} </span>
{item.subitems && '→'} {item.description && <span>{item.description}</span>}
</> </Styled.ItemLabel>
} {item.slotAfter}
{item.subitems && '→'}
</Styled.Item> </Styled.Item>
{searchValue && {searchValue &&
@@ -149,7 +137,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
onItemSelected?.(); onItemSelected?.();
}} }}
disabled={subitem.disabled} disabled={subitem.disabled}
$withItemBorders={withItemBorders}
> >
{subitem.slotBefore} {subitem.slotBefore}
<Styled.ItemLabel> <Styled.ItemLabel>
@@ -182,17 +169,14 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>` Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>`
--comboboxMenu-backgroundColor: var(--color-layer-2); --comboboxMenu-backgroundColor: var(--color-layer-2);
--comboboxMenu-input-backgroundColor: var(--color-layer-3); --comboboxMenu-input-backgroundColor: var(--color-layer-3);
--comboboxMenu-input-height: 2.5rem; --comboboxMenu-input-height: 2.5rem;
--comboboxMenu-item-checked-backgroundColor: ; --comboboxMenu-item-checked-backgroundColor: ;
--comboboxMenu-item-checked-textColor: ; --comboboxMenu-item-checked-textColor: ;
--comboboxMenu-item-highlighted-backgroundColor: var(--color-layer-3); --comboboxMenu-item-highlighted-backgroundColor: var(--color-layer-3);
--comboboxMenu-item-highlighted-textColor: var(--color-text-1); --comboboxMenu-item-highlighted-textColor: var(--color-text-1);
--comboboxMenu-item-backgroundColor: ; --comboboxMenu-item-backgroundColor: ;
--comboboxMenu-item-gap: 0.5rem; --comboboxMenu-item-gap: 0.5rem;
--comboboxMenu-item-padding: 0.5em 1em;
display: grid; display: grid;
align-content: start; align-content: start;
@@ -241,7 +225,7 @@ Styled.Input = styled(Command.Input)`
gap: 0.5rem; gap: 0.5rem;
`; `;
Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLayout?: boolean }>` Styled.Group = styled(Command.Group)<{ $withStickyLayout?: boolean }>`
color: var(--color-text-0); color: var(--color-text-0);
> [cmdk-group-heading] { > [cmdk-group-heading] {
@@ -264,14 +248,6 @@ Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLa
${layoutMixins.stickyArea3} ${layoutMixins.stickyArea3}
} }
`} `}
${({ $withItemBorders }) =>
$withItemBorders &&
css`
> [cmdk-group-items] {
padding: var(--border-width) 0;
}
`}
`; `;
Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>` Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
@@ -298,14 +274,13 @@ Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
`} `}
`; `;
Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>` Styled.Item = styled(Command.Item)`
${layoutMixins.scrollSnapItem} ${layoutMixins.scrollSnapItem}
${popoverMixins.item} ${popoverMixins.item}
--item-checked-backgroundColor: var(--comboboxMenu-item-checked-backgroundColor); --item-checked-backgroundColor: var(--comboboxMenu-item-checked-backgroundColor);
--item-checked-textColor: var(--comboboxMenu-item-checked-textColor); --item-checked-textColor: var(--comboboxMenu-item-checked-textColor);
--item-highlighted-textColor: var(--comboboxMenu-item-highlighted-textColor); --item-highlighted-textColor: var(--comboboxMenu-item-highlighted-textColor);
--item-gap: var(--comboboxMenu-item-gap); --item-gap: var(--comboboxMenu-item-gap);
--item-padding: var(--comboboxMenu-item-padding);
background-color: var(--comboboxMenu-backgroundColor, inherit); background-color: var(--comboboxMenu-backgroundColor, inherit);
@@ -316,12 +291,6 @@ Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>`
opacity: 0.75; opacity: 0.75;
cursor: not-allowed; cursor: not-allowed;
} }
${({ $withItemBorders }) =>
$withItemBorders &&
css`
${layoutMixins.withOuterBorder}
`}
`; `;
Styled.ItemLabel = styled.div` Styled.ItemLabel = styled.div`
@@ -340,8 +309,6 @@ Styled.ItemLabel = styled.div`
opacity: 0.8; opacity: 0.8;
} }
} }
min-width: 0;
`; `;
Styled.Empty = styled(Command.Empty)` Styled.Empty = styled(Command.Empty)`
+3 -3
View File
@@ -15,10 +15,10 @@ CopyButtonStory.args = {
}; };
CopyButtonStory.argTypes = { CopyButtonStory.argTypes = {
buttonType: { shownAsText: {
options: ["text", "icon", "default"], options: [true, false],
control: { type: 'select' }, control: { type: 'select' },
defaultValue: "default", defaultValue: false,
}, },
children: { children: {
options: ['some text to copy'], options: ['some text to copy'],
+4 -36
View File
@@ -9,20 +9,14 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { Button, ButtonProps } from './Button'; import { Button, ButtonProps } from './Button';
import { Icon, IconName } from './Icon'; import { Icon, IconName } from './Icon';
import { IconButton } from './IconButton';
export type CopyButtonProps = { export type CopyButtonProps = {
value?: string; value?: string;
buttonType?: 'text' | 'icon' | 'default'; shownAsText?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
} & ButtonProps; } & ButtonProps;
export const CopyButton = ({ export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: CopyButtonProps) => {
value,
buttonType = 'default',
children,
...buttonProps
}: CopyButtonProps) => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -34,19 +28,11 @@ export const CopyButton = ({
setTimeout(() => setCopied(false), 500); setTimeout(() => setCopied(false), 500);
}; };
return buttonType === 'text' ? ( return shownAsText ? (
<Styled.InlineRow onClick={onCopy} copied={copied}> <Styled.InlineRow onClick={onCopy} copied={copied}>
{children} {children}
<Styled.Icon copied={copied} iconName={copied ? IconName.Check : IconName.Copy} /> <Icon iconName={IconName.Copy} />
</Styled.InlineRow> </Styled.InlineRow>
) : buttonType === 'icon' ? (
<Styled.IconButton
{...buttonProps}
copied={copied}
action={ButtonAction.Base}
iconName={copied ? IconName.Check : IconName.Copy}
onClick={onCopy}
/>
) : ( ) : (
<Button <Button
{...buttonProps} {...buttonProps}
@@ -77,21 +63,3 @@ 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);
}
`}
`;
+28 -38
View File
@@ -38,7 +38,6 @@ type ElementProps = {
slotTrigger?: React.ReactNode; slotTrigger?: React.ReactNode;
slotHeaderInner?: React.ReactNode; slotHeaderInner?: React.ReactNode;
slotFooter?: React.ReactNode; slotFooter?: React.ReactNode;
withClose?: boolean;
}; };
type StyleProps = { type StyleProps = {
@@ -47,7 +46,6 @@ type StyleProps = {
hasHeaderBorder?: boolean; hasHeaderBorder?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
withAnimation?: boolean;
}; };
export type DialogProps = ElementProps & StyleProps; export type DialogProps = ElementProps & StyleProps;
@@ -81,11 +79,9 @@ export const Dialog = ({
slotTrigger, slotTrigger,
slotHeaderInner, slotHeaderInner,
slotFooter, slotFooter,
withClose = true,
placement = DialogPlacement.Default, placement = DialogPlacement.Default,
portalContainer, portalContainer,
hasHeaderBorder = false, hasHeaderBorder = false,
withAnimation = false,
children, children,
className, className,
}: DialogProps) => { }: DialogProps) => {
@@ -109,7 +105,6 @@ export const Dialog = ({
e.preventDefault(); e.preventDefault();
} }
}} }}
$withAnimation={withAnimation}
> >
<Styled.Header $withBorder={hasHeaderBorder}> <Styled.Header $withBorder={hasHeaderBorder}>
<Styled.HeaderTopRow> <Styled.HeaderTopRow>
@@ -119,7 +114,7 @@ export const Dialog = ({
{title && <Styled.Title>{title}</Styled.Title>} {title && <Styled.Title>{title}</Styled.Title>}
{!preventClose && withClose && ( {!preventClose && (
<Styled.Close ref={closeButtonRef}> <Styled.Close ref={closeButtonRef}>
<Icon iconName={IconName.Close} /> <Icon iconName={IconName.Close} />
</Styled.Close> </Styled.Close>
@@ -173,7 +168,7 @@ Styled.Overlay = styled(Overlay)`
} }
`; `;
Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?: boolean }>` Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
/* Params */ /* Params */
--dialog-inset: 1rem; --dialog-inset: 1rem;
--dialog-width: 30rem; --dialog-width: 30rem;
@@ -229,7 +224,7 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?
outline: none; outline: none;
${({ placement, $withAnimation }) => ${({ placement }) =>
({ ({
[DialogPlacement.Default]: css` [DialogPlacement.Default]: css`
inset: var(--dialog-inset); inset: var(--dialog-inset);
@@ -265,11 +260,9 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?
padding-bottom: var(--dialog-radius); */ padding-bottom: var(--dialog-radius); */
} }
${$withAnimation && @media (prefers-reduced-motion: no-preference) {
css` &[data-state='open'] {
@media (prefers-reduced-motion: no-preference) { animation: ${keyframes`
&[data-state='open'] {
animation: ${keyframes`
from { from {
opacity: 0; opacity: 0;
} }
@@ -277,19 +270,18 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?
max-height: 0; max-height: 0;
} }
`} 0.15s var(--ease-out-expo); `} 0.15s var(--ease-out-expo);
} }
&[data-state='closed'] { &[data-state='closed'] {
animation: ${keyframes` animation: ${keyframes`
to { to {
opacity: 0; opacity: 0;
scale: 0.9; scale: 0.9;
max-height: 0; max-height: 0;
} }
`} 0.15s; `} 0.15s;
}
} }
`} }
`, `,
[DialogPlacement.Sidebar]: css` [DialogPlacement.Sidebar]: css`
--dialog-width: var(--sidebar-width); --dialog-width: var(--sidebar-width);
@@ -299,52 +291,50 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?
margin-left: auto; margin-left: auto;
} }
${$withAnimation && @media (prefers-reduced-motion: no-preference) {
css` &[data-state='open'] {
@media (prefers-reduced-motion: no-preference) { animation: ${keyframes`
&[data-state='open'] {
animation: ${keyframes`
from { from {
translate: 100% 0; translate: 100% 0;
opacity: 0; opacity: 0;
} }
`} 0.15s var(--ease-out-expo); `} 0.15s var(--ease-out-expo);
} }
&[data-state='closed'] { &[data-state='closed'] {
animation: ${keyframes` animation: ${keyframes`
to { to {
translate: 100% 0; translate: 100% 0;
opacity: 0; opacity: 0;
} }
`} 0.15s var(--ease-out-expo); `} 0.15s var(--ease-out-expo);
}
} }
`} }
`, `,
[DialogPlacement.Inline]: css` [DialogPlacement.Inline]: css`
${$withAnimation && @media (prefers-reduced-motion: no-preference) {
css` &[data-state='open'] {
@media (prefers-reduced-motion: no-preference) { animation: ${keyframes`
&[data-state='open'] {
animation: ${keyframes`
from { from {
scale: 0.99; scale: 0.99;
opacity: 0; opacity: 0;
/* filter: blur(2px); */
/* backdrop-filter: none; */
} }
`} 0.15s var(--ease-out-expo); `} 0.15s var(--ease-out-expo);
} }
&[data-state='closed'] { &[data-state='closed'] {
animation: ${keyframes` animation: ${keyframes`
to { to {
scale: 0.99; scale: 0.99;
opacity: 0; opacity: 0;
/* filter: blur(2px); */
/* backdrop-filter: none; */
} }
`} 0.15s var(--ease-out-expo); `} 0.15s var(--ease-out-expo);
}
} }
`} }
`, `,
[DialogPlacement.FullScreen]: css` [DialogPlacement.FullScreen]: css`
--dialog-width: 100vw; --dialog-width: 100vw;
+1 -4
View File
@@ -9,7 +9,6 @@ import {
RadioItem, RadioItem,
Portal, Portal,
} from '@radix-ui/react-dropdown-menu'; } from '@radix-ui/react-dropdown-menu';
import { CheckIcon } from '@radix-ui/react-icons';
import { type MenuItem } from '@/constants/menus'; import { type MenuItem } from '@/constants/menus';
@@ -91,9 +90,7 @@ export const DropdownSelectMenu = <MenuItemValue extends string>({
{slotAfter} {slotAfter}
<Styled.ItemIndicator> <Styled.ItemIndicator>{/* <CheckIcon /> */}</Styled.ItemIndicator>
<CheckIcon />
</Styled.ItemIndicator>
</Styled.RadioItem> </Styled.RadioItem>
))} ))}
</RadioGroup> </RadioGroup>
+29 -2
View File
@@ -1,16 +1,43 @@
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
export const GreenCheckCircle = ({ className }: { className?: string }) => ( export const GreenCheckCircle = ({ className }: { className?: string }) => (
<Styled.Icon className={className} iconName={IconName.CheckCircle} /> <Styled.GreenCheckCircle className={className}>
<Icon iconName={IconName.Check} />
</Styled.GreenCheckCircle>
); );
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.Icon = styled(Icon)` Styled.GreenCheckCircle = styled.div`
${layoutMixins.stack}
--icon-size: 1.25rem; --icon-size: 1.25rem;
--icon-border-width: 3px;
width: var(--icon-size); width: var(--icon-size);
height: 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;
}
`; `;
-3
View File
@@ -15,7 +15,6 @@ import {
CautionCircleIcon, CautionCircleIcon,
ChatIcon, ChatIcon,
CheckIcon, CheckIcon,
CheckCircleIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
ClockIcon, ClockIcon,
@@ -87,7 +86,6 @@ export enum IconName {
CautionCircleStroked = 'CautionCircleStroked', CautionCircleStroked = 'CautionCircleStroked',
Chat = 'Chat', Chat = 'Chat',
Check = 'Check', Check = 'Check',
CheckCircle = 'CheckCircle',
ChevronLeft = 'ChevronLeft', ChevronLeft = 'ChevronLeft',
ChevronRight = 'ChevronRight', ChevronRight = 'ChevronRight',
Clock = 'Clock', Clock = 'Clock',
@@ -160,7 +158,6 @@ const icons = {
[IconName.CautionCircleStroked]: CautionCircleStrokeIcon, [IconName.CautionCircleStroked]: CautionCircleStrokeIcon,
[IconName.Chat]: ChatIcon, [IconName.Chat]: ChatIcon,
[IconName.Check]: CheckIcon, [IconName.Check]: CheckIcon,
[IconName.CheckCircle]: CheckCircleIcon,
[IconName.ChevronLeft]: ChevronLeftIcon, [IconName.ChevronLeft]: ChevronLeftIcon,
[IconName.ChevronRight]: ChevronRightIcon, [IconName.ChevronRight]: ChevronRightIcon,
[IconName.Clock]: ClockIcon, [IconName.Clock]: ClockIcon,
+64 -11
View File
@@ -1,4 +1,4 @@
import { forwardRef, Ref } from 'react'; import { forwardRef, Ref, useEffect, useRef, useState } from 'react';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { NavLink, matchPath, useLocation } from 'react-router-dom'; import { NavLink, matchPath, useLocation } from 'react-router-dom';
@@ -21,6 +21,8 @@ import {
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { isTruthy } from '@/lib/isTruthy';
import { Tag } from './Tag'; import { Tag } from './Tag';
import { Icon, IconName } from './Icon'; import { Icon, IconName } from './Icon';
@@ -59,6 +61,7 @@ const NavItem = forwardRef(
<> <>
{slotBefore} {slotBefore}
<span> <span>
{/* {`${label}${subitems?.length ? ' ' : ''}`} */}
{label} {label}
{tag && ( {tag && (
<> <>
@@ -97,6 +100,8 @@ const NavItem = forwardRef(
} }
); );
type TriggerRef = HTMLAnchorElement | HTMLDivElement | HTMLButtonElement | null;
export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue extends string>({ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue extends string>({
onSelectItem, onSelectItem,
onSelectGroup, onSelectGroup,
@@ -107,6 +112,33 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
dir = 'ltr', dir = 'ltr',
className, className,
}: ElementProps<MenuItemValue, MenuGroupValue> & StyleProps) => { }: 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 = ({ const renderSubitems = ({
item, item,
depth = 0, depth = 0,
@@ -116,18 +148,18 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
}) => ( }) => (
<> <>
<Styled.SubMenuTrigger <Styled.SubMenuTrigger
asChild asChild={depth > 0}
onPointerMove={(e: MouseEvent) => e.preventDefault()} ref={(ref: TriggerRef) => (triggerRefs.current[item.value] = ref)}
onPointerLeave={(e: MouseEvent) => e.preventDefault()} onClick={(e: MouseEvent) => {
if (clickIsDisabled) {
e.preventDefault();
}
}}
> >
<Styled.NavItem onSelect={onSelectGroup} orientation={itemOrientation} {...item} /> <Styled.NavItem onSelect={onSelectGroup} orientation={itemOrientation} {...item} />
</Styled.SubMenuTrigger> </Styled.SubMenuTrigger>
<Styled.Content <Styled.Content data-placement={submenuPlacement}>
onPointerEnter={(e: MouseEvent) => e.preventDefault()}
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
data-placement={submenuPlacement}
>
<Styled.Sub data-placement={submenuPlacement}> <Styled.Sub data-placement={submenuPlacement}>
<Styled.List <Styled.List
data-orientation={depth > 0 ? 'menu' : orientation === 'vertical' ? 'vertical' : 'menu'} data-orientation={depth > 0 ? 'menu' : orientation === 'vertical' ? 'vertical' : 'menu'}
@@ -378,12 +410,25 @@ Styled.List = styled(List)`
`; `;
Styled.ListItem = styled(Item)` Styled.ListItem = styled(Item)`
/* display: contents; */
display: grid; display: grid;
position: relative; position: relative;
${Styled.List}[data-orientation="horizontal"] > & { ${Styled.List}[data-orientation="horizontal"] > & {
gap: var(--submenu-side-offset); 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)` Styled.SubMenuTrigger = styled(Trigger)`
@@ -391,6 +436,11 @@ Styled.SubMenuTrigger = styled(Trigger)`
outline-offset: -2px; outline-offset: -2px;
&[data-state='open'] { &[data-state='open'] {
div {
background-color: var(--navigationMenu-item-checked-backgroundColor);
color: var(--navigationMenu-item-checked-textColor);
}
svg { svg {
rotate: 0.5turn; rotate: 0.5turn;
} }
@@ -402,10 +452,11 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
subitems?.length subitems?.length
? css` ? css`
${popoverMixins.trigger} ${popoverMixins.trigger}
--trigger-open-backgroundColor: var(--navigationMenu-item-checked-backgroundColor); --trigger-backgroundColor: transparent;
--trigger-open-backgroundColor: var(--color-layer-3);
` `
: css` : css`
&:hover:not(:active) { &:hover:not(.active) {
background-color: var(--navigationMenu-item-highlighted-backgroundColor); background-color: var(--navigationMenu-item-highlighted-backgroundColor);
color: var(--navigationMenu-item-highlighted-textColor); color: var(--navigationMenu-item-highlighted-textColor);
} }
@@ -419,6 +470,8 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
--item-radius: var(--navigationMenu-item-radius); --item-radius: var(--navigationMenu-item-radius);
--item-padding: var(--navigationMenu-item-padding); --item-padding: var(--navigationMenu-item-padding);
/* ${popoverMixins.backdropOverlay} */
${layoutMixins.scrollSnapItem} ${layoutMixins.scrollSnapItem}
min-height: var(--navigationMenu-item-height); min-height: var(--navigationMenu-item-height);
-201
View File
@@ -1,201 +0,0 @@
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);
`;
+1 -7
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components'; import styled, { type AnyStyledComponent, css } from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -22,11 +22,6 @@ export const SearchInput = ({
}: SearchInputProps) => { }: SearchInputProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (isOpen) inputRef?.current?.focus();
}, [inputRef, isOpen]);
return ( return (
<Styled.Search> <Styled.Search>
@@ -46,7 +41,6 @@ export const SearchInput = ({
/> />
<Styled.Input <Styled.Input
autoFocus autoFocus
ref={inputRef}
value={value} value={value}
isOpen={isOpen} isOpen={isOpen}
type={type} type={type}
+7 -12
View File
@@ -12,8 +12,6 @@ import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { WithLabel } from '@/components/WithLabel'; import { WithLabel } from '@/components/WithLabel';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import breakpoints from '@/styles/breakpoints';
type ElementProps = { type ElementProps = {
asChild?: boolean; asChild?: boolean;
@@ -86,30 +84,27 @@ export const SearchSelectMenu = ({
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.SearchSelectMenu = styled.div`
${layoutMixins.column}
`;
Styled.MenuTrigger = styled.div` Styled.MenuTrigger = styled.div`
height: var(--form-input-height); height: var(--form-input-height);
${layoutMixins.spacedRow} ${layoutMixins.spacedRow}
align-items: center; align-items: center;
padding: var(--form-input-paddingY) var(--form-input-paddingX); padding: 0 1rem;
@media ${breakpoints.tablet} {
height: var(--form-input-height-mobile);
}
`; `;
Styled.WithLabel = styled(WithLabel)` Styled.WithLabel = styled(WithLabel)`
${formMixins.inputLabel} --label-textColor: var(--color-text-0);
gap: 0.25rem;
label { label {
font: var(--font-mini-book); font: var(--font-mini-book);
} }
`; `;
Styled.SearchSelectMenu = styled.div`
${layoutMixins.column}
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2); --withReceipt-backgroundColor: var(--color-layer-2);
+9 -15
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import styled, { css, type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { import {
Root, Root,
@@ -13,13 +13,11 @@ import {
Portal, Portal,
Icon as SelectIcon, Icon as SelectIcon,
} from '@radix-ui/react-select'; } from '@radix-ui/react-select';
import { CheckIcon } from '@radix-ui/react-icons';
import { popoverMixins } from '@/styles/popoverMixins'; import { popoverMixins } from '@/styles/popoverMixins';
import { formMixins } from '@/styles/formMixins'; import { formMixins } from '@/styles/formMixins';
import { WithLabel } from '@/components/WithLabel'; import { WithLabel } from '@/components/WithLabel';
import { Icon, IconName } from './Icon';
export const SelectMenu = <T extends string>({ export const SelectMenu = <T extends string>({
children, children,
@@ -46,9 +44,7 @@ export const SelectMenu = <T extends string>({
) : ( ) : (
<Value /> <Value />
)} )}
{React.Children.toArray(children).length > 1 && ( {React.Children.toArray(children).length > 1 && <Styled.DropdownIcon />}
<Styled.TriggerIcon iconName={IconName.Triangle} />
)}
</Styled.Trigger> </Styled.Trigger>
<Portal> <Portal>
<Styled.Content className={className}> <Styled.Content className={className}>
@@ -72,9 +68,7 @@ export const SelectItem = <T extends string>({
}) => ( }) => (
<Styled.Item className={className} value={value}> <Styled.Item className={className} value={value}>
<ItemText>{label}</ItemText> <ItemText>{label}</ItemText>
<Styled.ItemIndicator> <Styled.ItemIndicator>{/* <CheckIcon /> */}</Styled.ItemIndicator>
<CheckIcon />
</Styled.ItemIndicator>
</Styled.Item> </Styled.Item>
); );
@@ -94,6 +88,12 @@ 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)` Styled.Content = styled(Content)`
--select-menu-content-maxWidth: ; --select-menu-content-maxWidth: ;
max-width: var(--select-menu-content-maxWidth); max-width: var(--select-menu-content-maxWidth);
@@ -116,9 +116,3 @@ Styled.WithLabel = styled(WithLabel)`
${formMixins.inputLabel} ${formMixins.inputLabel}
border-radius: 0; border-radius: 0;
`; `;
Styled.TriggerIcon = styled(Icon)`
width: 0.625rem;
height: 0.375rem;
color: var(--color-text-0);
`;
+8 -12
View File
@@ -831,7 +831,6 @@ Styled.SortArrow = styled.span<{ sortDirection: 'ascending' | 'descending' }>`
Styled.Thead = styled.thead<StyleProps>` Styled.Thead = styled.thead<StyleProps>`
${layoutMixins.stickyHeader} ${layoutMixins.stickyHeader}
scroll-snap-align: none; scroll-snap-align: none;
font: var(--font-mini-book);
> * { > * {
height: var(--stickyArea-topHeight); height: var(--stickyArea-topHeight);
@@ -840,17 +839,18 @@ Styled.Thead = styled.thead<StyleProps>`
color: var(--tableHeader-textColor); color: var(--tableHeader-textColor);
background-color: var(--tableHeader-backgroundColor); background-color: var(--tableHeader-backgroundColor);
${({ withInnerBorders, withGradientCardRows }) => @media ${breakpoints.notTablet} {
withInnerBorders && ${({ withInnerBorders, withGradientCardRows }) =>
!withGradientCardRows && withInnerBorders &&
css` !withGradientCardRows &&
${layoutMixins.withInnerHorizontalBorders} css`
`} ${layoutMixins.withInnerHorizontalBorders}
`}
}
`; `;
Styled.Tbody = styled.tbody<StyleProps>` Styled.Tbody = styled.tbody<StyleProps>`
${layoutMixins.stickyArea2} ${layoutMixins.stickyArea2}
font: var(--font-small-book);
// If <table> height is fixed with not enough rows to overflow, vertically center the rows // If <table> height is fixed with not enough rows to overflow, vertically center the rows
&:before, &:before,
@@ -869,10 +869,6 @@ Styled.Tbody = styled.tbody<StyleProps>`
--stickyArea2-paddingBottom: var(--border-width); --stickyArea2-paddingBottom: var(--border-width);
--stickyArea2-paddingLeft: var(--border-width); --stickyArea2-paddingLeft: var(--border-width);
--stickyArea2-paddingRight: var(--border-width); --stickyArea2-paddingRight: var(--border-width);
tr:first-of-type {
box-shadow: none;
}
`} `}
${({ withGradientCardRows }) => ${({ withGradientCardRows }) =>
+78 -72
View File
@@ -1,18 +1,24 @@
import { type MouseEvent, useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import { Root, Action, Close } from '@radix-ui/react-toast'; import { Root, Title, Description, Action, Close } from '@radix-ui/react-toast';
import { ButtonShape, ButtonSize } from '@/constants/buttons'; import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { popoverMixins } from '@/styles/popoverMixins';
import { Notification, type NotificationProps } from '@/components/Notification';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { IconName } from './Icon'; import { CloseIcon } from '@/icons';
import styled, { keyframes } from 'styled-components';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = { type ElementProps = {
isOpen?: boolean; isOpen?: boolean;
setIsOpen?: (isOpen: boolean, isClosedFromTimeout?: boolean) => void; setIsOpen?: (isOpen: boolean, isClosedFromTimeout?: boolean) => void;
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
actionDescription?: string; actionDescription?: string;
actionAltText?: string; actionAltText?: string;
sensitivity?: 'foreground' | 'background'; sensitivity?: 'foreground' | 'background';
@@ -20,22 +26,13 @@ type ElementProps = {
lastUpdated?: number; lastUpdated?: number;
}; };
type StyleProps = { type StyleProps = {};
className?: string;
};
type ToastProps = NotificationProps & ElementProps & StyleProps;
export const Toast = ({ export const Toast = ({
className,
isOpen = true, isOpen = true,
notification,
onClick,
setIsOpen, setIsOpen,
slotIcon, slotIcon,
slotTitle, slotTitle,
slotTitleLeft,
slotTitleRight,
slotDescription, slotDescription,
slotCustomContent, slotCustomContent,
slotAction, slotAction,
@@ -44,7 +41,7 @@ export const Toast = ({
sensitivity = 'background', sensitivity = 'background',
duration = Infinity, duration = Infinity,
lastUpdated, lastUpdated,
}: ToastProps) => { }: ElementProps & StyleProps) => {
// Timeout // Timeout
const timeout = useRef<number>(); const timeout = useRef<number>();
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
@@ -61,45 +58,37 @@ export const Toast = ({
return ( return (
<$Root <$Root
className={className}
type={sensitivity} type={sensitivity}
duration={Infinity} duration={Infinity}
open={isOpen} open={isOpen}
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
onPause={() => setIsPaused(true)} onPause={() => setIsPaused(true)}
onResume={() => setIsPaused(false)} onResume={() => setIsPaused(false)}
onClick={() => {
setIsOpen?.(false, false);
onClick?.();
}}
> >
<Close asChild> <div>
<$CloseButton <$Container>
iconName={IconName.Close} <$Header>
shape={ButtonShape.Square} {slotIcon && <$Icon>{slotIcon}</$Icon>}
size={ButtonSize.XSmall}
onClick={(e: MouseEvent) => e.stopPropagation()}
/>
</Close>
{slotCustomContent ?? ( <Close asChild>
<$Notification <$CloseButton
isToast iconComponent={CloseIcon}
notification={notification} shape={ButtonShape.Square}
slotIcon={slotIcon} size={ButtonSize.XSmall}
slotTitle={slotTitle} />
slotTitleLeft={slotTitleLeft} </Close>
slotTitleRight={slotTitleRight}
slotDescription={slotDescription} <$Title>{slotTitle}</$Title>
slotAction={ </$Header>
actionDescription && ( {!slotCustomContent && <$Description>{slotDescription}</$Description>}
<$Action asChild altText={actionAltText}> {slotCustomContent}
{slotAction} {actionDescription && (
</$Action> <$Action asChild altText={actionAltText}>
) {slotAction}
} </$Action>
/> )}
)} </$Container>
</div>
</$Root> </$Root>
); );
}; };
@@ -125,7 +114,7 @@ const $Root = styled(Root)`
translate: var(--x) var(--y); translate: var(--x) var(--y);
will-change: translate, margin-top; will-change: translate, margin-top;
/* margin-bottom: var(--toasts-gap); */ margin-bottom: var(--toasts-gap);
&[data-swipe-direction='right'] { &[data-swipe-direction='right'] {
&[data-state='open'] { &[data-state='open'] {
@@ -174,10 +163,6 @@ const $Root = styled(Root)`
cursor: n-resize; cursor: n-resize;
} }
&:focus:not([data-swipe='end']) & {
outline: var(--color-accent) 1px solid;
}
&[data-swipe='move'] { &[data-swipe='move'] {
transition-property: opacity; transition-property: opacity;
opacity: 0.98; opacity: 0.98;
@@ -186,10 +171,6 @@ const $Root = styled(Root)`
* { * {
cursor: inherit; cursor: inherit;
} }
& > * {
opacity: 0.5;
}
} }
&[data-state='closed'], &[data-state='closed'],
@@ -217,44 +198,69 @@ const $Root = styled(Root)`
} }
`; `;
const $Notification = styled(Notification)` const $Container = styled.div`
// Params // Params
--toast-icon-size: 1.75em; --toast-icon-size: 1.75em;
// Rules // Rules
${popoverMixins.popover} ${popoverMixins.popover}
overflow: visible;
padding: 1rem; padding: 1rem;
box-shadow: 0 0 0 var(--border-width) var(--color-border), box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border // border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow 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; 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)` const $CloseButton = styled(IconButton)`
--button-textColor: var(--color-text-0); --button-textColor: var(--color-text-0);
--button-border: none; --button-border: none;
--button-icon-size: 0.85em; --button-icon-size: 0.85em;
display: none;
/* float: right; */ float: right;
margin: -0.42rem -0.42rem -0.42rem 0.42rem; margin: -0.42rem -0.42rem -0.42rem 0.42rem;
`;
// Absolute const $Title = styled(Title)`
position: absolute; flex: 1;
top: 0;
right: 0;
border: solid var(--border-width) var(--color-border); font: var(--font-base-medium);
border-radius: 50%; color: var(--color-text-2);
${$Root}:hover & { overflow: hidden;
display: block; text-overflow: ellipsis;
z-index: 2; `;
}
const $Description = styled(Description)`
margin-top: 0.5rem;
color: var(--color-text-0);
font: var(--font-small-book);
`; `;
const $Action = styled(Action)` const $Action = styled(Action)`
-2
View File
@@ -34,7 +34,6 @@ const $ToastArea = styled.aside`
> * { > * {
display: contents; display: contents;
pointer-events: none; pointer-events: none;
position: relative;
> ol { > ol {
/* display: block; */ /* display: block; */
@@ -44,7 +43,6 @@ const $ToastArea = styled.aside`
// Desired display order: latest to earliest (¯\_(ツ)_/¯) // Desired display order: latest to earliest (¯\_(ツ)_/¯)
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
position: relative;
pointer-events: none; pointer-events: none;
-4
View File
@@ -52,19 +52,15 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.BaseButton = styled(BaseButton)` Styled.BaseButton = styled(BaseButton)`
--button-toggle-off-backgroundColor: var(--color-layer-3); --button-toggle-off-backgroundColor: var(--color-layer-3);
--button-toggle-off-textColor: var(--color-text-0); --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-backgroundColor: var(--color-layer-1);
--button-toggle-on-textColor: var(--color-text-2); --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-backgroundColor: var(--button-toggle-off-backgroundColor);
--button-textColor: var(--button-toggle-off-textColor); --button-textColor: var(--button-toggle-off-textColor);
--button-border: var(--button-toggle-off-border);
&[data-state='on'], &[data-state='on'],
&[data-state='active'] { &[data-state='active'] {
--button-backgroundColor: var(--button-toggle-on-backgroundColor); --button-backgroundColor: var(--button-toggle-on-backgroundColor);
--button-textColor: var(--button-toggle-on-textColor); --button-textColor: var(--button-toggle-on-textColor);
--button-border: var(--button-toggle-on-border);
} }
`; `;
+1 -34
View File
@@ -124,8 +124,6 @@ export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPo
export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder; export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder;
export type OrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus; export type OrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
export const AbacusOrderStatus = 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 SubaccountFills = Abacus.exchange.dydx.abacus.output.SubaccountFill[];
export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill; export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill;
export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment; export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment;
@@ -211,13 +209,11 @@ type ReadonlyKeysOf<T> = {
[K in keyof T]: IfEquals<{ [_ in K]: T[K] }, { readonly [_ in K]: T[K] }, K, never>; [K in keyof T]: IfEquals<{ [_ in K]: T[K] }, { readonly [_ in K]: T[K] }, K, never>;
}[keyof T]; }[keyof T];
export type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>; type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
export const ORDER_SIDES: Record<KotlinIrEnumValues<typeof AbacusOrderSide>, OrderSide> = { export const ORDER_SIDES: Record<KotlinIrEnumValues<typeof AbacusOrderSide>, OrderSide> = {
[AbacusOrderSide.buy.name]: OrderSide.BUY, [AbacusOrderSide.buy.name]: OrderSide.BUY,
[AbacusOrderSide.sell.name]: OrderSide.SELL, [AbacusOrderSide.sell.name]: OrderSide.SELL,
[AbacusOrderSide.buy.rawValue]: OrderSide.BUY,
[AbacusOrderSide.sell.rawValue]: OrderSide.SELL,
}; };
export const POSITION_SIDES: Record<KotlinIrEnumValues<typeof AbacusPositionSide>, PositionSide> = { export const POSITION_SIDES: Record<KotlinIrEnumValues<typeof AbacusPositionSide>, PositionSide> = {
@@ -238,25 +234,12 @@ export const HISTORICAL_PNL_PERIODS: Record<
export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderStatus>, string> = { export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderStatus>, string> = {
[AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS, [AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.open.rawValue]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.partiallyFilled.name]: STRING_KEYS.PARTIALLY_FILLED, [AbacusOrderStatus.partiallyFilled.name]: STRING_KEYS.PARTIALLY_FILLED,
[AbacusOrderStatus.partiallyFilled.rawValue]: STRING_KEYS.PARTIALLY_FILLED,
[AbacusOrderStatus.filled.name]: STRING_KEYS.ORDER_FILLED, [AbacusOrderStatus.filled.name]: STRING_KEYS.ORDER_FILLED,
[AbacusOrderStatus.filled.rawValue]: STRING_KEYS.ORDER_FILLED,
[AbacusOrderStatus.cancelled.name]: STRING_KEYS.CANCELED, [AbacusOrderStatus.cancelled.name]: STRING_KEYS.CANCELED,
[AbacusOrderStatus.cancelled.rawValue]: STRING_KEYS.CANCELED,
[AbacusOrderStatus.canceling.name]: STRING_KEYS.CANCELING, [AbacusOrderStatus.canceling.name]: STRING_KEYS.CANCELING,
[AbacusOrderStatus.canceling.rawValue]: STRING_KEYS.CANCELING,
[AbacusOrderStatus.pending.name]: STRING_KEYS.PENDING, [AbacusOrderStatus.pending.name]: STRING_KEYS.PENDING,
[AbacusOrderStatus.pending.rawValue]: STRING_KEYS.PENDING,
[AbacusOrderStatus.untriggered.name]: STRING_KEYS.UNTRIGGERED, [AbacusOrderStatus.untriggered.name]: STRING_KEYS.UNTRIGGERED,
[AbacusOrderStatus.untriggered.rawValue]: STRING_KEYS.UNTRIGGERED,
}; };
export const TRADE_TYPES: Record< export const TRADE_TYPES: Record<
@@ -264,31 +247,15 @@ export const TRADE_TYPES: Record<
Nullable<TradeTypes> Nullable<TradeTypes>
> = { > = {
[AbacusOrderType.limit.name]: TradeTypes.LIMIT, [AbacusOrderType.limit.name]: TradeTypes.LIMIT,
[AbacusOrderType.limit.rawValue]: TradeTypes.LIMIT,
[AbacusOrderType.market.name]: TradeTypes.MARKET, [AbacusOrderType.market.name]: TradeTypes.MARKET,
[AbacusOrderType.market.rawValue]: TradeTypes.MARKET,
[AbacusOrderType.stopLimit.name]: TradeTypes.STOP_LIMIT, [AbacusOrderType.stopLimit.name]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopLimit.rawValue]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopMarket.name]: TradeTypes.STOP_MARKET, [AbacusOrderType.stopMarket.name]: TradeTypes.STOP_MARKET,
[AbacusOrderType.stopMarket.rawValue]: TradeTypes.STOP_MARKET,
[AbacusOrderType.takeProfitLimit.name]: TradeTypes.TAKE_PROFIT, [AbacusOrderType.takeProfitLimit.name]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitLimit.rawValue]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitMarket.name]: TradeTypes.TAKE_PROFIT_MARKET, [AbacusOrderType.takeProfitMarket.name]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.takeProfitMarket.rawValue]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.liquidated.name]: null, [AbacusOrderType.liquidated.name]: null,
[AbacusOrderType.liquidation.name]: null, [AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.rawValue]: null,
[AbacusOrderType.trailingStop.name]: null, [AbacusOrderType.trailingStop.name]: null,
[AbacusOrderType.trailingStop.rawValue]: null,
}; };
// Custom types involving Abacus // Custom types involving Abacus
-2
View File
@@ -10,7 +10,6 @@ export enum DialogTypes {
MobileSignIn = 'MobileSignIn', MobileSignIn = 'MobileSignIn',
Onboarding = 'Onboarding', Onboarding = 'Onboarding',
OrderDetails = 'OrderDetails', OrderDetails = 'OrderDetails',
Preferences = 'Preferences',
RateLimit = 'RateLimit', RateLimit = 'RateLimit',
Receive = 'Receive', Receive = 'Receive',
RestrictedGeo = 'RestrictedGeo', RestrictedGeo = 'RestrictedGeo',
@@ -18,7 +17,6 @@ export enum DialogTypes {
Trade = 'Trade', Trade = 'Trade',
Transfer = 'Transfer', Transfer = 'Transfer',
Withdraw = 'Withdraw', Withdraw = 'Withdraw',
ManageFunds = 'ManageFunds',
} }
export enum TradeBoxDialogTypes { export enum TradeBoxDialogTypes {
-4
View File
@@ -13,7 +13,6 @@ export enum LocalStorageKey {
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled', PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated', PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
TransferNotifications = 'dydx.TransferNotifications', TransferNotifications = 'dydx.TransferNotifications',
NotificationPreferences = 'dydx.NotificationPreferences',
// UI State // UI State
LastViewedMarket = 'dydx.LastViewedMarket', LastViewedMarket = 'dydx.LastViewedMarket',
@@ -26,8 +25,5 @@ export enum LocalStorageKey {
export const LOCAL_STORAGE_VERSIONS = { export const LOCAL_STORAGE_VERSIONS = {
[LocalStorageKey.EvmDerivedAddresses]: 'v2', [LocalStorageKey.EvmDerivedAddresses]: 'v2',
[LocalStorageKey.NotificationPreferences]: 'v1',
[LocalStorageKey.TransferNotifications]: 'v1',
[LocalStorageKey.Notifications]: 'v1',
// TODO: version all localStorage keys // TODO: version all localStorage keys
}; };
-8
View File
@@ -6,8 +6,6 @@ import {
LOCALE_DATA, LOCALE_DATA,
TOOLTIPS, TOOLTIPS,
WARNINGS_STRING_KEYS, WARNINGS_STRING_KEYS,
NOTIFICATIONS_STRING_KEYS,
NOTIFICATIONS,
} from '@dydxprotocol/v4-localization'; } from '@dydxprotocol/v4-localization';
import { type LinksConfigs } from '@/hooks/useURLConfigs'; import { type LinksConfigs } from '@/hooks/useURLConfigs';
@@ -29,7 +27,6 @@ export enum SupportedLocales {
export const EN_LOCALE_DATA = { export const EN_LOCALE_DATA = {
...LOCALE_DATA[SupportedLocales.EN], ...LOCALE_DATA[SupportedLocales.EN],
...NOTIFICATIONS[SupportedLocales.EN],
TOOLTIPS: TOOLTIPS[SupportedLocales.EN], TOOLTIPS: TOOLTIPS[SupportedLocales.EN],
}; };
@@ -37,13 +34,8 @@ export const STRING_KEYS = {
...APP_STRING_KEYS, ...APP_STRING_KEYS,
...ERRORS_STRING_KEYS, ...ERRORS_STRING_KEYS,
...WARNINGS_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 StringKey = keyof typeof STRING_KEYS;
export type LocaleData = typeof EN_LOCALE_DATA; export type LocaleData = typeof EN_LOCALE_DATA;
-1
View File
@@ -26,7 +26,6 @@ export type MenuItem<MenuItemValue, MenuItemTypes = string> = {
tag?: React.ReactNode; tag?: React.ReactNode;
slotAfter?: React.ReactNode; slotAfter?: React.ReactNode;
description?: string; description?: string;
slotCustomContent?: React.ReactNode;
href?: string; href?: string;
onSelect?: (key: MenuItemValue) => void; onSelect?: (key: MenuItemValue) => void;
+6 -12
View File
@@ -1,18 +1,12 @@
import environments from '../../public/configs/env.json'; import environments from '../../public/configs/env.json';
const CURRENT_MODE = ({ // TODO: Update for Mainnet deployment
production: 'MAINNET', export const AVAILABLE_ENVIRONMENTS =
testnet: 'TESTNET', import.meta.env.MODE === 'production'
staging: 'DEV', ? environments.deployments.TESTNET
development: 'DEV', : environments.deployments.DEV;
}[import.meta.env.MODE] ?? 'MAINNET') as 'MAINNET' | 'TESTNET' | 'DEV';
export const isMainnet = CURRENT_MODE === 'MAINNET'; export const CURRENT_ABACUS_DEPLOYMENT = import.meta.env.MODE === 'production' ? 'TESTNET' : 'DEV';
export const isTestnet = CURRENT_MODE === 'TESTNET';
export const isDev = CURRENT_MODE === 'DEV';
export const AVAILABLE_ENVIRONMENTS = environments.deployments[CURRENT_MODE];
export const CURRENT_ABACUS_DEPLOYMENT = CURRENT_MODE;
export const ENVIRONMENT_CONFIG_MAP = environments.environments; export const ENVIRONMENT_CONFIG_MAP = environments.environments;
export type DydxNetwork = keyof typeof ENVIRONMENT_CONFIG_MAP; export type DydxNetwork = keyof typeof ENVIRONMENT_CONFIG_MAP;
export const DEFAULT_APP_ENVIRONMENT = AVAILABLE_ENVIRONMENTS.default as DydxNetwork; export const DEFAULT_APP_ENVIRONMENT = AVAILABLE_ENVIRONMENTS.default as DydxNetwork;
+8 -24
View File
@@ -2,7 +2,7 @@ import { StatusResponse } from '@0xsquid/sdk';
/** implemented in useNotificationTypes */ /** implemented in useNotificationTypes */
export enum NotificationType { export enum NotificationType {
AbacusGenerated = 'AbacusGenerated', OrderStatusChanged = 'OrderStatusChanged',
SquidTransfer = 'SquidTransfer', SquidTransfer = 'SquidTransfer',
} }
@@ -59,7 +59,7 @@ export enum NotificationStatus {
/** Toast or NotificationsMenu item interacted with or dismissed. "Seen" in NotificationsMenu. */ /** Toast or NotificationsMenu item interacted with or dismissed. "Seen" in NotificationsMenu. */
Seen, Seen,
/** Notification marked for deletion. Hidden in NotificationsMenu. */ /** Notification marked for deletion. "Archived" in NotificationsMenu. */
Cleared, Cleared,
} }
@@ -80,20 +80,14 @@ export type Notifications = Record<NotificationId, Notification<any>>;
/** Notification display data derived from app state at runtime. */ /** Notification display data derived from app state at runtime. */
export type NotificationDisplayData = { export type NotificationDisplayData = {
icon?: React.ReactNode; icon?: React.ReactNode;
title: string; // Title for Toast, Notification, and Push Notification
body?: string; // Description body for Toast, Notification, and Push Notification
slotTitleLeft?: React.ReactNode; title?: string;
slotTitleRight?: React.ReactNode;
// Overrides title/body for Notification in NotificationMenu description?: React.ReactNode;
renderCustomBody?: ({
isToast, customContent?: React.ReactNode;
notification,
}: { customMenuContent?: React.ReactNode;
isToast?: boolean;
notification: Notification;
}) => React.ReactNode; // Custom Notification
actionDescription?: string; actionDescription?: string;
@@ -128,15 +122,5 @@ export type TransferNotifcation = {
fromChainId?: string; fromChainId?: string;
toAmount?: number; toAmount?: number;
triggeredAt?: number; triggeredAt?: number;
errorCount?: number;
status?: StatusResponse; 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;
+2 -4
View File
@@ -42,7 +42,6 @@ const useAccountsContext = () => {
selectedWalletError, selectedWalletError,
evmAddress, evmAddress,
signerWagmi, signerWagmi,
publicClientWagmi,
dydxAddress: connectedDydxAddress, dydxAddress: connectedDydxAddress,
signerGraz, signerGraz,
} = useWalletConnection(); } = useWalletConnection();
@@ -136,8 +135,8 @@ const useAccountsContext = () => {
getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => { getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => {
try { try {
const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress); const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress);
setDydxSubaccounts(response?.subaccounts); setDydxSubaccounts(response.subaccounts);
return response?.subaccounts ?? []; return response.subaccounts;
} catch (error) { } catch (error) {
// 404 is expected if the user has no subaccounts // 404 is expected if the user has no subaccounts
if (error.status === 404) { if (error.status === 404) {
@@ -297,7 +296,6 @@ const useAccountsContext = () => {
// Wallet connection (EVM) // Wallet connection (EVM)
evmAddress, evmAddress,
signerWagmi, signerWagmi,
publicClientWagmi,
// Wallet connection (Cosmos) // Wallet connection (Cosmos)
signerGraz, signerGraz,
+1 -2
View File
@@ -22,7 +22,6 @@ export const useCurrentMarketId = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectedNetwork = useSelector(getSelectedNetwork); const selectedNetwork = useSelector(getSelectedNetwork);
const marketIds = useSelector(getMarketIds, shallowEqual); const marketIds = useSelector(getMarketIds, shallowEqual);
const hasMarketIds = marketIds.length > 0;
const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({ const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({
key: LocalStorageKey.LastViewedMarket, key: LocalStorageKey.LastViewedMarket,
defaultValue: DEFAULT_MARKETID, defaultValue: DEFAULT_MARKETID,
@@ -49,5 +48,5 @@ export const useCurrentMarketId = () => {
if (marketIds) { if (marketIds) {
abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID); abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID);
} }
}, [selectedNetwork, hasMarketIds, marketId]); }, [selectedNetwork, marketIds, marketId]);
}; };
+1 -2
View File
@@ -1,4 +1,3 @@
import { isDev } from '@/constants/networks';
import { WalletType } from '@/constants/wallets'; import { WalletType } from '@/constants/wallets';
import { isTruthy } from '@/lib/isTruthy'; import { isTruthy } from '@/lib/isTruthy';
@@ -7,7 +6,7 @@ export const useDisplayedWallets = () => {
return [ return [
WalletType.MetaMask, WalletType.MetaMask,
isDev && WalletType.Keplr, import.meta.env.MODE !== 'production' && WalletType.Keplr,
WalletType.WalletConnect2, WalletType.WalletConnect2,
+38 -68
View File
@@ -2,11 +2,11 @@ import { createContext, useContext, useCallback, useEffect, useMemo } from 'reac
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk'; import type { StatusResponse } from '@0xsquid/sdk';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage'; import { LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications'; import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts'; import { useAccounts } from '@/hooks/useAccounts';
import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid'; import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid';
import { useLocalStorage } from './useLocalStorage'; import { useLocalStorage } from './useLocalStorage';
const LocalNotificationsContext = createContext< const LocalNotificationsContext = createContext<
@@ -22,32 +22,16 @@ export const LocalNotificationsProvider = ({ ...props }) => (
export const useLocalNotifications = () => useContext(LocalNotificationsContext)!; export const useLocalNotifications = () => useContext(LocalNotificationsContext)!;
const TRANSFER_STATUS_FETCH_INTERVAL = 10_000; const TRANSFER_STATUS_FETCH_INTERVAL = 10_000;
const ERROR_COUNT_THRESHOLD = 3;
const useLocalNotificationsContext = () => { const useLocalNotificationsContext = () => {
// transfer notifications // transfer notifications
const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{ const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{
[key: `dydx${string}`]: TransferNotifcation[]; [key: `dydx${string}`]: TransferNotifcation[];
version: string;
}>({ }>({
key: LocalStorageKey.TransferNotifications, 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 { dydxAddress } = useAccounts();
const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : []; const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : [];
@@ -70,61 +54,47 @@ const useLocalNotificationsContext = () => {
const squid = useSquid(); const squid = useSquid();
useQuery({ const { data: transferStatuses } = useQuery({
queryKey: 'getTransactionStatus', queryKey: ['getTransactionStatus', transferNotifications],
queryFn: async () => { queryFn: async () => {
const processTransferNotifications = async (transferNotifications: TransferNotifcation[]) => { const statuses: { [key: string]: StatusResponse } = {};
const newTransferNotifications = await Promise.all( for (const {
transferNotifications.map(async (transferNotification) => { txHash,
const { toChainId,
txHash, fromChainId,
toChainId, triggeredAt,
fromChainId, status: currentStatus,
triggeredAt, } of transferNotifications) {
errorCount, try {
status: currentStatus, if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
} = transferNotification;
if ( const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
// @ts-ignore status.errors is not in the type definition but can be returned if (status) statuses[txHash] = status;
!currentStatus?.errors && } catch (error) {
!currentStatus?.error && // ignore errors for the first 120s since the status might not be available yet
(!currentStatus?.squidTransactionStatus || if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
currentStatus?.squidTransactionStatus === 'ongoing') statuses[txHash] = error;
) { }
try { }
const status = await squid?.getStatus({ }
transactionId: txHash, return statuses;
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, 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 { return {
transferNotifications, transferNotifications,
addTransferNotification, addTransferNotification,
+157 -165
View File
@@ -1,213 +1,205 @@
import { type ReactNode, useEffect } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import styled from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { isEqual, groupBy } from 'lodash'; import { 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 { DialogTypes } from '@/constants/dialogs';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; import { STRING_KEYS, StringKey } from '@/constants/localization';
import { AppRoute } from '@/constants/routes'; import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { DydxChainAsset } from '@/constants/wallets'; import { ORDER_SIDE_STRINGS } from '@/constants/trade';
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 { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { TradeNotification } from '@/views/notifications/TradeNotification'; import { Output, OutputType } from '@/components/Output';
import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification'; import { TransferStatusToast } from '@/views/TransferStatus';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors'; import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs'; import { openDialog } from '@/state/dialogs';
import { getAbacusNotifications } from '@/state/notificationsSelectors';
import { getMarketIds } from '@/state/perpetualsSelectors';
import { formatSeconds } from '@/lib/timeUtils'; import { OrderStatusIcon } from '@/views/OrderStatusIcon';
const parseStringParamsForNotification = ({ import { useStringGetter } from './useStringGetter';
stringGetter, import { TransferStatusSteps } from '@/views/TransferStatusSteps';
value, import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
}: {
stringGetter: StringGetterFunction;
value: unknown;
}): ReactNode => {
if (STRING_KEY_VALUES[value as StringKey]) {
return stringGetter({ key: value as StringKey });
}
return value as ReactNode; export const notificationTypes = [
};
export const notificationTypes: NotificationTypeConfig[] = [
{ {
type: NotificationType.AbacusGenerated, type: NotificationType.OrderStatusChanged,
useTrigger: ({ trigger }) => {
useTrigger: ({ trigger, lastUpdated }) => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const abacusNotifications = useSelector(getAbacusNotifications, isEqual);
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]
);
useEffect(() => { useEffect(() => {
for (const abacusNotif of abacusNotifications) { for (const orderId of orderIds) {
const [abacusNotificationType = '', id = ''] = abacusNotif.id.split(':'); const fills = fillsByOrderId[orderId];
const parsedData = abacusNotif.data ? JSON.parse(abacusNotif.data) : {};
const params = Object.fromEntries( const order =
Object.entries(parsedData).map(([key, value]) => { ordersByOrderId[orderId] ??
return [key, parseStringParamsForNotification({ stringGetter, value })]; (fills?.length
}) ? {
); ...fills[fills.length - 1],
id: orderId,
switch (abacusNotificationType) { createdAtMilliseconds: Math.max(
case 'order': { ...fills.map((fill) => fill.createdAtMilliseconds)
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,
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data], }
true : undefined);
);
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;
}
}
}, [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();
return (notificationId: string) => { if (order)
const [abacusNotificationType = '', id = ''] = notificationId.split(':'); trigger(
order.id,
if (ordersById[id]) { {
dispatch( icon: (
openDialog({ <OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />
type: DialogTypes.OrderDetails, ),
dialogProps: { orderId: id }, title: `${stringGetter({
}) key: order.resources.typeStringKey as StringKey,
); })} ${
} else if (fillsById[id]) { order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0
dispatch( ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
openDialog({ : stringGetter({ key: order.resources.statusStringKey as StringKey })
type: DialogTypes.FillDetails, }`,
dialogProps: { fillId: id }, description: `${stringGetter({
}) key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
); })} ${order.size} ${order.marketId} @ $${order.price}`,
} else if (marketIds.includes(id)) { actionDescription: 'View Order',
navigate(`${AppRoute.Trade}/${id}`, { actionAltText: 'View this order in the Orders tab or the Notifications menu.',
replace: true, toastSensitivity:
}); order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
toastDuration: 5000,
},
[order.status.name, order.size],
!order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated
);
} }
}; }, [orderIds]);
}, },
},
// useNotificationAction: () => {
// const dispatch = useDispatch();
// 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);
// 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]>,
{ {
type: NotificationType.SquidTransfer, type: NotificationType.SquidTransfer,
useTrigger: ({ trigger }) => { useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { transferNotifications } = useLocalNotifications(); 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(() => { useEffect(() => {
for (const transfer of transferNotifications) { for (const transfer of transferNotifications) {
const { fromChainId, status, txHash, toAmount } = transfer; const { fromChainId, status, txHash, toAmount } = transfer;
const isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing'; const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const icon = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />; const type = fromChainId === TESTNET_CHAIN_ID ? '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 type = // TODO: confirm with design what the description should be
fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId const description = (
? 'withdrawal' <div>
: 'deposit'; <Styled.TransferText>
{type === 'deposit' ? 'Deposit of ' : 'Withdraw of '}
<Output type={OutputType.Fiat} value={toAmount} />
</Styled.TransferText>
const title = stringGetter({ {error && (
key: { <Styled.ErrorMessage type={AlertType.Error}>
deposit: isFinished ? STRING_KEYS.DEPOSIT : STRING_KEYS.DEPOSIT_IN_PROGRESS, {stringGetter({
withdrawal: isFinished ? STRING_KEYS.WITHDRAW : STRING_KEYS.WITHDRAW_IN_PROGRESS, key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
}[type], params: {
}); ERROR_MESSAGE:
error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0; },
const estimatedDuration = formatSeconds(Math.max(toChainEta, 0)); })}
const body = stringGetter({ </Styled.ErrorMessage>
key: STRING_KEYS.DEPOSIT_STATUS, )}
params: { </div>
AMOUNT_USD: `${toAmount} ${DydxChainAsset.USDC.toUpperCase()}`, );
ESTIMATED_DURATION: estimatedDuration,
},
});
trigger( trigger(
txHash, txHash,
{ {
icon, icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title, title: stringGetter({ key: getTitleStringKey(type, finished) }),
body, description: description,
renderCustomBody: ({ isToast, notification }) => ( customContent: (
<TransferStatusNotification <TransferStatusToast
isToast={isToast}
slotIcon={icon}
slotTitle={title}
transfer={transfer}
type={type} type={type}
toAmount={transfer.toAmount}
triggeredAt={transfer.triggeredAt} triggeredAt={transfer.triggeredAt}
notification={notification} status={transfer.status}
/> />
), ),
customMenuContent: !finished && (
<div>
{description}
<TransferStatusSteps status={transfer.status} type={type} />
</div>
),
toastSensitivity: 'foreground', toastSensitivity: 'foreground',
}, },
[isFinished] []
); );
} }
}, [transferNotifications, stringGetter]); }, [transferNotifications]);
},
useNotificationAction: () => {
return () => {};
}, },
}, },
]; ] satisfies NotificationTypeConfig[];
const $Icon = styled.img` const Styled: Record<string, AnyStyledComponent> = {};
height: 1.5rem;
width: 1.5rem; Styled.TransferText = styled.span`
display: inline-flex;
align-items: center;
gap: 0.5ch;
`;
Styled.ErrorMessage = styled.div`
max-width: 13rem;
`; `;
+36 -81
View File
@@ -1,13 +1,11 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage'; import { LocalStorageKey } from '@/constants/localStorage';
import { import {
type Notification, type Notification,
type NotificationDisplayData, type NotificationDisplayData,
type NotificationPreferences,
type Notifications, type Notifications,
NotificationStatus, NotificationStatus,
NotificationType,
} from '@/constants/notifications'; } from '@/constants/notifications';
import { useLocalStorage } from './useLocalStorage'; import { useLocalStorage } from './useLocalStorage';
@@ -15,10 +13,8 @@ import { notificationTypes } from './useNotificationTypes';
import { renderSvgToDataUrl } from '../lib/renderSvgToDataUrl'; import { renderSvgToDataUrl } from '../lib/renderSvgToDataUrl';
type NotificationsContextType = ReturnType<typeof useNotificationsContext>; const NotificationsContext = createContext<ReturnType<typeof useNotificationsContext> | undefined>(
undefined
const NotificationsContext = createContext<NotificationsContextType>(
{} as NotificationsContextType
); );
NotificationsContext.displayName = 'Notifications'; NotificationsContext.displayName = 'Notifications';
@@ -31,7 +27,6 @@ export const useNotifications = () => useContext(NotificationsContext)!;
const useNotificationsContext = () => { const useNotificationsContext = () => {
// Local storage // Local storage
// const [notifications, setNotifications] = useState<Notifications>({});
const [notifications, setNotifications] = useLocalStorage<Notifications>({ const [notifications, setNotifications] = useLocalStorage<Notifications>({
key: LocalStorageKey.Notifications, key: LocalStorageKey.Notifications,
defaultValue: {}, defaultValue: {},
@@ -42,25 +37,12 @@ const useNotificationsContext = () => {
defaultValue: Date.now(), 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(() => { useEffect(() => {
setNotificationsLastUpdated(Date.now()); setNotificationsLastUpdated(Date.now());
}, [notifications]); }, [notifications]);
const getKey = useCallback( const getKey = <T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
<T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) => `${notification.type}/${notification.id}`;
`${notification.type}/${notification.id}`,
[]
);
// Display data // Display data
const [notificationsDisplayData, setNotificationsDisplayData] = useState( const [notificationsDisplayData, setNotificationsDisplayData] = useState(
@@ -72,46 +54,29 @@ const useNotificationsContext = () => {
[notificationsDisplayData] [notificationsDisplayData]
); );
// Check for version changes
useEffect(() => {
if (
notificationPreferences.version !==
LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences]
) {
setNotificationPreferences({
[NotificationType.AbacusGenerated]: true,
[NotificationType.SquidTransfer]: true,
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
});
}
}, []);
// Status changes // Status changes
const updateStatus = useCallback( const updateStatus = useCallback(
(notification: Notification, status: NotificationStatus) => { (notification: Notification, status: NotificationStatus) => {
notification.status = status; notification.status = status;
notification.timestamps[notification.status] = Date.now(); notification.timestamps[notification.status] = Date.now();
setNotifications({ ...notifications, [getKey(notification)]: notification }); setNotifications({ ...notifications });
}, },
[notifications, getKey] [notifications]
); );
const { markUnseen, markSeen, markCleared } = useMemo( const { markUnseen, markSeen, markCleared } = useMemo(
() => ({ () => ({
markUnseen: (notification: Notification) => { markUnseen: (notification: Notification) => {
if (notification.status < NotificationStatus.Unseen) { if (notification.status < NotificationStatus.Unseen)
updateStatus(notification, NotificationStatus.Unseen); updateStatus(notification, NotificationStatus.Unseen);
}
}, },
markSeen: (notification: Notification) => { markSeen: (notification: Notification) => {
if (notification.status < NotificationStatus.Seen) { if (notification.status < NotificationStatus.Seen)
updateStatus(notification, NotificationStatus.Seen); updateStatus(notification, NotificationStatus.Seen);
}
}, },
markCleared: (notification: Notification) => { markCleared: (notification: Notification) => {
if (notification.status < NotificationStatus.Cleared) { if (notification.status < NotificationStatus.Cleared)
updateStatus(notification, NotificationStatus.Cleared); updateStatus(notification, NotificationStatus.Cleared);
}
}, },
}), }),
[updateStatus] [updateStatus]
@@ -132,37 +97,33 @@ const useNotificationsContext = () => {
const notification = notifications[key]; const notification = notifications[key];
// Filter out notifications that are not enabled // New unique key - create new notification
if (notificationPreferences[type] !== false) { if (!notification) {
// New unique key - create new notification const notification = (notifications[key] = {
if (!notification) { id,
const notification = (notifications[key] = { type,
id, timestamps: {},
type, updateKey,
timestamps: {}, } as Notification);
updateKey,
} as Notification);
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
} else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
// updateKey changed - update existing notification
const notification = notifications[key]; updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
}
notification.updateKey = updateKey; // updateKey changed - update existing notification
updateStatus(notification, NotificationStatus.Updated); else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
} const notification = notifications[key];
} else {
// Notification is disabled - remove it notification.updateKey = updateKey;
delete notifications[key]; updateStatus(notification, NotificationStatus.Updated);
} }
notificationsDisplayData[key] = displayData; notificationsDisplayData[key] = displayData;
setNotificationsDisplayData({ ...notificationsDisplayData }); setNotificationsDisplayData({ ...notificationsDisplayData });
}, },
[notifications, updateStatus, notificationPreferences[type]] [notifications, updateStatus]
), ),
lastUpdated: notificationsLastUpdated, lastUpdated: notificationsLastUpdated,
@@ -175,8 +136,10 @@ const useNotificationsContext = () => {
) )
); );
const onNotificationAction = async (notification: Notification) => const onNotificationAction = useCallback(
await actions[notification.type]?.(notification.id); async (notification: Notification) => await actions[notification.type]?.(notification.id),
[actions]
);
// Push notifications // Push notifications
const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({ const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({
@@ -223,7 +186,7 @@ const useNotificationsContext = () => {
renotify: true, renotify: true,
tag: getKey(notification), tag: getKey(notification),
data: notification, data: notification,
description: displayData.body, body: displayData.description,
icon: iconUrl ?? '/favicon.svg', icon: iconUrl ?? '/favicon.svg',
badge: iconUrl ?? '/favicon.svg', badge: iconUrl ?? '/favicon.svg',
image: iconUrl ?? '/favicon.svg', image: iconUrl ?? '/favicon.svg',
@@ -245,7 +208,7 @@ const useNotificationsContext = () => {
setPushNotificationsLastUpdated(Date.now()); setPushNotificationsLastUpdated(Date.now());
})(); })();
}, [hasEnabledPush, notifications, markSeen]); }, [hasEnabledPush, notifications, onNotificationAction, markSeen]);
// Menu state // Menu state
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -276,13 +239,5 @@ const useNotificationsContext = () => {
// Menu state // Menu state
isMenuOpen, isMenuOpen,
setIsMenuOpen, setIsMenuOpen,
// Notification Preferences
notificationPreferences,
setNotificationPreferences,
getNotificationPreferenceForType: useCallback(
(type: NotificationType) => notificationPreferences[type],
[notificationPreferences]
),
}; };
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ import { getSelectedNetwork } from '@/state/appSelectors';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 300_000; export const STATUS_ERROR_GRACE_PERIOD = 120_000;
const useSquidContext = () => { const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork); const selectedNetwork = useSelector(getSelectedNetwork);
+6
View File
@@ -350,6 +350,11 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
placeOrderParams = abacusStateManager.placeOrder(callback); placeOrderParams = abacusStateManager.placeOrder(callback);
} }
track(AnalyticsEvent.TradePlaceOrder, {
...placeOrderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
return placeOrderParams; return placeOrderParams;
}, },
[subaccountClient] [subaccountClient]
@@ -378,6 +383,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
}) => { }) => {
const callback = (success: boolean, parsingError?: Nullable<ParsingError>) => { const callback = (success: boolean, parsingError?: Nullable<ParsingError>) => {
if (success) { if (success) {
track(AnalyticsEvent.TradeCancelOrder);
onSuccess?.(); onSuccess?.();
} else { } else {
onError?.({ errorStringKey: parsingError?.stringKey }); onError?.({ errorStringKey: parsingError?.stringKey });
-46
View File
@@ -1,46 +0,0 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import queryString from 'query-string';
class TestFlags {
public queryParams: queryString.ParsedQuery<string>;
constructor() {
this.queryParams = {};
}
setQueryParams(flags: queryString.ParsedQuery<string>) {
this.queryParams = flags;
}
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
}
export const testFlags = new TestFlags();
const useTestFlagsContext = () => {
const [queryParams, setQueryParams] = useState<queryString.ParsedQuery<string>>({});
const location = useLocation();
useEffect(() => {
const parsedQueryParams = queryString.parse(location.search.substring(-1));
testFlags.setQueryParams(parsedQueryParams);
setQueryParams(parsedQueryParams);
}, []);
return {
...queryParams,
};
};
type TestFlagsContextType = ReturnType<typeof useTestFlagsContext>;
const TestFlagsContext = createContext<TestFlagsContextType>({} as TestFlagsContextType);
TestFlagsContext.displayName = 'TestFlags';
export const TestFlagsProvider = ({ ...props }) => (
<TestFlagsContext.Provider value={useTestFlagsContext()} {...props} />
);
export const useTestFlags = () => useContext(TestFlagsContext);
+9 -11
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState, useMemo } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { LocalStorageKey } from '@/constants/localStorage'; import { LocalStorageKey } from '@/constants/localStorage';
@@ -90,21 +90,19 @@ export const useWalletConnection = () => {
const selectedNetwork = useSelector(getSelectedNetwork); const selectedNetwork = useSelector(getSelectedNetwork);
const walletConnectConfig = ENVIRONMENT_CONFIG_MAP[selectedNetwork].wallets.walletconnect; const walletConnectConfig = ENVIRONMENT_CONFIG_MAP[selectedNetwork].wallets.walletconnect;
const wagmiConnector = useMemo(
() => const { connectAsync: connectWagmi } =
walletType && walletConnectionType walletType && walletConnectionType
? resolveWagmiConnector({ ? useConnectWagmi({
connector: resolveWagmiConnector({
walletType, walletType,
walletConnection: { walletConnection: {
type: walletConnectionType, type: walletConnectionType,
}, },
walletConnectConfig, walletConnectConfig,
}) }),
: undefined, })
[walletConnectConfig, walletType, walletConnectionType] : useConnectWagmi();
);
const { connectAsync: connectWagmi } = useConnectWagmi({ connector: wagmiConnector })
const { suggestAndConnect: connectGraz } = useConnectGraz(); const { suggestAndConnect: connectGraz } = useConnectGraz();
const connectWallet = useCallback( const connectWallet = useCallback(
-6
View File
@@ -1,6 +0,0 @@
<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>

Before

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"> <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="2" stroke-linecap="round" <path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 235 B

-1
View File
@@ -10,7 +10,6 @@ export { default as CautionCircleIcon } from './caution-circle.svg';
export { default as CautionCircleStrokeIcon } from './caution-circle-stroke.svg'; export { default as CautionCircleStrokeIcon } from './caution-circle-stroke.svg';
export { default as ChatIcon } from './chat-bubble.svg'; export { default as ChatIcon } from './chat-bubble.svg';
export { default as CheckIcon } from './check.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 ChevronLeftIcon } from './chevron-left.svg';
export { default as ChevronRightIcon } from './chevron-right.svg'; export { default as ChevronRightIcon } from './chevron-right.svg';
export { default as ClockIcon } from './clock.svg'; export { default as ClockIcon } from './clock.svg';
+4 -12
View File
@@ -1,13 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g id="Wallet" clip-path="url(#clip0_14122_34311)"> <g fill="#FFFFFF">
<g id="Group 151"> <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"/>
<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"/> </g>
<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> </svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 405 B

-4
View File
@@ -15,7 +15,6 @@ import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog';
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog'; import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog'; import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog'; import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
import { PreferencesDialog } from '@/views/dialogs/PreferencesDialog';
import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog'; import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog'; import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog'; import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog';
@@ -23,7 +22,6 @@ import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog'; import { TransferDialog } from '@/views/dialogs/TransferDialog';
import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog'; import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog';
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog'; import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
import { ManageFundsDialog } from '@/views/dialogs/ManageFundsDialog';
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog'; import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog'; import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
@@ -58,7 +56,6 @@ export const DialogManager = () => {
[DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />, [DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />,
[DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />, [DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />,
[DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />, [DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />,
[DialogTypes.Preferences]: <PreferencesDialog {...modalProps} />,
[DialogTypes.RateLimit]: <RateLimitDialog {...modalProps} />, [DialogTypes.RateLimit]: <RateLimitDialog {...modalProps} />,
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />, [DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
[DialogTypes.RestrictedGeo]: <RestrictedGeoDialog {...modalProps} />, [DialogTypes.RestrictedGeo]: <RestrictedGeoDialog {...modalProps} />,
@@ -66,6 +63,5 @@ export const DialogManager = () => {
[DialogTypes.Trade]: <TradeDialog {...modalProps} />, [DialogTypes.Trade]: <TradeDialog {...modalProps} />,
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />, [DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />, [DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
[DialogTypes.ManageFunds]: <ManageFundsDialog {...modalProps} />,
}[type]; }[type];
}; };
+2 -2
View File
@@ -3,7 +3,7 @@ import styled, { type AnyStyledComponent, css } from 'styled-components';
import { AbacusApiStatus } from '@/constants/abacus'; import { AbacusApiStatus } from '@/constants/abacus';
import { ButtonSize } from '@/constants/buttons'; import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP, isDev } from '@/constants/networks'; import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { useApiState, useSelectedNetwork, useStringGetter } from '@/hooks'; import { useApiState, useSelectedNetwork, useStringGetter } from '@/hooks';
import { ChatIcon, LinkOutIcon } from '@/icons'; import { ChatIcon, LinkOutIcon } from '@/icons';
@@ -75,7 +75,7 @@ export const FooterDesktop = () => {
)} )}
</Styled.Row> </Styled.Row>
{isDev && ( {import.meta.env.MODE !== 'production' && (
<Styled.Details <Styled.Details
withSeparators withSeparators
items={[ items={[
-1
View File
@@ -236,5 +236,4 @@ Styled.IconButton = styled(IconButton)<{ size?: string }>`
${headerMixins.button} ${headerMixins.button}
--button-border: none; --button-border: none;
--button-icon-size: 1rem; --button-icon-size: 1rem;
--button-padding: 0 0.5em;
`; `;
+37 -64
View File
@@ -1,22 +1,19 @@
import { useMemo } from 'react';
import styled, { css } from 'styled-components';
import { NotificationStatus } from '@/constants/notifications'; import { NotificationStatus } from '@/constants/notifications';
import { ButtonSize } from '@/constants/buttons'; import { ButtonSize } from '@/constants/buttons';
import { useNotifications } from '@/hooks/useNotifications'; import { useNotifications } from '@/hooks/useNotifications';
import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useBreakpoints } from '@/hooks/useBreakpoints';
import { Button } from '@/components/Button';
import { Toast } from '@/components/Toast';
import { ToastArea } from '@/components/ToastArea'; import { ToastArea } from '@/components/ToastArea';
import { Toast } from '@/components/Toast';
import { Button } from '@/components/Button';
import styled from 'styled-components';
type StyleProps = { type StyleProps = {
className?: string; className?: string;
}; };
const MAX_TOASTS = 10;
export const NotificationsToastArea = ({ className }: StyleProps) => { export const NotificationsToastArea = ({ className }: StyleProps) => {
const { const {
notifications, notifications,
@@ -29,9 +26,10 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
} = useNotifications(); } = useNotifications();
const { isMobile } = useBreakpoints(); const { isMobile } = useBreakpoints();
const notificationMap = useMemo(() => {
return ( return (
Object.values(notifications) <StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{Object.values(notifications)
// Sort by time of first trigger // Sort by time of first trigger
.sort( .sort(
(n1, n2) => (n1, n2) =>
@@ -44,69 +42,44 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
displayData: getDisplayData(notification), displayData: getDisplayData(notification),
})) }))
.filter(({ displayData }) => displayData) .filter(({ displayData }) => displayData)
.slice(-MAX_TOASTS) .map(({ notification, key, displayData }) => (
); <Toast
}, [notifications, getKey, getDisplayData]); key={key}
isOpen={notification.status < NotificationStatus.Unseen}
if (isMenuOpen) return null; slotIcon={displayData.icon}
slotTitle={displayData.title}
return ( slotDescription={displayData.description}
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}> slotCustomContent={displayData.customContent}
{notificationMap.map(({ notification, key, displayData }, idx) => ( slotAction={
<StyledToast <Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
key={key} {displayData.actionDescription}
layer={notificationMap.length - 1 - idx} </Button>
isOpen={notification.status < NotificationStatus.Unseen} }
notification={notification} actionDescription={displayData.actionDescription}
slotIcon={displayData.icon} actionAltText={displayData.actionAltText}
slotTitle={displayData.title} duration={isMenuOpen ? Infinity : displayData.toastDuration}
slotTitleLeft={displayData.slotTitleLeft} sensitivity={displayData.toastSensitivity}
slotTitleRight={displayData.slotTitleRight} setIsOpen={(isOpen, isClosedFromTimeout) => {
slotDescription={displayData.body} if (!isOpen)
slotCustomContent={displayData.renderCustomBody?.({ isToast: true, notification })} if (isClosedFromTimeout)
onClick={() => onNotificationAction(notification)} // Toast timer expired without user interaction
slotAction={ markUnseen(notification);
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}> // Toast interacted with or dismissed
{displayData.actionDescription} else markSeen(notification);
</Button> }}
} lastUpdated={notification.timestamps[notification.status]}
actionDescription={displayData.actionDescription} />
actionAltText={displayData.actionAltText} ))}
duration={displayData.toastDuration ?? Infinity}
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]}
/>
))}
</StyledToastArea> </StyledToastArea>
); );
}; };
const StyledToastArea = styled(ToastArea)` const StyledToastArea = styled(ToastArea)`
position: absolute; position: absolute;
width: min(17.5rem, 100%); width: min(16rem, 100%);
inset: 0 0 0 auto; inset: 0 0 0 auto;
padding: 0.75rem 0.75rem 0.75rem 0; padding: 0.75rem 0.75rem 0.75rem 0;
mask-image: linear-gradient(to left, transparent, white 0.5rem); mask-image: linear-gradient(to left, transparent, white 0.5rem);
`; `;
const StyledToast = styled(Toast)<{ layer: number }>`
// Stacked toast
position: absolute;
top: 0;
left: 0;
right: 0;
${({ layer }) => css`
right: calc(${layer} * -2px);
top: calc(${layer} * 2px);
`}
`;
+17 -22
View File
@@ -1,7 +1,6 @@
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus'; import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long'; import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate'; import type { IndexedTx } from '@cosmjs/stargate';
import { encodeJson } from '@dydxprotocol/v4-client-js';
import { import {
CompositeClient, CompositeClient,
@@ -31,7 +30,7 @@ import {
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade'; import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade';
import { ENVIRONMENT_CONFIG_MAP, DydxNetwork, isTestnet } from '@/constants/networks'; import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { RootStore } from '@/state/_store'; import { RootStore } from '@/state/_store';
import { addUncommittedOrderClientId, removeUncommittedOrderClientId } from '@/state/account'; import { addUncommittedOrderClientId, removeUncommittedOrderClientId } from '@/state/account';
@@ -196,19 +195,14 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
throw new StatefulOrderError('Stateful order has failed to commit.', tx); throw new StatefulOrderError('Stateful order has failed to commit.', tx);
} }
const encodedTx = encodeJson(tx); const parsedTx = this.parseToPrimitives(tx);
const parsedTx = JSON.parse(encodedTx); const hash = parsedTx?.hash;
const hash = parsedTx.hash.toUpperCase();
if (isTestnet) { if (import.meta.env.MODE === 'production') {
console.log( console.log(`https://testnet.mintscan.io/dydx-testnet/txs/${hash}`);
`${ENVIRONMENT_CONFIG_MAP[
this.compositeClient.network.getString() as DydxNetwork
]?.links?.mintscan?.replace('{tx_hash}', hash.toString())}`
);
} else console.log(`txHash: ${hash}`); } else console.log(`txHash: ${hash}`);
return encodedTx; return JSON.stringify(parsedTx);
} catch (error) { } catch (error) {
if (error?.name !== 'BroadcastError') { if (error?.name !== 'BroadcastError') {
log('DydxChainTransactions/placeOrderTransaction', error); log('DydxChainTransactions/placeOrderTransaction', error);
@@ -235,17 +229,12 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
orderFlags, orderFlags,
clobPairId, clobPairId,
goodTilBlock || undefined, goodTilBlock || undefined,
goodTilBlockTime || undefined goodTilBlockTime || undefined,
); );
const encodedTx = encodeJson(tx); const parsedTx = this.parseToPrimitives(tx);
if (import.meta.env.MODE === 'development') { return JSON.stringify(parsedTx);
const parsedTx = JSON.parse(encodedTx);
console.log(parsedTx, parsedTx.hash.toUpperCase());
}
return encodedTx;
} catch (error) { } catch (error) {
log('DydxChainTransactions/cancelOrderTransaction', error); log('DydxChainTransactions/cancelOrderTransaction', error);
@@ -306,11 +295,15 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
if (!this.localWallet) { if (!this.localWallet) {
throw new Error('Missing compositeClient or localWallet'); throw new Error('Missing compositeClient or localWallet');
} }
const msg = compositeClient?.sendTokenMessage(this.localWallet, amount, recipient); const msg = compositeClient?.sendTokenMessage(
this.localWallet,
amount,
recipient,
);
resolve([msg]); resolve([msg]);
}), }),
this.compositeClient?.validatorClient?.post.defaultDydxGasPrice this.compositeClient?.validatorClient?.post.defaultDydxGasPrice,
); );
const parsedTx = this.parseToPrimitives(tx); const parsedTx = this.parseToPrimitives(tx);
@@ -441,6 +434,8 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parseDelegations = this.parseToPrimitives(delegations); const parseDelegations = this.parseToPrimitives(delegations);
callback(JSON.stringify(parseDelegations)); callback(JSON.stringify(parseDelegations));
break; break;
// Do not implement Transfers (yet)
case QueryType.Transfers:
default: default:
break; break;
} }
+5
View File
@@ -1,5 +1,10 @@
import type { AbacusFileSystemProtocol, FileLocation, Nullable } from '@/constants/abacus'; import type { AbacusFileSystemProtocol, FileLocation, Nullable } from '@/constants/abacus';
export const ENDPOINTS_PATH =
import.meta.env.MODE === 'production'
? 'config/prod/endpoints.json'
: 'config/staging/dev_endpoints.json';
class AbacusFileSystem implements AbacusFileSystemProtocol { class AbacusFileSystem implements AbacusFileSystemProtocol {
readTextFile(location: FileLocation, path: string): Nullable<string> { readTextFile(location: FileLocation, path: string): Nullable<string> {
return null; return null;
+1 -1
View File
@@ -41,7 +41,7 @@ import AbacusStateNotifier from './stateNotification';
import AbacusLocalizer from './localizer'; import AbacusLocalizer from './localizer';
import AbacusFormatter from './formatter'; import AbacusFormatter from './formatter';
import AbacusThreading from './threading'; import AbacusThreading from './threading';
import AbacusFileSystem from './filesystem'; import AbacusFileSystem, { ENDPOINTS_PATH } from './filesystem';
import { LocaleSeparators } from '../numbers'; import { LocaleSeparators } from '../numbers';
class AbacusStateManager { class AbacusStateManager {
private store: RootStore | undefined; private store: RootStore | undefined;
+7 -2
View File
@@ -56,8 +56,13 @@ class AbacusRest implements AbacusRestProtocol {
.then(async (response) => { .then(async (response) => {
const data = await response.text(); const data = await response.text();
callback(data, response.status); if (response.ok) {
callback(data, response.status);
} else {
// response not OK, call callback with null data and the status, this includes 400/500 status codes
callback(null, response.status);
}
try { try {
lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now(); lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now();
} catch {} } catch {}
-2
View File
@@ -35,7 +35,6 @@ import { setApiState } from '@/state/app';
import { setAssets } from '@/state/assets'; import { setAssets } from '@/state/assets';
import { setConfigs } from '@/state/configs'; import { setConfigs } from '@/state/configs';
import { setInputs } from '@/state/inputs'; import { setInputs } from '@/state/inputs';
import { updateNotifications } from '@/state/notifications';
import { setHistoricalFundings, setLiveTrades, setMarkets, setOrderbook } from '@/state/perpetuals'; import { setHistoricalFundings, setLiveTrades, setMarkets, setOrderbook } from '@/state/perpetuals';
import { isTruthy } from '../isTruthy'; import { isTruthy } from '../isTruthy';
@@ -51,7 +50,6 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
} }
notificationsChanged(notifications: kollections.List<AbacusNotification>): void { notificationsChanged(notifications: kollections.List<AbacusNotification>): void {
this.store?.dispatch(updateNotifications(notifications.toArray()));
return; return;
} }
+4 -23
View File
@@ -1,6 +1,5 @@
import type { AbacusWebsocketProtocol } from '@/constants/abacus'; import type { AbacusWebsocketProtocol } from '@/constants/abacus';
import type { TradingViewBar } from '@/constants/candles'; import type { TradingViewBar } from '@/constants/candles';
import { isDev } from '@/constants/networks';
import { import {
PING_INTERVAL_MS, PING_INTERVAL_MS,
@@ -9,12 +8,11 @@ import {
PONG_MESSAGE_TYPE, PONG_MESSAGE_TYPE,
} from '@/constants/websocket'; } from '@/constants/websocket';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { testFlags } from '@/hooks/useTestFlags';
import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache'; import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache';
import { mapCandle } from '@/lib/tradingView/utils'; import { mapCandle } from '@/lib/tradingView/utils';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { log } from '../telemetry'; import { log } from '../telemetry';
const RECONNECT_INTERVAL_MS = 10_000; const RECONNECT_INTERVAL_MS = 10_000;
@@ -145,23 +143,6 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
break; break;
} }
case 'v4_markets': {
if (testFlags.displayInitializingMarkets) {
shouldProcess = false;
const { contents } = parsedMessage;
Object.keys(contents.markets ?? {}).forEach((market: any) => {
const status = contents.markets[market].status;
if (status === 'INITIALIZING') {
contents.markets[market].status = 'ONLINE';
}
});
this.receivedCallback?.(JSON.stringify(parsedMessage));
}
break;
}
default: { default: {
break; break;
} }
@@ -180,13 +161,13 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
this.socket.onclose = (e) => { this.socket.onclose = (e) => {
this.connectedCallback?.(false); this.connectedCallback?.(false);
if (!isDev) return; if (import.meta.env.MODE === 'production') return;
console.warn('AbacusStateManager > WS > close > ', e); console.warn('AbacusStateManager > WS > close > ', e);
}; };
this.socket.onerror = (e) => { this.socket.onerror = (e) => {
this.connectedCallback?.(false); this.connectedCallback?.(false);
if (!isDev) return; if (import.meta.env.MODE === 'production') return;
console.error('AbacusStateManager > WS > error > ', e); console.error('AbacusStateManager > WS > error > ', e);
}; };
}; };
+1 -3
View File
@@ -1,7 +1,5 @@
import { isDev } from "@/constants/networks";
export const log = (location: string, error: Error, metadata?: any) => { export const log = (location: string, error: Error, metadata?: any) => {
if (isDev) { if (import.meta.env.MODE !== 'production') {
console.warn('telemetry/log:', { location, error, metadata }); console.warn('telemetry/log:', { location, error, metadata });
} }
+5 -8
View File
@@ -51,14 +51,11 @@ const Profile = () => {
icon: { iconName: IconName.Gear }, icon: { iconName: IconName.Gear },
href: AppRoute.Settings, href: AppRoute.Settings,
}, },
isConnected && { // {
key: 'transfers', // key: 'tutorials',
label: stringGetter({ key: STRING_KEYS.MANAGE_FUNDS }), // label: stringGetter({ key: STRING_KEYS.TUTORIALS }),
icon: { iconName: IconName.Transfer }, // icon: { iconName: IconName.Comment },
onClick: () => { // },
dispatch(openDialog({ type: DialogTypes.ManageFunds }));
},
},
isConnected isConnected
? { ? {
key: 'sign-out', key: 'sign-out',
File diff suppressed because it is too large Load Diff
+4
View File
@@ -304,6 +304,10 @@ Styled.FeeTable = styled(Table)`
@media ${breakpoints.notTablet} { @media ${breakpoints.notTablet} {
--tableHeader-backgroundColor: var(--color-layer-1); --tableHeader-backgroundColor: var(--color-layer-1);
thead tr {
--border-width: 0;
}
} }
`; `;
+42 -104
View File
@@ -1,13 +1,11 @@
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { useEffect } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { OnboardingState } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { HistoryRoute, PortfolioRoute } from '@/constants/routes'; import { HistoryRoute, PortfolioRoute } from '@/constants/routes';
import { useAccountBalance, useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks'; import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable'; import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable';
@@ -16,9 +14,6 @@ import { Icon, IconName } from '@/components/Icon';
import { NavigationMenu } from '@/components/NavigationMenu'; import { NavigationMenu } from '@/components/NavigationMenu';
import { WithSidebar } from '@/components/WithSidebar'; import { WithSidebar } from '@/components/WithSidebar';
import { getOnboardingState, getSubaccount } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { PortfolioNavMobile } from './PortfolioNavMobile'; import { PortfolioNavMobile } from './PortfolioNavMobile';
import { Overview } from './Overview'; import { Overview } from './Overview';
import { Positions } from './Positions'; import { Positions } from './Positions';
@@ -27,20 +22,11 @@ import { Fees } from './Fees';
import { History } from './History'; import { History } from './History';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { Button } from '@/components/Button';
import { ButtonAction } from '@/constants/buttons';
export default () => { export default () => {
const dispatch = useDispatch();
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { isTablet, isNotTablet } = useBreakpoints(); const { isTablet, isNotTablet } = useBreakpoints();
const onboardingState = useSelector(getOnboardingState);
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { nativeTokenBalance } = useAccountBalance();
const usdcBalance = freeCollateral?.current || 0;
useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO })); useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO }));
const routesComponent = ( const routesComponent = (
@@ -98,74 +84,46 @@ export default () => {
<WithSidebar <WithSidebar
sidebar={ sidebar={
isTablet ? null : ( isTablet ? null : (
<Styled.SideBar> <Styled.NavigationMenu
<Styled.NavigationMenu items={[
items={[ {
{ group: 'views',
group: 'views', groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }), items: [
items: [ {
{ value: PortfolioRoute.Overview,
value: PortfolioRoute.Overview, slotBefore: <Styled.Icon iconName={IconName.Overview} />,
slotBefore: <Styled.Icon iconName={IconName.Overview} />, label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
label: stringGetter({ key: STRING_KEYS.OVERVIEW }), href: PortfolioRoute.Overview,
href: PortfolioRoute.Overview, },
}, {
{ value: PortfolioRoute.Positions,
value: PortfolioRoute.Positions, slotBefore: <Styled.Icon iconName={IconName.Cube} />,
slotBefore: <Styled.Icon iconName={IconName.Cube} />, label: stringGetter({ key: STRING_KEYS.POSITIONS }),
label: stringGetter({ key: STRING_KEYS.POSITIONS }), href: PortfolioRoute.Positions,
href: PortfolioRoute.Positions, },
}, {
{ value: PortfolioRoute.Orders,
value: PortfolioRoute.Orders, slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />, label: stringGetter({ key: STRING_KEYS.ORDERS }),
label: stringGetter({ key: STRING_KEYS.ORDERS }), href: PortfolioRoute.Orders,
href: PortfolioRoute.Orders, },
}, {
{ value: PortfolioRoute.Fees,
value: PortfolioRoute.Fees, slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
slotBefore: <Styled.Icon iconName={IconName.Calculator} />, label: stringGetter({ key: STRING_KEYS.FEES }),
label: stringGetter({ key: STRING_KEYS.FEES }), href: PortfolioRoute.Fees,
href: PortfolioRoute.Fees, },
}, {
{ value: PortfolioRoute.History,
value: PortfolioRoute.History, slotBefore: <Styled.Icon iconName={IconName.History} />,
slotBefore: <Styled.Icon iconName={IconName.History} />, label: stringGetter({ key: STRING_KEYS.HISTORY }),
label: stringGetter({ key: STRING_KEYS.HISTORY }), href: PortfolioRoute.History,
href: PortfolioRoute.History, },
}, ],
], },
}, ]}
]} />
/>
{onboardingState === OnboardingState.AccountConnected && (
<Styled.Footer>
<Button
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
</Button>
{usdcBalance > 0 && (
<Button
action={ButtonAction.Base}
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
)}
{(usdcBalance > 0 || nativeTokenBalance.gt(0)) && (
<Button
action={ButtonAction.Base}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
)}
</Styled.Footer>
)}
</Styled.SideBar>
) )
} }
> >
@@ -181,26 +139,6 @@ Styled.PortfolioMobile = styled.div`
${layoutMixins.expandingColumnWithHeader} ${layoutMixins.expandingColumnWithHeader}
`; `;
Styled.SideBar = styled.div`
${layoutMixins.flexColumn}
justify-content: space-between;
height: 100%;
`;
Styled.Footer = styled.div`
${layoutMixins.row}
flex-wrap: wrap;
padding: 1rem;
gap: 0.5rem;
> button {
flex-grow: 1;
}
`;
Styled.NavigationMenu = styled(NavigationMenu)` Styled.NavigationMenu = styled(NavigationMenu)`
padding: 0.5rem; padding: 0.5rem;
padding-top: 0; padding-top: 0;
+17 -8
View File
@@ -43,14 +43,23 @@ export const DYDXBalancePanel = () => {
{!canAccountTrade ? ( {!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Small} /> <OnboardingTriggerButton size={ButtonSize.Small} />
) : ( ) : (
<Button <>
slotLeft={<Icon iconName={IconName.Send} />} <Styled.ReceiveButton
size={ButtonSize.Small} slotLeft={<Icon iconName={IconName.Qr} />}
action={ButtonAction.Primary} size={ButtonSize.Small}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))} onClick={() => dispatch(openDialog({ type: DialogTypes.Receive }))}
> >
{stringGetter({ key: STRING_KEYS.TRANSFER })} {stringGetter({ key: STRING_KEYS.RECEIVE })}
</Button> </Styled.ReceiveButton>
<Button
slotLeft={<Icon iconName={IconName.Send} />}
size={ButtonSize.Small}
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
</>
)} )}
</Styled.ReceiveAndTransferButtons> </Styled.ReceiveAndTransferButtons>
</Styled.Header> </Styled.Header>
-3
View File
@@ -1,7 +1,6 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import abacusStateManager from '@/lib/abacus'; import abacusStateManager from '@/lib/abacus';
import appMiddleware from './appMiddleware'; import appMiddleware from './appMiddleware';
import localizationMiddleware from './localizationMiddleware'; import localizationMiddleware from './localizationMiddleware';
@@ -14,7 +13,6 @@ import { inputsSlice } from './inputs';
import { layoutSlice } from './layout'; import { layoutSlice } from './layout';
import { localizationSlice } from './localization'; import { localizationSlice } from './localization';
import { perpetualsSlice } from './perpetuals'; import { perpetualsSlice } from './perpetuals';
import { notificationsSlice } from './notifications';
export const commandMenuSlices = [layoutSlice, localizationSlice]; export const commandMenuSlices = [layoutSlice, localizationSlice];
@@ -28,7 +26,6 @@ export const store = configureStore({
inputs: inputsSlice.reducer, inputs: inputsSlice.reducer,
layout: layoutSlice.reducer, layout: layoutSlice.reducer,
localization: localizationSlice.reducer, localization: localizationSlice.reducer,
notifications: notificationsSlice.reducer,
perpetuals: perpetualsSlice.reducer, perpetuals: perpetualsSlice.reducer,
}, },
+1 -2
View File
@@ -2,7 +2,6 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { AbacusApiStatus } from '@/constants/abacus'; import { AbacusApiStatus } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { isDev } from '@/constants/networks';
import { setApiState, setSelectedNetwork } from '@/state/app'; import { setApiState, setSelectedNetwork } from '@/state/app';
import { resetPerpetualsState } from '@/state/perpetuals'; import { resetPerpetualsState } from '@/state/perpetuals';
@@ -31,7 +30,7 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
store.dispatch( store.dispatch(
openDialog({ openDialog({
type: DialogTypes.ExchangeOffline, type: DialogTypes.ExchangeOffline,
dialogProps: { preventClose: !isDev }, dialogProps: { preventClose: import.meta.env.MODE === 'production' },
}) })
); );
} }
-9
View File
@@ -48,15 +48,6 @@ export const getTradeInputErrors = (state: RootState) => {
return currentInput === 'trade' ? getInputErrors(state) : []; return currentInput === 'trade' ? getInputErrors(state) : [];
}; };
/**
* @param state
* @returns input errors for Close position
*/
export const getClosePositionInputErrors = (state: RootState) => {
const currentInput = state.inputs.current;
return currentInput === 'closePosition' ? getInputErrors(state) : [];
};
/** /**
* @param state * @param state
* @returns input errors for Transfer * @returns input errors for Transfer
+1 -7
View File
@@ -1,10 +1,5 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { import { LOCALE_DATA, SupportedLocale, TOOLTIPS } from '@dydxprotocol/v4-localization';
LOCALE_DATA,
NOTIFICATIONS,
SupportedLocale,
TOOLTIPS,
} from '@dydxprotocol/v4-localization';
import { import {
type LocaleData, type LocaleData,
@@ -31,7 +26,6 @@ const getNewLocaleData = ({
const newLocaleData = { const newLocaleData = {
...LOCALE_DATA[locale], ...LOCALE_DATA[locale],
...NOTIFICATIONS[locale],
TOOLTIPS: TOOLTIPS[locale], TOOLTIPS: TOOLTIPS[locale],
}; };
-24
View File
@@ -1,24 +0,0 @@
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
@@ -1,3 +0,0 @@
import { RootState } from './_store';
export const getAbacusNotifications = (state: RootState) => state.notifications.abacusNotifications;
-29
View File
@@ -107,17 +107,12 @@ export const formMixins: Record<
`, `,
inputLabel: css` inputLabel: css`
--label-textColor: var(--color-text-1);
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
gap: 0; gap: 0;
border-radius: inherit; border-radius: inherit;
@media ${breakpoints.tablet} {
gap: 0.25rem;
}
`, `,
inputToggleGroup: css` inputToggleGroup: css`
@@ -135,28 +130,4 @@ export const formMixins: Record<
--button-toggle-off-textColor: var(--color-text-1); --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;
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); --trigger-open-textColor: var(--color-text-2);
&:hover:not(:disabled) { &:hover:not(:disabled) {
--trigger-backgroundColor: var(--trigger-hover-backgroundColor); background-color: var(--trigger-hover-backgroundColor);
--trigger-textColor: var(--trigger-hover-textColor); color: var(--trigger-hover-textColor);
} }
`, `,
+4
View File
@@ -18,5 +18,9 @@ export const tradeViewMixins: Record<
tbody { tbody {
font: var(--font-small-book); font: var(--font-small-book);
} }
thead tr {
box-shadow: none;
}
`, `,
}; };
+5 -5
View File
@@ -82,18 +82,18 @@ const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string
), ),
}, },
{ {
columnKey: 'priceChange24HPercent', columnKey: 'priceChange24H',
getCellValue: (row) => row.priceChange24HPercent, getCellValue: (row) => row.priceChange24H,
label: stringGetter({ key: STRING_KEYS._24H }), label: stringGetter({ key: STRING_KEYS._24H }),
renderCell: ({ priceChange24HPercent }) => ( renderCell: ({ priceChange24H, priceChange24HPercent }) => (
<Styled.InlineRow> <Styled.InlineRow>
{!priceChange24HPercent ? ( {!priceChange24H ? (
<Styled.Output type={OutputType.Text} value={null} /> <Styled.Output type={OutputType.Text} value={null} />
) : ( ) : (
<Styled.PriceChangeOutput <Styled.PriceChangeOutput
type={OutputType.Percent} type={OutputType.Percent}
value={priceChange24HPercent} value={priceChange24HPercent}
isNegative={MustBigNumber(priceChange24HPercent).isNegative()} isNegative={MustBigNumber(priceChange24H).isNegative()}
/> />
)} )}
</Styled.InlineRow> </Styled.InlineRow>
+40 -49
View File
@@ -1,7 +1,6 @@
import styled from 'styled-components'; import { type OrderStatus, AbacusOrderStatus } from '@/constants/abacus';
import { AbacusOrderStatus } from '@/constants/abacus';
import { Icon } from '@/components/Icon';
import { import {
OrderCanceledIcon, OrderCanceledIcon,
OrderFilledIcon, OrderFilledIcon,
@@ -10,57 +9,49 @@ import {
OrderPendingIcon, OrderPendingIcon,
} from '@/icons'; } from '@/icons';
import { Icon } from '@/components/Icon'; import styled from 'styled-components';
type ElementProps = { type ElementProps = {
status: string; status: OrderStatus;
totalFilled: number; totalFilled: number;
}; };
type StyleProps = { export const OrderStatusIcon = ({ status, totalFilled }: ElementProps) => (
className?: string; <$Icon
}; {...{
[AbacusOrderStatus.open.name]:
export const OrderStatusIcon = ({ className, status, totalFilled }: ElementProps & StyleProps) => { totalFilled > 0
const { iconComponent, color } = { ? {
[AbacusOrderStatus.open.rawValue]: iconComponent: OrderPartiallyFilledIcon,
totalFilled > 0 color: 'var(--color-warning)',
? { }
iconComponent: OrderPartiallyFilledIcon, : {
color: 'var(--color-warning)', iconComponent: OrderOpenIcon,
} color: 'var(--color-text-2)',
: { },
iconComponent: OrderOpenIcon, [AbacusOrderStatus.filled.name]: {
color: 'var(--color-text-2)', iconComponent: OrderFilledIcon,
}, color: 'var(--color-success)',
[AbacusOrderStatus.partiallyFilled.rawValue]: { },
iconComponent: OrderPartiallyFilledIcon, [AbacusOrderStatus.cancelled.name]: {
color: 'var(--color-warning)', iconComponent: OrderCanceledIcon,
}, color: 'var(--color-error)',
[AbacusOrderStatus.filled.rawValue]: { },
iconComponent: OrderFilledIcon, [AbacusOrderStatus.canceling.name]: {
color: 'var(--color-success)', iconComponent: OrderPendingIcon,
}, color: 'var(--color-error)',
[AbacusOrderStatus.cancelled.rawValue]: { },
iconComponent: OrderCanceledIcon, [AbacusOrderStatus.pending.name]: {
color: 'var(--color-error)', iconComponent: OrderPendingIcon,
}, color: 'var(--color-text-2)',
[AbacusOrderStatus.canceling.rawValue]: { },
iconComponent: OrderPendingIcon, [AbacusOrderStatus.untriggered.name]: {
color: 'var(--color-error)', iconComponent: OrderPendingIcon,
}, color: 'var(--color-text-2)',
[AbacusOrderStatus.pending.rawValue]: { },
iconComponent: OrderPendingIcon, }[status.name]}
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 }>` const $Icon = styled(Icon)<{ color: string }>`
color: ${({ color }) => color}; color: ${({ color }) => color};
+176
View File
@@ -0,0 +1,176 @@
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 | undefined>();
// @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 statusString =
type === 'deposit'
? status?.squidTransactionStatus === 'success'
? STRING_KEYS.DEPOSIT_COMPLETE
: STRING_KEYS.DEPOSIT_STATUS
: status?.squidTransactionStatus === 'success'
? STRING_KEYS.WITHDRAW_COMPLETE
: STRING_KEYS.WITHDRAW_STATUS;
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;
}
`;
@@ -1,8 +1,8 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import styled, { css, type AnyStyledComponent } from 'styled-components'; import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components';
import { StatusResponse } from '@0xsquid/sdk'; import { StatusResponse } from '@0xsquid/sdk';
import { useStringGetter, useSelectedNetwork, useURLConfigs } from '@/hooks'; import { useStringGetter } from '@/hooks';
import { Link } from '@/components/Link'; import { Link } from '@/components/Link';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
@@ -11,17 +11,12 @@ import { LoadingSpinner } from '@/components/Loading/LoadingSpinner';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
type ElementProps = { type ElementProps = {
status?: StatusResponse; status?: StatusResponse;
type: 'withdrawal' | 'deposit'; type: 'withdrawal' | 'deposit';
}; };
type StyleProps = {
className?: string;
};
enum TransferStatusStep { enum TransferStatusStep {
FromChain, FromChain,
Bridge, Bridge,
@@ -29,19 +24,14 @@ enum TransferStatusStep {
Complete, Complete,
} }
export const TransferStatusSteps = ({ className, status, type }: ElementProps & StyleProps) => { export const TransferStatusSteps = ({ status, type }: ElementProps) => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { selectedNetwork } = useSelectedNetwork();
const { mintscan: mintscanTxUrl } = useURLConfigs();
const dydxChainId = ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId;
const { currentStep, steps } = useMemo(() => { const { currentStep, steps } = useMemo(() => {
const routeStatus = status?.routeStatus; const routeStatus = status?.routeStatus;
const fromChain = status?.fromChain?.chainData?.chainId; const fromChain = status?.fromChain?.chainData?.chainId;
const toChain = status?.toChain?.chainData?.chainId; const toChain = status?.toChain?.chainData?.chainId;
const currentStatus = routeStatus?.[routeStatus?.length - 1];
const steps = [ const steps = [
{ {
label: stringGetter({ label: stringGetter({
@@ -49,12 +39,7 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL, type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
}), }),
step: TransferStatusStep.FromChain, step: TransferStatusStep.FromChain,
link: link: status?.fromChain?.transactionUrl,
type === 'deposit'
? status?.fromChain?.transactionUrl
: routeStatus?.[0]?.chainId === dydxChainId && routeStatus[0].txHash
? `${mintscanTxUrl?.replace('{tx_hash}', routeStatus[0].txHash.replace('0x', ''))}`
: undefined,
}, },
{ {
label: stringGetter({ key: STRING_KEYS.BRIDGING_TOKENS }), label: stringGetter({ key: STRING_KEYS.BRIDGING_TOKENS }),
@@ -69,15 +54,12 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
}, },
}), }),
step: TransferStatusStep.ToChain, step: TransferStatusStep.ToChain,
link: link: status?.toChain?.transactionUrl,
type === 'withdrawal'
? status?.toChain?.transactionUrl
: currentStatus?.chainId === dydxChainId && currentStatus?.txHash
? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash.replace('0x', ''))}`
: undefined,
}, },
]; ];
const currentStatus = routeStatus?.[routeStatus?.length - 1];
let currentStep = TransferStatusStep.Bridge; let currentStep = TransferStatusStep.Bridge;
if (!routeStatus?.length) { if (!routeStatus?.length) {
@@ -105,7 +87,7 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
if (!status) return <LoadingDots size={3} />; if (!status) return <LoadingDots size={3} />;
return ( return (
<Styled.BridgingStatus className={className}> <Styled.BridgingStatus>
{steps.map((step) => ( {steps.map((step) => (
<Styled.Step key={step.step}> <Styled.Step key={step.step}>
<Styled.row> <Styled.row>
+39 -2
View File
@@ -1,15 +1,22 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks'; import { useBreakpoints, useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog'; import { Dialog, DialogPlacement } from '@/components/Dialog';
import { DepositDialogContent } from './DepositDialog/DepositDialogContent'; import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = { type ElementProps = {
setIsOpen?: (open: boolean) => void; setIsOpen?: (open: boolean) => void;
}; };
export const DepositDialog = ({ setIsOpen }: ElementProps) => { export const DepositDialog = ({ setIsOpen }: ElementProps) => {
const [showTestDeposit, setShowTestDeposit] = useState(true);
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { isMobile } = useBreakpoints(); const { isMobile } = useBreakpoints();
@@ -20,9 +27,39 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
isOpen isOpen
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
title={stringGetter({ key: STRING_KEYS.DEPOSIT })} title={stringGetter({ key: STRING_KEYS.DEPOSIT })}
description={showTestDeposit && 'Test funds will be sent directly to your dYdX account.'}
placement={isMobile ? DialogPlacement.FullScreen : DialogPlacement.Default} placement={isMobile ? DialogPlacement.FullScreen : DialogPlacement.Default}
> >
<DepositDialogContent onDeposit={closeDialog} /> <Styled.Content>
{showTestDeposit ? <TestnetDepositForm onDeposit={closeDialog} /> : <DepositForm />}
<Styled.TextToggle onClick={() => setShowTestDeposit(!showTestDeposit)}>
{showTestDeposit ? 'Show deposit form (Under Construction)' : 'Show test faucet'}
</Styled.TextToggle>
</Styled.Content>
</Dialog> </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;
`;
@@ -1,56 +0,0 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { isMainnet } from '@/constants/networks';
import { layoutMixins } from '@/styles/layoutMixins';
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
type ElementProps = {
onDeposit?: () => void;
};
export const DepositDialogContent = ({ onDeposit }: ElementProps) => {
const [showFaucet, setShowFaucet] = useState(!isMainnet);
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;
}
`;
+1 -2
View File
@@ -5,7 +5,6 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { AbacusApiStatus } from '@/constants/abacus'; import { AbacusApiStatus } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { isDev } from '@/constants/networks';
import { useApiState, useStringGetter } from '@/hooks'; import { useApiState, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
@@ -46,7 +45,7 @@ export const ExchangeOfflineDialog = ({ preventClose, setIsOpen }: ElementProps)
> >
<Styled.Content> <Styled.Content>
<p>{statusErrorMessage}</p> <p>{statusErrorMessage}</p>
{isDev && <NetworkSelectMenu />} {import.meta.env.MODE !== 'production' && <NetworkSelectMenu />}
</Styled.Content> </Styled.Content>
</Dialog> </Dialog>
); );
-86
View File
@@ -1,86 +0,0 @@
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 = {
setIsOpen?: (open: boolean) => void;
};
export const ManageFundsDialog = ({ setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const { type } = useSelector(getTransferInputs, shallowEqual) || {};
const currentType = type?.rawValue ?? 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;
}
`;
+11 -7
View File
@@ -5,7 +5,7 @@ import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { useAccounts, useStringGetter } from '@/hooks'; import { useAccounts, useStringGetter } from '@/hooks';
import breakpoints from '@/styles/breakpoints'; import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { AlertMessage } from '@/components/AlertMessage'; import { AlertMessage } from '@/components/AlertMessage';
@@ -54,14 +54,11 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => {
<Styled.WithReceipt <Styled.WithReceipt
slotReceipt={ slotReceipt={
<Styled.CheckboxContainer> <Styled.CheckboxContainer>
<Checkbox <Styled.Checkbox
checked={hasAcknowledged} checked={hasAcknowledged}
onCheckedChange={setHasAcknowledged} onClick={() => setHasAcknowledged(!hasAcknowledged)}
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> </Styled.CheckboxContainer>
} }
> >
@@ -165,10 +162,17 @@ Styled.WithReceipt = styled(WithReceipt)`
`; `;
Styled.CheckboxContainer = styled.div` Styled.CheckboxContainer = styled.div`
${layoutMixins.spacedRow};
padding: 1rem; padding: 1rem;
gap: 0.75rem;
color: var(--color-text-0); color: var(--color-text-0);
`; `;
Styled.Checkbox = styled(Checkbox)`
--checkbox-backgroundColor: var(--color-layer-3);
`;
Styled.AlertMessage = styled(AlertMessage)` Styled.AlertMessage = styled(AlertMessage)`
font: var(--font-base-book); font: var(--font-base-book);
margin: 0; margin: 0;
+11 -18
View File
@@ -3,7 +3,6 @@ import styled, { AnyStyledComponent, css } from 'styled-components';
import { AnalyticsEvent } from '@/constants/analytics'; import { AnalyticsEvent } from '@/constants/analytics';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { isMainnet } from '@/constants/networks';
import { EvmDerivedAccountStatus, OnboardingSteps } from '@/constants/account'; import { EvmDerivedAccountStatus, OnboardingSteps } from '@/constants/account';
import { wallets } from '@/constants/wallets'; import { wallets } from '@/constants/wallets';
@@ -27,7 +26,6 @@ import { track } from '@/lib/analytics';
import { AcknowledgeTerms } from './OnboardingDialog/AcknowledgeTerms'; import { AcknowledgeTerms } from './OnboardingDialog/AcknowledgeTerms';
import { ChooseWallet } from './OnboardingDialog/ChooseWallet'; import { ChooseWallet } from './OnboardingDialog/ChooseWallet';
import { GenerateKeys } from './OnboardingDialog/GenerateKeys'; import { GenerateKeys } from './OnboardingDialog/GenerateKeys';
import { DepositForm } from '../forms/AccountManagementForms/DepositForm';
type ElementProps = { type ElementProps = {
setIsOpen?: (open: boolean) => void; setIsOpen?: (open: boolean) => void;
@@ -52,7 +50,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
disconnect(); disconnect();
} }
setIsOpen?.(open); setIsOpen?.(open);
}; }
return ( return (
<Styled.Dialog <Styled.Dialog
@@ -84,7 +82,10 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
description: stringGetter({ key: STRING_KEYS.SIGNATURE_CREATES_COSMOS_WALLET }), description: stringGetter({ key: STRING_KEYS.SIGNATURE_CREATES_COSMOS_WALLET }),
children: ( children: (
<Styled.Content> <Styled.Content>
<GenerateKeys status={derivationStatus} setStatus={setDerivationStatus} /> <GenerateKeys
status={derivationStatus}
setStatus={setDerivationStatus}
/>
</Styled.Content> </Styled.Content>
), ),
width: '23rem', width: '23rem',
@@ -100,22 +101,14 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
}, },
[OnboardingSteps.DepositFunds]: { [OnboardingSteps.DepositFunds]: {
title: stringGetter({ key: STRING_KEYS.DEPOSIT }), title: stringGetter({ key: STRING_KEYS.DEPOSIT }),
description: !isMainnet && 'Test funds will be sent directly to your dYdX account.', description: 'Test funds will be sent directly to your dYdX account.',
children: ( children: (
<Styled.Content> <Styled.Content>
{isMainnet ? ( <TestnetDepositForm
<DepositForm onDeposit={() => {
onDeposit={() => { track(AnalyticsEvent.TransferFaucet);
track(AnalyticsEvent.TransferDeposit); }}
}} />
/>
) : (
<TestnetDepositForm
onDeposit={() => {
track(AnalyticsEvent.TransferFaucet);
}}
/>
)}
</Styled.Content> </Styled.Content>
), ),
}, },
-86
View File
@@ -1,86 +0,0 @@
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),
},
],
}),
[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;
`;
@@ -1,13 +1,11 @@
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { isDev } from '@/constants/networks';
import { useStringGetter } from '@/hooks'; import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { Dialog } from '@/components/Dialog'; import { Dialog } from '@/components/Dialog';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
type ElementProps = { type ElementProps = {
preventClose?: boolean; preventClose?: boolean;
@@ -27,7 +25,6 @@ export const RestrictedGeoDialog = ({ preventClose, setIsOpen }: ElementProps) =
> >
<Styled.Content> <Styled.Content>
{stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })} {stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })}
{isDev && <NetworkSelectMenu />}
</Styled.Content> </Styled.Content>
</Dialog> </Dialog>
); );
@@ -1,13 +1,11 @@
import styled, { AnyStyledComponent } from 'styled-components'; import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { isDev } from '@/constants/networks';
import { useStringGetter } from '@/hooks'; import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { Dialog } from '@/components/Dialog'; import { Dialog } from '@/components/Dialog';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
type ElementProps = { type ElementProps = {
preventClose?: boolean; preventClose?: boolean;
@@ -27,7 +25,6 @@ export const RestrictedWalletDialog = ({ preventClose, setIsOpen }: ElementProps
> >
<Styled.Content> <Styled.Content>
{stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })} {stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })}
{isDev && <NetworkSelectMenu />}
</Styled.Content> </Styled.Content>
</Dialog> </Dialog>
); );
+2
View File
@@ -112,6 +112,8 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>` Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>`
--dialog-backgroundColor: var(--color-layer-2); --dialog-backgroundColor: var(--color-layer-2);
--dialog-header-height: 1rem; --dialog-header-height: 1rem;
--dialog-header-paddingTop: 1.5rem;
--dialog-header-paddingBottom: 1rem;
--dialog-content-paddingTop: 0; --dialog-content-paddingTop: 0;
--dialog-content-paddingBottom: 0; --dialog-content-paddingBottom: 0;
--dialog-content-paddingLeft: 0; --dialog-content-paddingLeft: 0;
@@ -2,22 +2,22 @@ import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import { type NumberFormatValues } from 'react-number-format'; import { type NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import { parseUnits } from 'viem'; import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { parseUnits } from 'viem'
import erc20 from '@/abi/erc20.json'; import erc20 from '@/abi/erc20.json';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus'; import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts'; import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons'; import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NotificationStatus } from '@/constants/notifications';
import { NumberSign } from '@/constants/numbers'; import { NumberSign } from '@/constants/numbers';
import type { EvmAddress } from '@/constants/wallets'; import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks'; import { useAccounts, useDebounce, useStringGetter } from '@/hooks';
import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance'; import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications'; import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid'; import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins'; import { formMixins } from '@/styles/formMixins';
@@ -54,9 +54,9 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork();
const { evmAddress, signerWagmi, publicClientWagmi } = useAccounts(); const { evmAddress, signerWagmi } = useAccounts();
const { publicClientWagmi } = useWalletConnection();
const { addTransferNotification } = useLocalNotifications(); const { addTransferNotification } = useLocalNotifications();
@@ -66,8 +66,6 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
chain: chainIdStr, chain: chainIdStr,
resources, resources,
summary, summary,
errors: routeErrors,
errorMessage: routeErrorMessage,
} = useSelector(getTransferInputs, shallowEqual) || {}; } = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined; const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
@@ -100,7 +98,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
useEffect(() => { useEffect(() => {
const hasInvalidInput = const hasInvalidInput =
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gt(balanceBN); debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gte(balanceBN);
abacusStateManager.setTransferValue({ abacusStateManager.setTransferValue({
value: hasInvalidInput ? 0 : debouncedAmount, value: hasInvalidInput ? 0 : debouncedAmount,
@@ -174,8 +172,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const validateTokenApproval = useCallback(async () => { const validateTokenApproval = useCallback(async () => {
if (!signerWagmi || !publicClientWagmi) throw new Error('Missing signer'); if (!signerWagmi || !publicClientWagmi) throw new Error('Missing signer');
if (!sourceToken?.address || !sourceToken.decimals) if (!sourceToken?.address || !sourceToken.decimals) throw new Error('Missing source token address');
throw new Error('Missing source token address');
if (!sourceChain?.rpc) throw new Error('Missing source chain rpc'); if (!sourceChain?.rpc) throw new Error('Missing source chain rpc');
if (!requestPayload?.targetAddress) throw new Error('Missing target address'); if (!requestPayload?.targetAddress) throw new Error('Missing target address');
if (!requestPayload?.value) throw new Error('Missing transaction value'); if (!requestPayload?.value) throw new Error('Missing transaction value');
@@ -185,11 +182,11 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
address: sourceToken.address as EvmAddress, address: sourceToken.address as EvmAddress,
abi: erc20, abi: erc20,
functionName: 'allowance', functionName: 'allowance',
args: [evmAddress as EvmAddress, requestPayload.targetAddress as EvmAddress], args: [evmAddress as EvmAddress, requestPayload.targetAddress as EvmAddress]
}); });
const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals); const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals);
if (sourceAmountBN > (allowance as bigint)) { if (sourceAmountBN > (allowance as bigint)) {
const { request } = await publicClientWagmi.simulateContract({ const { request } = await publicClientWagmi.simulateContract({
account: evmAddress, account: evmAddress,
@@ -197,12 +194,12 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
abi: erc20, abi: erc20,
functionName: 'approve', functionName: 'approve',
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN], args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
}); })
const approveTx = await signerWagmi.writeContract(request); const approveTx = await signerWagmi.writeContract(request);
await publicClientWagmi.waitForTransactionReceipt({ await publicClientWagmi.waitForTransactionReceipt({
hash: approveTx, hash: approveTx,
}); })
} }
}, [signerWagmi, sourceToken, sourceChain, requestPayload, publicClientWagmi]); }, [signerWagmi, sourceToken, sourceChain, requestPayload, publicClientWagmi]);
@@ -232,7 +229,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
to: requestPayload.targetAddress as EvmAddress, to: requestPayload.targetAddress as EvmAddress,
data: requestPayload.data as EvmAddress, data: requestPayload.data as EvmAddress,
gasLimit: BigInt(requestPayload.gasLimit), gasLimit: BigInt(requestPayload.gasLimit),
value: requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined, value:
requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined,
}; };
const txHash = await signerWagmi.sendTransaction(tx); const txHash = await signerWagmi.sendTransaction(tx);
@@ -241,11 +239,10 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (txHash) { if (txHash) {
addTransferNotification({ addTransferNotification({
txHash: txHash, txHash: txHash,
toChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId, toChainId: TESTNET_CHAIN_ID,
fromChainId: chainIdStr || undefined, fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined, toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(), triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
}); });
abacusStateManager.clearTransferInputValues(); abacusStateManager.clearTransferInputValues();
setFromAmount(''); setFromAmount('');
@@ -291,15 +288,6 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
return parseWalletError({ error, stringGetter }).message; return parseWalletError({ error, stringGetter }).message;
} }
if (routeErrors) {
return routeErrorMessage
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: routeErrorMessage },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (fromAmount) { if (fromAmount) {
if (!chainId) { if (!chainId) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN }); return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
@@ -313,16 +301,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
} }
return undefined; return undefined;
}, [ }, [error, balance, chainId, fromAmount, sourceToken]);
error,
routeErrors,
routeErrorMessage,
balance,
chainId,
fromAmount,
sourceToken,
stringGetter,
]);
const isDisabled = const isDisabled =
Boolean(errorMessage) || Boolean(errorMessage) ||
@@ -353,17 +332,14 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
/> />
</Styled.WithDetailsReceipt> </Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>} {errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<DepositButtonAndReceipt
<Styled.Footer> isDisabled={isDisabled}
<DepositButtonAndReceipt isLoading={isLoading}
isDisabled={isDisabled} chainId={chainId || undefined}
isLoading={isLoading} setSlippage={onSetSlippage}
chainId={chainId || undefined} slippage={slippage}
setSlippage={onSetSlippage} sourceToken={sourceToken || undefined}
slippage={slippage} />
sourceToken={sourceToken || undefined}
/>
</Styled.Footer>
</Styled.Form> </Styled.Form>
); );
}; };
@@ -371,12 +347,13 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form` Styled.Form = styled.form`
${formMixins.transfersForm} --form-input-height: 3.5rem;
`;
Styled.Footer = styled.footer` ${layoutMixins.flexColumn}
${formMixins.footer} gap: 1.5rem;
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
${layoutMixins.stickyArea1}
min-height: calc(100% - var(--stickyArea0-bottomHeight));
`; `;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)` Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
@@ -28,9 +28,7 @@ import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag'; import { Tag } from '@/components/Tag';
import { ToggleButton } from '@/components/ToggleButton'; import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt'; import { WithReceipt } from '@/components/WithReceipt';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccountBuyingPower, getSubaccountEquity } from '@/state/accountSelectors'; import { getSubaccountBuyingPower, getSubaccountEquity } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors'; import { getTransferInputs } from '@/state/inputsSelectors';
@@ -66,8 +64,6 @@ export const DepositButtonAndReceipt = ({
const [isEditingSlippage, setIsEditingSlipapge] = useState(false); const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const { const {
matchNetwork: switchNetwork, matchNetwork: switchNetwork,
isSwitchingNetwork, isSwitchingNetwork,
@@ -168,7 +164,6 @@ export const DepositButtonAndReceipt = ({
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>, label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
value: ( value: (
<SlippageEditor <SlippageEditor
disabled
slippage={slippage} slippage={slippage}
setIsEditing={setIsEditingSlipapge} setIsEditing={setIsEditingSlipapge}
setSlippage={setSlippage} setSlippage={setSlippage}
@@ -214,9 +209,7 @@ export const DepositButtonAndReceipt = ({
} }
slotError={slotError} slotError={slotError}
> >
{!canAccountTrade ? ( {!isMatchingNetwork ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : !isMatchingNetwork ? (
<Button <Button
action={ButtonAction.Primary} action={ButtonAction.Primary}
onClick={switchNetwork} onClick={switchNetwork}
@@ -253,7 +246,8 @@ Styled.WithReceipt = styled(WithReceipt)`
Styled.CollapsibleDetails = styled.div` Styled.CollapsibleDetails = styled.div`
${layoutMixins.column} ${layoutMixins.column}
padding: var(--form-input-paddingY) var(--form-input-paddingX); padding: 0.375rem 1rem 0.5rem;
gap: 0.5rem;
`; `;
Styled.Details = styled(Details)` Styled.Details = styled(Details)`
@@ -24,17 +24,11 @@ type ElementProps = {
slippage: number; slippage: number;
setIsEditing?: Dispatch<SetStateAction<boolean>>; setIsEditing?: Dispatch<SetStateAction<boolean>>;
setSlippage: (slippage: number) => void; setSlippage: (slippage: number) => void;
disabled?: boolean;
}; };
export type SlippageEditorProps = ElementProps; export type SlippageEditorProps = ElementProps;
export const SlippageEditor = ({ export const SlippageEditor = ({ slippage, setIsEditing, setSlippage }: SlippageEditorProps) => {
disabled,
slippage,
setIsEditing,
setSlippage,
}: SlippageEditorProps) => {
const percentSlippage = slippage * 100; const percentSlippage = slippage * 100;
const [slippageInputValue, setSlippageInputValue] = useState(percentSlippage.toString()); const [slippageInputValue, setSlippageInputValue] = useState(percentSlippage.toString());
const [editorState, setEditorState] = useState(EditorState.Viewing); const [editorState, setEditorState] = useState(EditorState.Viewing);
@@ -86,10 +80,6 @@ export const SlippageEditor = ({
} }
}; };
if (disabled) {
return <Output type={OutputType.Percent} value={slippage} />;
}
return ( return (
<Styled.WithConfirmationPopover <Styled.WithConfirmationPopover
open={editorState !== EditorState.Viewing} open={editorState !== EditorState.Viewing}
@@ -6,7 +6,7 @@ import { ButtonAction, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { useAccounts, useStringGetter, useSubaccount } from '@/hooks'; import { useAccounts, useStringGetter, useSubaccount } from '@/hooks';
import { formMixins } from '@/styles/formMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { Button } from '@/components/Button'; import { Button } from '@/components/Button';
@@ -71,11 +71,13 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
}, },
})} })}
</p> </p>
<Styled.Footer> <Styled.SubmitButton
<Button action={ButtonAction.Primary} type={ButtonType.Submit} state={{ isLoading }}> action={ButtonAction.Primary}
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })} type={ButtonType.Submit}
</Button> state={{ isLoading }}
</Styled.Footer> >
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Styled.SubmitButton>
</Styled.Form> </Styled.Form>
); );
}; };
@@ -83,14 +85,10 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form` Styled.Form = styled.form`
${formMixins.transfersForm} ${layoutMixins.column}
gap: 1rem;
`; `;
Styled.Footer = styled.footer` Styled.SubmitButton = styled(Button)`
${formMixins.footer} ${layoutMixins.stickyFooter}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
button {
--button-width: 100%;
}
`; `;
@@ -3,14 +3,13 @@ import type { ChangeEvent, FormEvent } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { type AnyStyledComponent } from 'styled-components';
import type { NumberFormatValues } from 'react-number-format'; import type { NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { isAddress } from 'viem'; import { isAddress } from 'viem';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus'; import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts'; import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons'; import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NotificationStatus } from '@/constants/notifications';
import { NumberSign } from '@/constants/numbers'; import { NumberSign } from '@/constants/numbers';
import { import {
@@ -18,7 +17,6 @@ import {
useDebounce, useDebounce,
useDydxClient, useDydxClient,
useRestrictions, useRestrictions,
useSelectedNetwork,
useStringGetter, useStringGetter,
useSubaccount, useSubaccount,
} from '@/hooks'; } from '@/hooks';
@@ -48,12 +46,12 @@ import { MustBigNumber } from '@/lib/numbers';
import { TokenSelectMenu } from './TokenSelectMenu'; import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt'; import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
import { join } from 'path';
export const WithdrawForm = () => { export const WithdrawForm = () => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork();
const { sendSquidWithdraw } = useSubaccount(); const { sendSquidWithdraw } = useSubaccount();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
@@ -69,8 +67,6 @@ export const WithdrawForm = () => {
chain: chainIdStr, chain: chainIdStr,
address: toAddress, address: toAddress,
resources, resources,
errors: routeErrors,
errorMessage: routeErrorMessage,
} = useSelector(getTransferInputs, shallowEqual) || {}; } = useSelector(getTransferInputs, shallowEqual) || {};
const isValidAddress = toAddress && isAddress(toAddress); const isValidAddress = toAddress && isAddress(toAddress);
@@ -168,11 +164,10 @@ export const WithdrawForm = () => {
const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`; const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
addTransferNotification({ addTransferNotification({
txHash: hash, txHash: hash,
fromChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId, fromChainId: TESTNET_CHAIN_ID,
toChainId: chainIdStr || undefined, toChainId: chainIdStr || undefined,
toAmount: debouncedAmountBN.toNumber(), toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(), triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
}); });
abacusStateManager.clearTransferInputValues(); abacusStateManager.clearTransferInputValues();
setWithdrawAmount(''); setWithdrawAmount('');
@@ -284,15 +279,6 @@ export const WithdrawForm = () => {
}); });
} }
if (routeErrors) {
return routeErrorMessage
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: routeErrorMessage },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS }); if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS });
if (sanctionedAddresses.has(toAddress)) if (sanctionedAddresses.has(toAddress))
@@ -315,15 +301,12 @@ export const WithdrawForm = () => {
return undefined; return undefined;
}, [ }, [
error, error,
routeErrors,
routeErrorMessage,
freeCollateralBN, freeCollateralBN,
chainIdStr, chainIdStr,
debouncedAmountBN, debouncedAmountBN,
toToken, toToken,
toAddress, toAddress,
sanctionedAddresses, sanctionedAddresses,
stringGetter,
]); ]);
const isDisabled = const isDisabled =
@@ -371,16 +354,14 @@ export const WithdrawForm = () => {
/> />
</Styled.WithDetailsReceipt> </Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>} {errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<Styled.Footer> <WithdrawButtonAndReceipt
<WithdrawButtonAndReceipt isDisabled={isDisabled}
isDisabled={isDisabled} isLoading={isLoading}
isLoading={isLoading} setSlippage={onSetSlippage}
setSlippage={onSetSlippage} slippage={slippage}
slippage={slippage} withdrawChain={chainIdStr || undefined}
withdrawChain={chainIdStr || undefined} withdrawToken={toToken || undefined}
withdrawToken={toToken || undefined} />
/>
</Styled.Footer>
</Styled.Form> </Styled.Form>
); );
}; };
@@ -392,12 +373,14 @@ Styled.DiffOutput = styled(DiffOutput)`
`; `;
Styled.Form = styled.form` Styled.Form = styled.form`
${formMixins.transfersForm} --form-input-height: 3.5rem;
`;
Styled.Footer = styled.footer` min-height: calc(100% - var(--stickyArea0-bottomHeight));
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom); ${layoutMixins.flexColumn}
gap: 1.25rem;
${layoutMixins.stickyArea1}
`; `;
Styled.DestinationRow = styled.div` Styled.DestinationRow = styled.div`
@@ -24,9 +24,7 @@ import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag'; import { Tag } from '@/components/Tag';
import { ToggleButton } from '@/components/ToggleButton'; import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt'; import { WithReceipt } from '@/components/WithReceipt';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors'; import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors'; import { getTransferInputs } from '@/state/inputsSelectors';
@@ -59,7 +57,6 @@ export const WithdrawButtonAndReceipt = ({
const { leverage } = useSelector(getSubaccount, shallowEqual) || {}; const { leverage } = useSelector(getSubaccount, shallowEqual) || {};
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {}; const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const toAmount = const toAmount =
summary?.toAmount && summary?.toAmount &&
@@ -150,7 +147,6 @@ export const WithdrawButtonAndReceipt = ({
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>, label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
value: ( value: (
<SlippageEditor <SlippageEditor
disabled
slippage={slippage} slippage={slippage}
setIsEditing={setIsEditingSlipapge} setIsEditing={setIsEditingSlipapge}
setSlippage={setSlippage} setSlippage={setSlippage}
@@ -195,20 +191,16 @@ export const WithdrawButtonAndReceipt = ({
</Styled.CollapsibleDetails> </Styled.CollapsibleDetails>
} }
> >
{!canAccountTrade ? ( <Button
<OnboardingTriggerButton size={ButtonSize.Base} /> action={ButtonAction.Primary}
) : ( type={ButtonType.Submit}
<Button state={{
action={ButtonAction.Primary} isDisabled: !isFormValid,
type={ButtonType.Submit} isLoading: (isFormValid && !requestPayload) || isLoading,
state={{ }}
isDisabled: !isFormValid, >
isLoading: (isFormValid && !requestPayload) || isLoading, {stringGetter({ key: STRING_KEYS.WITHDRAW })}
}} </Button>
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
)}
</Styled.WithReceipt> </Styled.WithReceipt>
); );
}; };
@@ -230,7 +222,8 @@ Styled.WithReceipt = styled(WithReceipt)`
Styled.CollapsibleDetails = styled.div` Styled.CollapsibleDetails = styled.div`
${layoutMixins.column} ${layoutMixins.column}
padding: var(--form-input-paddingY) var(--form-input-paddingX); padding: 0.375rem 1rem 0.5rem;
gap: 0.5rem;
`; `;
Styled.Details = styled(Details)` Styled.Details = styled(Details)`
+7 -31
View File
@@ -4,10 +4,8 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { import {
ClosePositionInputField, ClosePositionInputField,
ValidationError,
type HumanReadablePlaceOrderPayload, type HumanReadablePlaceOrderPayload,
type Nullable, type Nullable,
ErrorType,
} from '@/constants/abacus'; } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts'; import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
@@ -37,14 +35,13 @@ import { PositionPreview } from '@/views/forms/TradeForm/PositionPreview';
import { getCurrentMarketPositionData } from '@/state/accountSelectors'; import { getCurrentMarketPositionData } from '@/state/accountSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getClosePositionInputErrors, getInputClosePositionData } from '@/state/inputsSelectors'; import { getInputClosePositionData } from '@/state/inputsSelectors';
import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors'; import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors';
import { closeDialog } from '@/state/dialogs'; import { closeDialog } from '@/state/dialogs';
import { getCurrentInput } from '@/state/inputsSelectors'; import { getCurrentInput } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus'; import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
import { getTradeInputAlert } from '@/lib/tradeData';
const MAX_KEY = 'MAX'; const MAX_KEY = 'MAX';
@@ -84,38 +81,15 @@ export const ClosePositionForm = ({
const market = useSelector(getCurrentMarketId); const market = useSelector(getCurrentMarketId);
const { id } = useSelector(getCurrentMarketAssetData, shallowEqual) || {}; const { id } = useSelector(getCurrentMarketAssetData, shallowEqual) || {};
const { stepSizeDecimals, tickSizeDecimals } = const { stepSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {};
useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { size: sizeData, summary } = useSelector(getInputClosePositionData, shallowEqual) || {}; const { size: sizeData, summary } = useSelector(getInputClosePositionData, shallowEqual) || {};
const { size, percent } = sizeData || {}; const { size, percent } = sizeData || {};
const currentInput = useSelector(getCurrentInput); const currentInput = useSelector(getCurrentInput);
const closePositionInputErrors = useSelector(getClosePositionInputErrors, shallowEqual);
const currentPositionData = useSelector(getCurrentMarketPositionData, shallowEqual); const currentPositionData = useSelector(getCurrentMarketPositionData, shallowEqual);
const { size: currentPositionSize } = currentPositionData || {}; const { size: currentPositionSize } = currentPositionData || {};
const { current: currentSize } = currentPositionSize || {}; const { current: currentSize } = currentPositionSize || {};
const currentSizeBN = MustBigNumber(currentSize).abs(); const currentSizeBN = MustBigNumber(currentSize).abs();
const hasInputErrors = closePositionInputErrors?.some(
(error: ValidationError) => error.type !== ErrorType.warning
);
const inputAlert = getTradeInputAlert({
abacusInputErrors: closePositionInputErrors ?? [],
stringGetter,
stepSizeDecimals,
tickSizeDecimals,
});
let alertContent;
let alertType = AlertType.Error;
if (closePositionError) {
alertContent = closePositionError;
} else if (inputAlert) {
alertContent = inputAlert?.alertString;
alertType = inputAlert?.type;
}
useEffect(() => { useEffect(() => {
if (currentStep && currentStep !== MobilePlaceOrderSteps.EditOrder) return; if (currentStep && currentStep !== MobilePlaceOrderSteps.EditOrder) return;
@@ -208,7 +182,9 @@ export const ClosePositionForm = ({
}); });
}; };
const alertMessage = alertContent && <AlertMessage type={alertType}>{alertContent}</AlertMessage>; const alertMessage = closePositionError && (
<AlertMessage type={AlertType.Error}>{closePositionError}</AlertMessage>
);
const inputs = ( const inputs = (
<Styled.InputsColumn> <Styled.InputsColumn>
@@ -289,8 +265,8 @@ export const ClosePositionForm = ({
<PlaceOrderButtonAndReceipt <PlaceOrderButtonAndReceipt
isLoading={isClosingPosition} isLoading={isClosingPosition}
hasValidationErrors={hasInputErrors} hasValidationErrors={!size}
actionStringKey={inputAlert?.actionStringKey} actionStringKey={!size ? STRING_KEYS.ENTER_AMOUNT : undefined}
summary={summary ?? undefined} summary={summary ?? undefined}
currentStep={currentStep} currentStep={currentStep}
isClosePosition isClosePosition
+2 -2
View File
@@ -460,10 +460,10 @@ Styled.ButtonRow = styled.div`
`; `;
Styled.Footer = styled.footer` Styled.Footer = styled.footer`
${formMixins.footer} ${layoutMixins.stickyFooter}
--stickyFooterBackdrop-outsetY: var(--tradeBox-content-paddingBottom);
backdrop-filter: none; backdrop-filter: none;
${layoutMixins.column} ${layoutMixins.column}
${layoutMixins.noPointerEvents} ${layoutMixins.noPointerEvents}
margin-top: auto;
`; `;
@@ -130,38 +130,40 @@ export const AdvancedTradeOptions = () => {
</Styled.SelectMenu> </Styled.SelectMenu>
)} )}
{needsPostOnly && ( {needsPostOnly && (
<Checkbox <Styled.CheckboxField>
checked={postOnly || false} <Checkbox
onCheckedChange={(checked) => checked={postOnly || false}
abacusStateManager.setTradeValue({ onClick={() =>
value: checked, abacusStateManager.setTradeValue({
field: TradeInputField.postOnly, value: !postOnly,
}) field: TradeInputField.postOnly,
} })
id="post-only" }
label={ />
<Styled.CheckboxLabel>
<WithTooltip tooltip="post-only" side="right"> <WithTooltip tooltip="post-only" side="right">
{stringGetter({ key: STRING_KEYS.POST_ONLY })} {stringGetter({ key: STRING_KEYS.POST_ONLY })}
</WithTooltip> </WithTooltip>
} </Styled.CheckboxLabel>
/> </Styled.CheckboxField>
)} )}
{needsReduceOnly && ( {needsReduceOnly && (
<Checkbox <Styled.CheckboxField>
checked={reduceOnly || false} <Checkbox
onCheckedChange={(checked) => checked={reduceOnly || false}
abacusStateManager.setTradeValue({ onClick={() =>
value: checked, abacusStateManager.setTradeValue({
field: TradeInputField.reduceOnly, value: !reduceOnly,
}) field: TradeInputField.reduceOnly,
} })
id="reduce-only" }
label={ />
<Styled.CheckboxLabel>
<WithTooltip tooltip="reduce-only" side="right"> <WithTooltip tooltip="reduce-only" side="right">
{stringGetter({ key: STRING_KEYS.REDUCE_ONLY })} {stringGetter({ key: STRING_KEYS.REDUCE_ONLY })}
</WithTooltip> </WithTooltip>
} </Styled.CheckboxLabel>
/> </Styled.CheckboxField>
)} )}
</> </>
)} )}
@@ -172,6 +174,15 @@ export const AdvancedTradeOptions = () => {
const Styled: Record<string, AnyStyledComponent> = {}; 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)` Styled.Collapsible = styled(Collapsible)`
--trigger-backgroundColor: transparent; --trigger-backgroundColor: transparent;
--trigger-open-backgroundColor: transparent; --trigger-open-backgroundColor: transparent;
@@ -56,8 +56,7 @@ const ToggleContainer = styled(ToggleGroup)<{ value: OrderSide }>`
position: relative; position: relative;
> button { > button {
--button-toggle-on-border: none; --button-border: none;
--button-toggle-off-border: none;
--button-toggle-off-backgroundColor: transparent; --button-toggle-off-backgroundColor: transparent;
--button-toggle-on-backgroundColor: transparent; --button-toggle-on-backgroundColor: transparent;
--button-toggle-on-textColor: var(--toggle-color); --button-toggle-on-textColor: var(--toggle-color);
+66 -68
View File
@@ -61,53 +61,38 @@ export const TransferForm = ({
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {}; const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { dydxAddress } = useAccounts(); const { dydxAddress } = useAccounts();
const {
address: recipientAddress,
size,
fee,
} = useSelector(getTransferInputs, shallowEqual) || {};
const { transfer } = useSubaccount(); const { transfer } = useSubaccount();
const { nativeTokenBalance, usdcBalance } = useAccountBalance(); const { nativeTokenBalance, usdcBalance } = useAccountBalance();
const { selectedNetwork } = useSelectedNetwork(); const { selectedNetwork } = useSelectedNetwork();
const { tokensConfigs, usdcLabel, chainTokenLabel } = useTokenConfigs(); const { tokensConfigs, usdcLabel, chainTokenLabel } = useTokenConfigs();
const { // User Input
address: recipientAddress, const [asset, setAsset] = useState<DydxChainAsset>(selectedAsset);
size,
fee,
token,
} = useSelector(getTransferInputs, shallowEqual) || {};
// Form states // Form states
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// temp fix: TODO: reset fees when changing token in Abacus const balance = asset === DydxChainAsset.USDC ? freeCollateral?.current : nativeTokenBalance;
const [currentFee, setCurrentFee] = useState(fee); const newBalance =
useEffect(() => { asset === DydxChainAsset.USDC
setCurrentFee(fee); ? freeCollateral?.postOrder
}, [fee]); : MustBigNumber(nativeTokenBalance)
.minus(size?.size ?? 0)
.toNumber();
const amount = asset === DydxChainAsset.USDC ? size?.usdcSize : size?.size;
const asset = (token ?? selectedAsset) as DydxChainAsset; const showNotEnoughGasWarning = fee && asset === DydxChainAsset.USDC && usdcBalance < fee;
const isUSDCSelected = asset === DydxChainAsset.USDC;
const amount = isUSDCSelected ? size?.usdcSize : size?.size;
const showNotEnoughGasWarning = fee && isUSDCSelected && usdcBalance < fee;
const balance = isUSDCSelected ? freeCollateral?.current : nativeTokenBalance;
// BN // BN
const newBalanceBN = isUSDCSelected
? MustBigNumber(freeCollateral?.postOrder)
: nativeTokenBalance.minus(size?.size ?? 0);
const amountBN = MustBigNumber(amount); const amountBN = MustBigNumber(amount);
const balanceBN = MustBigNumber(balance); const balanceBN = MustBigNumber(balance);
const newBalanceBN = MustBigNumber(newBalance);
const onChangeAsset = (asset: DydxChainAsset) => {
setError(undefined);
setCurrentFee(undefined);
if (asset) {
abacusStateManager.setTransferValue({
value: asset,
field: TransferInputField.token,
});
}
};
useEffect(() => { useEffect(() => {
abacusStateManager.setTransferValue({ abacusStateManager.setTransferValue({
@@ -115,13 +100,19 @@ export const TransferForm = ({
field: TransferInputField.type, field: TransferInputField.type,
}); });
onChangeAsset(selectedAsset);
return () => { return () => {
abacusStateManager.clearTransferInputValues(); abacusStateManager.clearTransferInputValues();
}; };
}, []); }, []);
useEffect(() => {
setError(undefined);
abacusStateManager.setTransferValue({
value: asset,
field: TransferInputField.token,
});
}, [asset]);
const { sanctionedAddresses } = useRestrictions(); const { sanctionedAddresses } = useRestrictions();
const isAddressValid = useMemo( const isAddressValid = useMemo(
@@ -143,6 +134,11 @@ export const TransferForm = ({
setError(undefined); setError(undefined);
try { try {
// Subtract fees from amount if sending native tokens
const amountToTransfer = (
asset === DydxChainAsset.CHAINTOKEN ? amountBN.minus(fee) : amountBN
).toNumber();
const screenResults = await screenAddresses({ const screenResults = await screenAddresses({
addresses: [recipientAddress!, dydxAddress!], addresses: [recipientAddress!, dydxAddress!],
}); });
@@ -161,9 +157,9 @@ export const TransferForm = ({
); );
} else { } else {
const txResponse = await transfer( const txResponse = await transfer(
amountBN.toNumber(), amountToTransfer,
recipientAddress as string, recipientAddress as string,
tokensConfigs[asset]?.denom tokensConfigs[asset].denom,
); );
if (txResponse?.code === 0) { if (txResponse?.code === 0) {
@@ -204,7 +200,7 @@ export const TransferForm = ({
const onChangeAmount = (value?: number) => { const onChangeAmount = (value?: number) => {
abacusStateManager.setTransferValue({ abacusStateManager.setTransferValue({
value, value,
field: isUSDCSelected ? TransferInputField.usdcSize : TransferInputField.size, field: asset === DydxChainAsset.USDC ? TransferInputField.usdcSize : TransferInputField.size,
}); });
}; };
@@ -230,7 +226,7 @@ export const TransferForm = ({
value: DydxChainAsset.CHAINTOKEN, value: DydxChainAsset.CHAINTOKEN,
label: ( label: (
<Styled.InlineRow> <Styled.InlineRow>
<AssetIcon symbol={chainTokenLabel} /> {/* <AssetIcon symbol="DYDX" /> */}
{chainTokenLabel} {chainTokenLabel}
</Styled.InlineRow> </Styled.InlineRow>
), ),
@@ -253,15 +249,15 @@ export const TransferForm = ({
key: 'amount', key: 'amount',
label: ( label: (
<span> <span>
{stringGetter({ key: STRING_KEYS.AVAILABLE })} <Tag>{tokensConfigs[asset]?.name}</Tag> {stringGetter({ key: STRING_KEYS.AVAILABLE })} <Tag>{tokensConfigs[asset].name}</Tag>
</span> </span>
), ),
value: ( value: (
<DiffOutput <DiffOutput
type={OutputType.Asset} type={OutputType.Asset}
value={balanceBN} value={balanceBN.toString()}
newValue={newBalanceBN.toString()}
sign={NumberSign.Negative} sign={NumberSign.Negative}
newValue={newBalanceBN}
hasInvalidNewValue={newBalanceBN.isNegative()} hasInvalidNewValue={newBalanceBN.isNegative()}
withDiff={Boolean(amount && balance) && !amountBN.isNaN()} withDiff={Boolean(amount && balance) && !amountBN.isNaN()}
/> />
@@ -300,7 +296,7 @@ export const TransferForm = ({
}} }}
> >
<Styled.Row> <Styled.Row>
<FormInput <Styled.FormInput
id="destination" id="destination"
onInput={(e: SyntheticInputEvent) => onChangeAddress(e.target?.value)} onInput={(e: SyntheticInputEvent) => onChangeAddress(e.target?.value)}
label={ label={
@@ -344,8 +340,8 @@ export const TransferForm = ({
<Styled.SelectMenu <Styled.SelectMenu
label={stringGetter({ key: STRING_KEYS.ASSET })} label={stringGetter({ key: STRING_KEYS.ASSET })}
value={token} value={asset}
onValueChange={onChangeAsset} onValueChange={setAsset}
disabled={isLoading} disabled={isLoading}
> >
{assetOptions.map(({ value, label }) => ( {assetOptions.map(({ value, label }) => (
@@ -353,31 +349,27 @@ export const TransferForm = ({
))} ))}
</Styled.SelectMenu> </Styled.SelectMenu>
<Styled.WithDetailsReceipt side="bottom" detailItems={amountDetailItems}> <WithDetailsReceipt side="bottom" detailItems={amountDetailItems}>
<FormInput <Styled.FormInput
label={stringGetter({ key: STRING_KEYS.AMOUNT })} label={stringGetter({ key: STRING_KEYS.AMOUNT })}
type={InputType.Number} type={InputType.Number}
onChange={({ floatValue }: NumberFormatValues) => onChangeAmount(floatValue)} onChange={({ floatValue }: NumberFormatValues) => onChangeAmount(floatValue)}
value={amount ?? undefined} value={amount ?? undefined}
slotRight={ slotRight={renderFormInputButton({
isUSDCSelected && label: stringGetter({ key: STRING_KEYS.MAX }),
balanceBN.gt(0) && isInputEmpty: size?.usdcSize == null,
renderFormInputButton({ onClear: () => onChangeAmount(undefined),
label: stringGetter({ key: STRING_KEYS.MAX }), onClick: () => onChangeAmount(balanceBN.toNumber()),
isInputEmpty: size?.usdcSize == null, })}
onClear: () => onChangeAmount(undefined),
onClick: () => onChangeAmount(balanceBN.toNumber()),
})
}
disabled={isLoading} disabled={isLoading}
/> />
</Styled.WithDetailsReceipt> </WithDetailsReceipt>
{showNotEnoughGasWarning && ( {showNotEnoughGasWarning && (
<AlertMessage type={AlertType.Warning}> <AlertMessage type={AlertType.Warning}>
{stringGetter({ {stringGetter({
key: STRING_KEYS.TRANSFER_INSUFFICIENT_GAS, key: STRING_KEYS.TRANSFER_INSUFFICIENT_GAS,
params: { USDC_BALANCE: `(${usdcBalance} USDC)` }, params: { USDC_BALANCE: `(${usdcBalance})` },
})} })}
</AlertMessage> </AlertMessage>
)} )}
@@ -387,9 +379,9 @@ export const TransferForm = ({
<Styled.Footer> <Styled.Footer>
<TransferButtonAndReceipt <TransferButtonAndReceipt
selectedAsset={asset} selectedAsset={asset}
fee={currentFee || undefined} fees={fee || undefined}
isDisabled={!isAmountValid || !isAddressValid || !currentFee || isLoading} isDisabled={!isAmountValid || !isAddressValid || !fee || isLoading}
isLoading={isLoading || Boolean(isAmountValid && isAddressValid && !currentFee)} isLoading={isLoading || Boolean(isAmountValid && isAddressValid && !fee)}
/> />
</Styled.Footer> </Styled.Footer>
</Styled.Form> </Styled.Form>
@@ -399,12 +391,17 @@ export const TransferForm = ({
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form` Styled.Form = styled.form`
${formMixins.transfersForm} ${formMixins.inputsColumn}
gap: 1.25rem;
`;
Styled.FormInput = styled(FormInput)`
--form-input-height: 3.5rem;
`; `;
Styled.Footer = styled.footer` Styled.Footer = styled.footer`
${formMixins.footer} ${layoutMixins.stickyFooter}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom); margin-top: auto;
`; `;
Styled.Row = styled.div` Styled.Row = styled.div`
@@ -414,6 +411,7 @@ Styled.Row = styled.div`
Styled.SelectMenu = styled(SelectMenu)` Styled.SelectMenu = styled(SelectMenu)`
${formMixins.inputSelectMenu} ${formMixins.inputSelectMenu}
--form-input-height: 3.5rem;
`; `;
Styled.SelectItem = styled(SelectItem)` Styled.SelectItem = styled(SelectItem)`
@@ -424,10 +422,6 @@ Styled.NetworkSelectMenu = styled(Styled.SelectMenu)`
pointer-events: none; pointer-events: none;
`; `;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.InlineRow = styled.span` Styled.InlineRow = styled.span`
${layoutMixins.inlineRow} ${layoutMixins.inlineRow}
height: 100%; height: 100%;
@@ -442,6 +436,10 @@ Styled.DestinationInputLabel = styled.span`
svg { svg {
color: var(--color-positive); color: var(--color-positive);
path {
stroke-width: 2;
}
} }
`; `;
@@ -1,5 +1,4 @@
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
@@ -13,9 +12,9 @@ import { DiffOutput } from '@/components/DiffOutput';
import { Output, OutputType } from '@/components/Output'; import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag'; import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors'; import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors'; import { getTransferInputs } from '@/state/inputsSelectors';
@@ -24,14 +23,14 @@ import { MustBigNumber } from '@/lib/numbers';
type ElementProps = { type ElementProps = {
selectedAsset: DydxChainAsset; selectedAsset: DydxChainAsset;
fee?: number; fees?: number;
isDisabled?: boolean; isDisabled?: boolean;
isLoading?: boolean; isLoading?: boolean;
}; };
export const TransferButtonAndReceipt = ({ export const TransferButtonAndReceipt = ({
selectedAsset, selectedAsset,
fee, fees,
isDisabled, isDisabled,
isLoading, isLoading,
}: ElementProps) => { }: ElementProps) => {
@@ -48,46 +47,43 @@ export const TransferButtonAndReceipt = ({
const { current: equity, postOrder: newEquity } = equityInfo || {}; const { current: equity, postOrder: newEquity } = equityInfo || {};
const { current: leverage, postOrder: newLeverage } = leverageInfo || {}; const { current: leverage, postOrder: newLeverage } = leverageInfo || {};
const isUSDCSelected = selectedAsset === DydxChainAsset.USDC; // TODO(@aforaleka): add wallet balance for DYDX
const balance = selectedAsset === DydxChainAsset.USDC ? equity : nativeTokenBalance;
const balance = isUSDCSelected ? equity : nativeTokenBalance; const newBalance =
const newNativeTokenBalance = nativeTokenBalance selectedAsset === DydxChainAsset.USDC
.minus(size?.size ?? 0) ? newEquity
.minus(fee ?? 0) // show balance after fees for button receipt : MustBigNumber(nativeTokenBalance)
.toNumber(); .minus(size?.size ?? 0)
.toNumber();
const newBalance = isUSDCSelected ? newEquity : newNativeTokenBalance;
const transferDetailItems = [ const transferDetailItems = [
{ {
key: 'fees', key: 'fees',
label: ( label: (
<span> <span>
{stringGetter({ key: STRING_KEYS.FEES })} <Tag>{tokensConfigs[selectedAsset]?.name}</Tag> {stringGetter({ key: STRING_KEYS.FEES })} <Tag>{tokensConfigs[selectedAsset].name}</Tag>
</span> </span>
), ),
value: <Output type={OutputType.Asset} value={fee} />, value: <Output type={OutputType.Asset} value={fees} />,
}, },
{ {
key: 'balance', key: 'balance',
label: ( label: (
<span> <span>
{stringGetter({ key: STRING_KEYS.BALANCE })}{' '} {stringGetter({ key: STRING_KEYS.BALANCE })} <Tag>{tokensConfigs[selectedAsset].name}</Tag>
<Tag>{tokensConfigs[selectedAsset]?.name}</Tag>
</span> </span>
), ),
value: ( value: (
<DiffOutput <DiffOutput
type={OutputType.Asset} type={OutputType.Asset}
value={balance} value={balance}
sign={NumberSign.Negative}
newValue={newBalance} newValue={newBalance}
hasInvalidNewValue={MustBigNumber(newBalance).isNegative()} sign={NumberSign.Negative}
withDiff={newBalance !== null && !MustBigNumber(balance).eq(newBalance ?? 0)} withDiff={Boolean(newBalance) && balance !== newBalance}
/> />
), ),
}, },
isUSDCSelected && { selectedAsset === DydxChainAsset.USDC && {
key: 'leverage', key: 'leverage',
label: <span>{stringGetter({ key: STRING_KEYS.LEVERAGE })}</span>, label: <span>{stringGetter({ key: STRING_KEYS.LEVERAGE })}</span>,
value: ( value: (
@@ -103,7 +99,7 @@ export const TransferButtonAndReceipt = ({
].filter(isTruthy); ].filter(isTruthy);
return ( return (
<Styled.WithDetailsReceipt detailItems={transferDetailItems}> <WithDetailsReceipt detailItems={transferDetailItems}>
{!canAccountTrade ? ( {!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} /> <OnboardingTriggerButton size={ButtonSize.Base} />
) : ( ) : (
@@ -115,16 +111,6 @@ export const TransferButtonAndReceipt = ({
{stringGetter({ key: STRING_KEYS.CONFIRM_TRANSFER })} {stringGetter({ key: STRING_KEYS.CONFIRM_TRANSFER })}
</Button> </Button>
)} )}
</Styled.WithDetailsReceipt> </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);
}
`;
+31 -23
View File
@@ -7,7 +7,6 @@ import { OnboardingState } from '@/constants/account';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, TOOLTIP_STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS, TOOLTIP_STRING_KEYS } from '@/constants/localization';
import { isMainnet } from '@/constants/networks';
import { DydxChainAsset, wallets } from '@/constants/wallets'; import { DydxChainAsset, wallets } from '@/constants/wallets';
import { import {
@@ -22,7 +21,6 @@ import {
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { AssetIcon } from '@/components/AssetIcon'; import { AssetIcon } from '@/components/AssetIcon';
import { CopyButton } from '@/components/CopyButton';
import { DropdownMenu } from '@/components/DropdownMenu'; import { DropdownMenu } from '@/components/DropdownMenu';
import { Output, OutputType } from '@/components/Output'; import { Output, OutputType } from '@/components/Output';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
@@ -40,7 +38,6 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { headerMixins } from '@/styles/headerMixins'; import { headerMixins } from '@/styles/headerMixins';
import { MustBigNumber } from '@/lib/numbers'; import { MustBigNumber } from '@/lib/numbers';
export const AccountMenu = () => { export const AccountMenu = () => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { mintscanBase } = useURLConfigs(); const { mintscanBase } = useURLConfigs();
@@ -90,8 +87,13 @@ export const AccountMenu = () => {
</WithTooltip> </WithTooltip>
<Styled.Address>{truncateAddress(dydxAddress)}</Styled.Address> <Styled.Address>{truncateAddress(dydxAddress)}</Styled.Address>
</Styled.Column> </Styled.Column>
<Styled.CopyButton buttonType="icon" value={dydxAddress} shape={ButtonShape.Square} /> <IconButton
<Styled.IconButton action={ButtonAction.Base}
iconName={IconName.Copy}
onClick={() => dydxAddress && navigator.clipboard.writeText(dydxAddress)}
shape={ButtonShape.Square}
/>
<IconButton
action={ButtonAction.Base} action={ButtonAction.Base}
href={`${mintscanBase}/account/${dydxAddress}`} href={`${mintscanBase}/account/${dydxAddress}`}
iconName={IconName.LinkOut} iconName={IconName.LinkOut}
@@ -119,7 +121,7 @@ export const AccountMenu = () => {
key: STRING_KEYS.ASSET_BALANCE, key: STRING_KEYS.ASSET_BALANCE,
params: { ASSET: chainTokenLabel }, params: { ASSET: chainTokenLabel },
})} })}
<AssetIcon symbol={chainTokenLabel} /> {/* <AssetIcon symbol="DYDX" /> */}
</Styled.label> </Styled.label>
<Styled.BalanceOutput type={OutputType.Asset} value={nativeTokenBalance} /> <Styled.BalanceOutput type={OutputType.Asset} value={nativeTokenBalance} />
</div> </div>
@@ -167,9 +169,22 @@ export const AccountMenu = () => {
onSelect: onRecoverKeys, onSelect: onRecoverKeys,
separator: true, 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: '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 ...(onboardingState === OnboardingState.AccountConnected && hdKey
? [ ? [
!isMainnet && { {
value: 'MobileQrSignIn', value: 'MobileQrSignIn',
icon: <Icon iconName={IconName.Qr} />, icon: <Icon iconName={IconName.Qr} />,
label: stringGetter({ key: STRING_KEYS.TITLE_SIGN_INTO_MOBILE }), label: stringGetter({ key: STRING_KEYS.TITLE_SIGN_INTO_MOBILE }),
@@ -177,19 +192,17 @@ export const AccountMenu = () => {
}, },
{ {
value: 'MnemonicExport', value: 'MnemonicExport',
icon: <Icon iconName={IconName.ExportKeys} />, icon: (
<span>
<Icon iconName={IconName.ExportKeys} />
</span>
),
label: <span>{stringGetter({ key: STRING_KEYS.EXPORT_SECRET_PHRASE })}</span>, label: <span>{stringGetter({ key: STRING_KEYS.EXPORT_SECRET_PHRASE })}</span>,
highlightColor: 'negative', highlightColor: 'negative',
onSelect: () => dispatch(openDialog({ type: DialogTypes.MnemonicExport })), onSelect: () => dispatch(openDialog({ type: DialogTypes.MnemonicExport })),
}, },
].filter(isTruthy) ]
: []), : []),
{
value: 'Preferences',
icon: <Icon iconName={IconName.Gear} />,
label: stringGetter({ key: STRING_KEYS.PREFERENCES }),
onSelect: () => dispatch(openDialog({ type: DialogTypes.Preferences })),
},
{ {
value: 'Disconnect', value: 'Disconnect',
icon: <Icon iconName={IconName.BoxClose} />, icon: <Icon iconName={IconName.BoxClose} />,
@@ -320,12 +333,13 @@ Styled.label = styled.div`
Styled.Balances = styled.div` Styled.Balances = styled.div`
${layoutMixins.flexColumn} ${layoutMixins.flexColumn}
gap: 2px;
> div { > div {
${layoutMixins.spacedRow} ${layoutMixins.spacedRow}
box-shadow: 0 0 0 1px var(--color-border);
gap: 0.5rem; gap: 0.5rem;
padding: 0.625rem 1rem; padding: 0.5rem 1rem;
background-color: var(--color-layer-4); background-color: var(--color-layer-4);
@@ -372,10 +386,4 @@ Styled.ConnectToChain = styled(Styled.Column)`
Styled.IconButton = styled(IconButton)` Styled.IconButton = styled(IconButton)`
--button-padding: 0 0.25rem; --button-padding: 0 0.25rem;
--button-border: solid var(--border-width) var(--color-layer-6);
`;
Styled.CopyButton = styled(CopyButton)`
--button-padding: 0 0.25rem;
--button-border: solid var(--border-width) var(--color-layer-6);
`; `;

Some files were not shown because too many files have changed in this diff Show More