diff --git a/.eslintrc.json b/.eslintrc.json index e8b938d9e..5f3531fda 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,26 +20,20 @@ "sourceType": "module", "project": "./tsconfig.json" }, - "plugins": [ - "react", - "prettier", - "eslint-plugin-no-inline-styles", - "@tanstack/query" - ], + "plugins": ["react", "prettier", "eslint-plugin-no-inline-styles", "@tanstack/query"], "rules": { "consistent-return": "off", "import/prefer-default-export": 1, "no-restricted-imports": [ "warn", { - "patterns": [ - "@secretkeylabs/xverse-core/**/*" - ] + "patterns": ["@secretkeylabs/xverse-core/**/*"] } ], "no-promise-executor-return": "warn", "max-len": "off", "no-inline-styles/no-inline-styles": 2, + "no-nested-ternary": "off", "no-param-reassign": "off", "react-hooks/exhaustive-deps": "warn", "react/jsx-key": "warn", @@ -61,16 +55,8 @@ "settings": { "import/resolver": { "node": { - "extensions": [ - ".js", - ".jsx", - ".ts", - ".tsx" - ], - "moduleDirectory": [ - "node_modules", - "src/" - ] + "extensions": [".js", ".jsx", ".ts", ".tsx"], + "moduleDirectory": ["node_modules", "src/"] } } } diff --git a/package-lock.json b/package-lock.json index 4d875bfaf..25ebe9a3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "2.3.2-a791a1d", + "@secretkeylabs/xverse-core": "3.0.0", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "6.1.1", @@ -47,6 +47,7 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.0", "react-i18next": "^11.18.6", + "react-is-visible": "^1.2.0", "react-modal": "^3.15.1", "react-number-format": "^5.0.0", "react-qr-code": "^2.0.8", @@ -135,73 +136,6 @@ "node": "^18.18.2" } }, - "../xverse-core-private": { - "name": "@secretkeylabs/xverse-core", - "version": "2.2.0", - "extraneous": true, - "license": "ISC", - "dependencies": { - "@bitcoinerlab/secp256k1": "^1.0.2", - "@noble/curves": "^1.2.0", - "@noble/secp256k1": "^1.7.1", - "@scure/base": "^1.1.1", - "@scure/btc-signer": "1.1.0", - "@stacks/auth": "^6.5.1", - "@stacks/encryption": "6.1.1", - "@stacks/network": "4.3.5", - "@stacks/storage": "^6.0.0", - "@stacks/transactions": "4.3.5", - "@stacks/wallet-sdk": "^5.0.2", - "@zondax/ledger-stacks": "^1.0.4", - "axios": "0.27.2", - "base64url": "^3.0.1", - "bip32": "^4.0.0", - "bip39": "3.0.3", - "bitcoin-address-validation": "^2.2.1", - "bitcoinjs-lib": "^6.1.3", - "bitcoinjs-message": "^2.2.0", - "bn.js": "^5.1.3", - "bs58check": "^3.0.1", - "buffer": "6.0.3", - "c32check": "^2.0.0", - "ecdsa-sig-formatter": "^1.0.11", - "ecpair": "^2.1.0", - "jsontokens": "^4.0.1", - "ledger-bitcoin": "^0.2.1", - "process": "^0.11.10", - "util": "^0.12.4", - "uuidv4": "^6.2.13", - "varuint-bitcoin": "^1.1.2" - }, - "devDependencies": { - "@types/react": "^18.2.18", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", - "@vitest/coverage-c8": "^0.31.1", - "airbnb": "^0.0.2", - "bip322-js": "^1.1.0", - "eslint": "^8.38.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-prettier": "^4.2.1", - "husky": "^8.0.3", - "lint-staged": "^13.2.3", - "mockdate": "^3.0.5", - "prettier": "^2.8.7", - "rimraf": "^3.0.2", - "ts-loader": "^9.4.1", - "typescript": "^4.8.3", - "vitest": "^0.31.1", - "webpack": "^5.74.0", - "webpack-cli": "^4.10.0" - }, - "peerDependencies": { - "bignumber.js": "^9.0.0", - "react": ">18.0.0" - } - }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -3474,9 +3408,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "2.3.2-a791a1d", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/2.3.2-a791a1d/7ad92ad24b2fa0c3c1e3d9f02af9115851746348", - "integrity": "sha512-PB3Yg4GHFwH05SwwaRIzzlaYy3sslMcQlUPD72reOBWB0aWiPfE90lQSKhgvwg5cPgO9Z0n+lC/4kh0h/fSXDg==", + "version": "3.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/3.0.0/90bd130bc7b3f9c0739e65d34772d6a550b5818d", + "integrity": "sha512-dtPkHY+VSNf/iwta3cSO2ZsKxdURTM4E+FPCwrotXc85MfxO0MIjyRa4VlOCtHsj8fNS2yH+KHg3uClLK9+WiQ==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -3765,9 +3699,9 @@ } }, "node_modules/@stacks/common": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.5.5.tgz", - "integrity": "sha512-6v2AVHTTryvl1Govu5rmBXLywAyen2fU3doMCx/7Lk/tFLc4OjMEx4uf1wzpPx1zw/fwJnvoz74OrT/RSALDYw==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.8.1.tgz", + "integrity": "sha512-ewL9GLZNQYa5a/3K4xSHlHIgHkD4rwWW/QEaPId8zQIaL+1O9qCaF4LX9orNQeOmEk8kvG0x2xGV54fXKCZeWQ==", "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -3983,12 +3917,12 @@ } }, "node_modules/@stacks/network": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.5.5.tgz", - "integrity": "sha512-lw7+g6UhOpvWasMeRYMb2OGRKm9ptYkGt27Usg3Eo0z/pu20jZxvHXLBMdDQqxNQOOmwiG4FadICnwTlmnHaqw==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.8.1.tgz", + "integrity": "sha512-n8M25pPbLqpSBctabtsLOTBlmPvm9EPQpTI//x7HLdt5lEjDXxauEQt0XGSvDUZwecrmztqt9xNxlciiGApRBw==", "peer": true, "dependencies": { - "@stacks/common": "^6.5.5", + "@stacks/common": "^6.8.1", "cross-fetch": "^3.1.5" } }, @@ -4540,9 +4474,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -15813,6 +15747,19 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "peer": true }, + "node_modules/react-is-visible": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-is-visible/-/react-is-visible-1.2.0.tgz", + "integrity": "sha512-052RH7aHdKrABOBH12wvS2zTAzJ8MnaswPvX2AQGq5wxvD8BnQ5vcnf0cA0cj2g4bjH0+qXz1AIkEL9ZlnN23A==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "hoist-non-react-statics": "3.3.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -21554,9 +21501,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "2.3.2-a791a1d", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/2.3.2-a791a1d/7ad92ad24b2fa0c3c1e3d9f02af9115851746348", - "integrity": "sha512-PB3Yg4GHFwH05SwwaRIzzlaYy3sslMcQlUPD72reOBWB0aWiPfE90lQSKhgvwg5cPgO9Z0n+lC/4kh0h/fSXDg==", + "version": "3.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/3.0.0/90bd130bc7b3f9c0739e65d34772d6a550b5818d", + "integrity": "sha512-dtPkHY+VSNf/iwta3cSO2ZsKxdURTM4E+FPCwrotXc85MfxO0MIjyRa4VlOCtHsj8fNS2yH+KHg3uClLK9+WiQ==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/secp256k1": "^1.7.1", @@ -21811,9 +21758,9 @@ } }, "@stacks/common": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.5.5.tgz", - "integrity": "sha512-6v2AVHTTryvl1Govu5rmBXLywAyen2fU3doMCx/7Lk/tFLc4OjMEx4uf1wzpPx1zw/fwJnvoz74OrT/RSALDYw==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.8.1.tgz", + "integrity": "sha512-ewL9GLZNQYa5a/3K4xSHlHIgHkD4rwWW/QEaPId8zQIaL+1O9qCaF4LX9orNQeOmEk8kvG0x2xGV54fXKCZeWQ==", "requires": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -22022,12 +21969,12 @@ } }, "@stacks/network": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.5.5.tgz", - "integrity": "sha512-lw7+g6UhOpvWasMeRYMb2OGRKm9ptYkGt27Usg3Eo0z/pu20jZxvHXLBMdDQqxNQOOmwiG4FadICnwTlmnHaqw==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.8.1.tgz", + "integrity": "sha512-n8M25pPbLqpSBctabtsLOTBlmPvm9EPQpTI//x7HLdt5lEjDXxauEQt0XGSvDUZwecrmztqt9xNxlciiGApRBw==", "peer": true, "requires": { - "@stacks/common": "^6.5.5", + "@stacks/common": "^6.8.1", "cross-fetch": "^3.1.5" } }, @@ -22489,9 +22436,9 @@ } }, "@testing-library/dom": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", "peer": true, "requires": { "@babel/code-frame": "^7.10.4", @@ -31065,6 +31012,15 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "peer": true }, + "react-is-visible": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-is-visible/-/react-is-visible-1.2.0.tgz", + "integrity": "sha512-052RH7aHdKrABOBH12wvS2zTAzJ8MnaswPvX2AQGq5wxvD8BnQ5vcnf0cA0cj2g4bjH0+qXz1AIkEL9ZlnN23A==", + "requires": { + "@babel/runtime": "^7.12.5", + "hoist-non-react-statics": "3.3.2" + } + }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", diff --git a/package.json b/package.json index 8d004d536..d6c93f731 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "2.3.2-a791a1d", + "@secretkeylabs/xverse-core": "3.0.0", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "6.1.1", @@ -46,6 +46,7 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.0", "react-i18next": "^11.18.6", + "react-is-visible": "^1.2.0", "react-modal": "^3.15.1", "react-number-format": "^5.0.0", "react-qr-code": "^2.0.8", diff --git a/src/app/App.tsx b/src/app/App.tsx index 215a33c1b..73c0f2138 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -2,16 +2,16 @@ import LoadingScreen from '@components/loadingScreen'; import rootStore from '@stores/index'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { MIX_PANEL_TOKEN } from '@utils/constants'; import { queryClient } from '@utils/query'; +import mixpanel from 'mixpanel-browser'; +import { useEffect } from 'react'; import { Toaster } from 'react-hot-toast'; import { Provider } from 'react-redux'; import { RouterProvider } from 'react-router-dom'; import { PersistGate } from 'redux-persist/integration/react'; import { ThemeProvider } from 'styled-components'; import '../locales'; -import { useEffect } from 'react'; -import mixpanel from 'mixpanel-browser'; -import { MIX_PANEL_TOKEN } from '@utils/constants'; import Theme from '../theme'; import GlobalStyle from '../theme/global'; import SessionGuard from './components/guards/session'; @@ -40,7 +40,7 @@ function App(): JSX.Element { - + diff --git a/src/app/components/barLoader/index.tsx b/src/app/components/barLoader/index.tsx index 5c53d37da..428a9cc76 100644 --- a/src/app/components/barLoader/index.tsx +++ b/src/app/components/barLoader/index.tsx @@ -77,8 +77,8 @@ export function BetterBarLoader({ height, className, }: { - width: number; - height: number; + width: number | string; + height: number | string; className?: string; }) { return ( @@ -88,7 +88,6 @@ export function BetterBarLoader({ interval={0.1} width={width} height={height} - viewBox={`0 0 ${width} ${height}`} backgroundColor={Theme.colors.elevation3} foregroundColor={Theme.colors.grey} className={className} diff --git a/src/app/components/bundleAsset/bundleAsset.tsx b/src/app/components/bundleAsset/bundleAsset.tsx index 4ab31b673..284534cba 100644 --- a/src/app/components/bundleAsset/bundleAsset.tsx +++ b/src/app/components/bundleAsset/bundleAsset.tsx @@ -2,7 +2,7 @@ import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; import { Bundle } from '@utils/rareSats'; import styled from 'styled-components'; -import RareSatsCollage from './rareSatsCollage'; +import CollectibleCollage from '../collectibleCollage/collectibleCollage'; const ImageContainer = styled.div` display: flex; @@ -29,7 +29,7 @@ function BundleAsset({ bundle }: Props) { return ( {isMoreThanOneItem ? ( - + ) : ( diff --git a/src/app/components/bundleAsset/rareSatsCollage.tsx b/src/app/components/collectibleCollage/collectibleCollage.tsx similarity index 66% rename from src/app/components/bundleAsset/rareSatsCollage.tsx rename to src/app/components/collectibleCollage/collectibleCollage.tsx index b5d0a7466..e15ee56c5 100644 --- a/src/app/components/bundleAsset/rareSatsCollage.tsx +++ b/src/app/components/collectibleCollage/collectibleCollage.tsx @@ -1,4 +1,6 @@ import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; +import Nft from '@screens/nftDashboard/nft'; +import { NonFungibleToken } from '@secretkeylabs/xverse-core'; import { BundleItem } from '@utils/rareSats'; import styled from 'styled-components'; @@ -31,15 +33,16 @@ const RemainingAmountOfAssets = styled.div((props) => ({ borderRadius: props.theme.radius(1), background: props.theme.colors.elevation1, p: { - ...props.theme.body_medium_m, - fontSize: 'calc(2vw + 2vh)', + ...props.theme.typography.body_medium_m, + fontSize: 'calc((2vw + 2vh)* 0.8)', color: props.theme.colors.white_0, }, })); -function RareSatsCollage({ items }: { items: Array }) { +function CollectibleCollage({ items }: { items: Array }) { const moreThanFourItems = items.length > 4; + const isBundleItem = (item: any): boolean => (item as BundleItem).rarity_ranking !== undefined; return ( {items.slice(0, 4).map((item, index) => ( @@ -49,8 +52,11 @@ function RareSatsCollage({ items }: { items: Array }) {

+{items.length - 4}

+ ) : // Conditionally render RareSatAsset if item is a BundleItem otherwise render Nft + isBundleItem(item) ? ( + ) : ( - + )} ))} @@ -58,4 +64,4 @@ function RareSatsCollage({ items }: { items: Array }) { ); } -export default RareSatsCollage; +export default CollectibleCollage; diff --git a/src/app/screens/ordinalsCollection/ordinalsCollectionGridItem.tsx b/src/app/components/collectibleCollectionGridItem/index.tsx similarity index 50% rename from src/app/screens/ordinalsCollection/ordinalsCollectionGridItem.tsx rename to src/app/components/collectibleCollectionGridItem/index.tsx index 933c6d92a..d11c4be81 100644 --- a/src/app/screens/ordinalsCollection/ordinalsCollectionGridItem.tsx +++ b/src/app/components/collectibleCollectionGridItem/index.tsx @@ -1,14 +1,7 @@ -import useOrdinalDataReducer from '@hooks/stores/useOrdinalReducer'; -import OrdinalImage from '@screens/ordinals/ordinalImage'; -import type { Inscription } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; -import { - getInscriptionsCollectionGridItemId, - getInscriptionsCollectionGridItemSubText, - getInscriptionsCollectionGridItemSubTextColor, -} from '@utils/inscriptions'; -import { useNavigate } from 'react-router-dom'; +import { ReactNode } from 'react'; import styled from 'styled-components'; +import { Color } from 'theme'; const InfoContainer = styled.div` display: flex; @@ -22,6 +15,7 @@ const StyledItemId = styled(StyledP)` text-wrap: nowrap; overflow: hidden; width: 100%; + text-overflow: ellipsis; `; const StyledItemSub = styled(StyledP)` @@ -49,35 +43,46 @@ const GridItemContainer = styled.button` flex-direction: column; background: transparent; gap: ${(props) => props.theme.space.s}; + cursor: ${(props) => (props.onClick ? 'pointer' : 'initial')}; + width: 100%; `; -export function OrdinalsCollectionGridItem({ item }: { item: Inscription }) { - const navigate = useNavigate(); - const { setSelectedOrdinalDetails } = useOrdinalDataReducer(); - - const handleOnClick = () => { - setSelectedOrdinalDetails(item); - navigate(`/nft-dashboard/ordinal-detail/${item.id}`); - }; - - const itemId = getInscriptionsCollectionGridItemId(item); - const itemSubText = getInscriptionsCollectionGridItemSubText(item); - const itemSubTextColor = getInscriptionsCollectionGridItemSubTextColor(item); +interface Props { + item: any; + itemId: string; + itemSubText?: string; + itemSubTextColor?: Color; + children: ReactNode; + onClick?: (collectible: any) => void; +} +export function CollectibleCollectionGridItem({ + item, + itemId, + itemSubText, + itemSubTextColor, + children, + onClick, +}: Props) { + const handleOnClick = onClick + ? () => { + onClick(item); + } + : undefined; return ( - - - + {children} {itemId} - - {itemSubText} - + {itemSubText && ( + + {itemSubText} + + )} ); } -export default OrdinalsCollectionGridItem; +export default CollectibleCollectionGridItem; diff --git a/src/app/components/collectibleDetailTile/index.tsx b/src/app/components/collectibleDetailTile/index.tsx index 7ef735d22..88058a8dd 100644 --- a/src/app/components/collectibleDetailTile/index.tsx +++ b/src/app/components/collectibleDetailTile/index.tsx @@ -7,17 +7,17 @@ interface DetailSectionProps { isGallery: boolean; } const DescriptionHeadingText = styled.p((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white['400'], + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, marginBottom: props.theme.spacing(2), whiteSpace: 'nowrap', textAlign: props.isGallery ? 'right' : 'left', })); const DescriptionValueText = styled.p((props) => ({ - ...props.theme.body_medium_m, + ...props.theme.typography.body_medium_m, wordWrap: 'break-word', - color: props.theme.colors.white['0'], + color: props.theme.colors.white_0, textAlign: !props.isGallery ? 'left' : 'right', maxWidth: 375, })); @@ -30,6 +30,7 @@ const Container = styled.div((props) => ({ display: 'flex', flexDirection: props.isColumnAlignment ? 'column' : 'row', width: '100%', + flex: 1, justifyContent: 'space-between', })); diff --git a/src/app/components/optionsDialog/optionsDialog.tsx b/src/app/components/optionsDialog/optionsDialog.tsx index 084d8cbf9..fd151bfdf 100644 --- a/src/app/components/optionsDialog/optionsDialog.tsx +++ b/src/app/components/optionsDialog/optionsDialog.tsx @@ -23,7 +23,7 @@ const OuterContainer = styled.div({ left: 0, right: 0, backgroundColor: 'transparent', - zIndex: 1, + zIndex: 2, }); interface Props { diff --git a/src/app/components/recipientAddressView/index.tsx b/src/app/components/recipientAddressView/index.tsx index cdc3d5d75..54e71e372 100644 --- a/src/app/components/recipientAddressView/index.tsx +++ b/src/app/components/recipientAddressView/index.tsx @@ -1,6 +1,5 @@ import ArrowSquareOut from '@assets/img/arrow_square_out.svg'; import { useBnsName } from '@hooks/queries/useBnsName'; -import useNetworkSelector from '@hooks/useNetwork'; import { getExplorerUrl } from '@utils/helper'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -56,9 +55,8 @@ interface Props { recipient: string; } function RecipientAddressView({ recipient }: Props) { - const selectedNetwork = useNetworkSelector(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const bnsName = useBnsName(recipient, selectedNetwork); + const bnsName = useBnsName(recipient); const handleOnPress = () => { window.open(getExplorerUrl(recipient)); }; diff --git a/src/app/components/sendForm/index.tsx b/src/app/components/sendForm/index.tsx index 74002267f..c10231387 100644 --- a/src/app/components/sendForm/index.tsx +++ b/src/app/components/sendForm/index.tsx @@ -1,12 +1,11 @@ import ActionButton from '@components/button'; import InfoContainer from '@components/infoContainer'; import TokenImage from '@components/tokenImage'; -import { useBnsName, useBNSResolver } from '@hooks/queries/useBnsName'; +import { useBnsName, useBnsResolver } from '@hooks/queries/useBnsName'; import useDebounce from '@hooks/useDebounce'; -import useNetworkSelector from '@hooks/useNetwork'; import useWalletSelector from '@hooks/useWalletSelector'; -import type { FungibleToken } from '@secretkeylabs/xverse-core'; import { + FungibleToken, getBtcEquivalent, getFiatEquivalent, getStxTokenEquivalent, @@ -67,45 +66,45 @@ const MemoContainer = styled.div((props) => ({ marginBottom: props.theme.spacing(6), })); -const ErrorText = styled.h1((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.feedback.error, +const ErrorText = styled.p((props) => ({ + ...props.theme.typography.body_s, + color: props.theme.colors.danger_medium, })); const InputFieldContainer = styled.div(() => ({ flex: 1, })); -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const TitleText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, flex: 1, display: 'flex', })); -const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const Text = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, })); -const SubText = styled.h1((props) => ({ - ...props.theme.body_xs, +const SubText = styled.p((props) => ({ + ...props.theme.typography.body_s, display: 'flex', flex: 1, color: props.theme.colors.white_400, })); -const AssociatedText = styled.h1((props) => ({ - ...props.theme.body_xs, +const AssociatedText = styled.p((props) => ({ + ...props.theme.typography.body_s, wordWrap: 'break-word', })); -const BalanceText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const BalanceText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_400, marginRight: props.theme.spacing(2), })); const InputField = styled.input((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, backgroundColor: props.theme.colors.elevation_n1, color: props.theme.colors.white_0, width: '100%', @@ -227,10 +226,9 @@ function SendForm({ const { stxBtcRate, btcFiatRate, fiatCurrency, stxAddress, selectedAccount } = useWalletSelector(); - const network = useNetworkSelector(); const debouncedSearchTerm = useDebounce(recipientAddress, 300); - const associatedBnsName = useBnsName(recipientAddress, network); - const associatedAddress = useBNSResolver(debouncedSearchTerm, stxAddress, currencyType); + const associatedBnsName = useBnsName(recipientAddress); + const associatedAddress = useBnsResolver(debouncedSearchTerm, stxAddress, currencyType); const { isAccountSwitched } = useClearFormOnAccountSwitch(); useEffect(() => { diff --git a/src/app/components/tabBar/index.tsx b/src/app/components/tabBar/index.tsx index 3bd6786fc..92601e9fd 100644 --- a/src/app/components/tabBar/index.tsx +++ b/src/app/components/tabBar/index.tsx @@ -31,7 +31,7 @@ const Button = styled.button({ zIndex: 2, }); -type Tab = 'dashboard' | 'nft' | 'stacking' | 'settings'; +export type Tab = 'dashboard' | 'nft' | 'stacking' | 'settings'; interface Props { tab: Tab; diff --git a/src/app/components/tilesSkeletonLoader.tsx b/src/app/components/tilesSkeletonLoader.tsx index 1252ced0d..3f7325179 100644 --- a/src/app/components/tilesSkeletonLoader.tsx +++ b/src/app/components/tilesSkeletonLoader.tsx @@ -7,7 +7,7 @@ const TilesLoaderContainer = styled.div<{ width: '100%', display: 'flex', justifyContent: 'flex-start', - columnGap: props.isGalleryOpen ? props.theme.spacing(16) : props.theme.spacing(12), + columnGap: props.isGalleryOpen ? props.theme.spacing(16) : props.theme.spacing(8), })); const TileLoaderContainer = styled.div({ diff --git a/src/app/components/topRow/index.tsx b/src/app/components/topRow/index.tsx index ed4a6b966..38e44039c 100644 --- a/src/app/components/topRow/index.tsx +++ b/src/app/components/topRow/index.tsx @@ -40,7 +40,7 @@ const AnimatedBackButton = styled(BackButton)` interface Props { title: string; - onClick: () => void; + onClick: (e: React.MouseEvent) => void; showBackButton?: boolean; className?: string; } diff --git a/src/app/hooks/queries/useBnsName.ts b/src/app/hooks/queries/useBnsName.ts index 028d1c06c..6f02fe79a 100644 --- a/src/app/hooks/queries/useBnsName.ts +++ b/src/app/hooks/queries/useBnsName.ts @@ -1,10 +1,10 @@ +import { fetchAddressOfBnsName, getBnsName, validateStxAddress } from '@secretkeylabs/xverse-core'; import { useEffect, useState } from 'react'; -import { StacksNetwork, validateStxAddress } from '@secretkeylabs/xverse-core'; -import { fetchAddressOfBnsName, getBnsName } from '@secretkeylabs/xverse-core/api'; -import useWalletSelector from '../useWalletSelector'; import useNetworkSelector from '../useNetwork'; +import useWalletSelector from '../useWalletSelector'; -export const useBnsName = (walletAddress: string, network: StacksNetwork) => { +export const useBnsName = (walletAddress: string) => { + const network = useNetworkSelector(); const [bnsName, setBnsName] = useState(''); useEffect(() => { @@ -12,15 +12,15 @@ export const useBnsName = (walletAddress: string, network: StacksNetwork) => { const name = await getBnsName(walletAddress, network); setBnsName(name ?? ''); })(); - }, [walletAddress]); + }, [walletAddress, network]); return bnsName; }; -export const useBNSResolver = ( +export const useBnsResolver = ( recipientAddress: string, walletAddress: string, - currencyType: string, + currencyType?: string, ) => { const { network } = useWalletSelector(); const selectedNetwork = useNetworkSelector(); @@ -43,7 +43,7 @@ export const useBNSResolver = ( setAssociatedAddress(''); } })(); - }, [recipientAddress]); + }, [recipientAddress, network, currencyType, selectedNetwork, walletAddress]); return associatedAddress; }; diff --git a/src/app/hooks/queries/useNftDetail.ts b/src/app/hooks/queries/useNftDetail.ts new file mode 100644 index 000000000..85a37841a --- /dev/null +++ b/src/app/hooks/queries/useNftDetail.ts @@ -0,0 +1,28 @@ +import { getNftDetail, NftDetailResponse, NonFungibleToken } from '@secretkeylabs/xverse-core'; +import { useQuery } from '@tanstack/react-query'; +import { getIdentifier, isBnsCollection } from '@utils/nfts'; +import { handleRetries } from '@utils/query'; + +const useNftDetail = (id: string | NonFungibleToken['identifier']) => { + const { tokenId, contractAddress, contractName } = + typeof id === 'string' ? getIdentifier(id) : id; + + const fetchNft = (): Promise => + getNftDetail(tokenId, contractAddress, contractName); + + return useQuery({ + enabled: !!( + id && + tokenId && + contractAddress && + contractName && + !isBnsCollection(`${contractAddress}.${contractName}`) + ), + retry: handleRetries, + queryKey: ['nft-detail', contractAddress, contractName, tokenId], + queryFn: fetchNft, + staleTime: 24 * 60 * 60 * 1000, // 24 hours + }); +}; + +export default useNftDetail; diff --git a/src/app/hooks/queries/useStacksCollectibles.ts b/src/app/hooks/queries/useStacksCollectibles.ts index d93cdffbc..3f2e4d754 100644 --- a/src/app/hooks/queries/useStacksCollectibles.ts +++ b/src/app/hooks/queries/useStacksCollectibles.ts @@ -1,42 +1,29 @@ import useNetworkSelector from '@hooks/useNetwork'; import useWalletSelector from '@hooks/useWalletSelector'; -import { getNfts, NftsListData } from '@secretkeylabs/xverse-core'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import { handleRetries, InvalidParamsError } from '@utils/query'; +import { getNftCollections, StacksCollectionList } from '@secretkeylabs/xverse-core'; +import { useQuery } from '@tanstack/react-query'; +import { handleRetries } from '@utils/query'; const useStacksCollectibles = () => { - const { stxAddress } = useWalletSelector(); + let { stxAddress } = useWalletSelector(); const selectedNetwork = useNetworkSelector(); - function fetchNfts({ pageParam = 0 }): Promise { - if (!stxAddress) { - return Promise.reject(new InvalidParamsError('stxAddress is required')); - } - return getNfts(stxAddress, selectedNetwork, pageParam); + // TODO remove this after testing + const testAddress = localStorage.getItem('stxAddress'); + if (testAddress) { + stxAddress = testAddress; } - const { isLoading, data, fetchNextPage, isFetchingNextPage, hasNextPage, refetch, error } = - useInfiniteQuery([`nft-meta-data-${stxAddress}`], fetchNfts, { - retry: handleRetries, - keepPreviousData: false, - getNextPageParam: (lastpage, pages) => { - const currentLength = pages.map((page) => page.nftsList).flat().length; - if (currentLength < lastpage.total) { - return currentLength; - } - return false; - }, - }); + const fetchNftCollections = (): Promise => + getNftCollections(stxAddress, selectedNetwork); - return { - isLoading, - error, - data, - fetchNextPage, - isFetchingNextPage, - hasNextPage, - refetch, - }; + return useQuery({ + enabled: !!stxAddress, + retry: handleRetries, + queryKey: ['nft-collection-data', stxAddress], + queryFn: fetchNftCollections, + staleTime: 5 * 60 * 1000, // 5mins + }); }; export default useStacksCollectibles; diff --git a/src/app/hooks/stores/useNftReducer.ts b/src/app/hooks/stores/useNftReducer.ts deleted file mode 100644 index a6caf3510..000000000 --- a/src/app/hooks/stores/useNftReducer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NftData } from '@secretkeylabs/xverse-core/types/api/gamma/currency'; -import { setNftDataAction } from '@stores/nftData/actions/actionCreator'; -import { useDispatch } from 'react-redux'; -import useNftDataSelector from './useNftDataSelector'; - -const useNftDataReducer = () => { - const { nftData } = useNftDataSelector(); - const dispatch = useDispatch(); - - const storeNftData = (data: NftData) => { - const nftExists = nftData.find((nftItem) => nftItem?.token_id === data?.token_id); - if (data && !nftExists) { - const modifiedNftList = [...nftData]; - modifiedNftList.push(data); - dispatch(setNftDataAction(modifiedNftList)); - } - }; - return { - storeNftData, - }; -}; - -export default useNftDataReducer; diff --git a/src/app/hooks/stores/useOrdinalReducer.ts b/src/app/hooks/stores/useOrdinalReducer.ts index 966d2c64d..86f7f2645 100644 --- a/src/app/hooks/stores/useOrdinalReducer.ts +++ b/src/app/hooks/stores/useOrdinalReducer.ts @@ -1,4 +1,4 @@ -import { Inscription } from '@secretkeylabs/xverse-core/types/api/ordinals'; +import type { Inscription } from '@secretkeylabs/xverse-core'; import { setSelectedOrdinalAction } from '@stores/nftData/actions/actionCreator'; import { useDispatch } from 'react-redux'; diff --git a/src/app/hooks/useResetUserFlow.ts b/src/app/hooks/useResetUserFlow.ts index 932f416f3..7443eb201 100644 --- a/src/app/hooks/useResetUserFlow.ts +++ b/src/app/hooks/useResetUserFlow.ts @@ -14,18 +14,19 @@ const userFlowConfig: Record = { '/send-brc20': { resetTo: '/' }, '/confirm-brc20-tx': { resetTo: '/' }, '/confirm-inscription-request': { resetTo: '/' }, - '/ordinal-detail': { resetTo: '/nft-dashboard' }, - '/send-ordinal': { resetTo: '/nft-dashboard' }, - '/confirm-ordinal-tx': { resetTo: '/nft-dashboard' }, - '/nft-detail': { resetTo: '/nft-dashboard' }, - '/send-nft': { resetTo: '/nft-dashboard' }, - '/confirm-nft-tx': { resetTo: '/nft-dashboard' }, - '/verify-ledger': { resetTo: '/verify-ledger?mismatch=true' }, - '/add-stx-address-ledger': { resetTo: '/add-stx-address-ledger?mismatch=true' }, + '/ordinals-collection': { resetTo: '/nft-dashboard?tab=inscriptions' }, + '/ordinal-detail': { resetTo: '/nft-dashboard?tab=inscriptions' }, + '/send-ordinal': { resetTo: '/nft-dashboard?tab=inscriptions' }, + '/confirm-ordinal-tx': { resetTo: '/nft-dashboard?tab=inscriptions' }, + '/nft-collection': { resetTo: '/nft-dashboard?tab=nfts' }, + '/nft-detail': { resetTo: '/nft-dashboard?tab=nfts' }, + '/send-nft': { resetTo: '/nft-dashboard?tab=nfts' }, + '/confirm-nft-tx': { resetTo: '/nft-dashboard?tab=nfts' }, '/rare-sats-detail': { resetTo: '/nft-dashboard?tab=rareSats' }, '/rare-sats-bundle': { resetTo: '/nft-dashboard?tab=rareSats' }, '/send-rare-sat': { resetTo: '/nft-dashboard?tab=rareSats' }, - '/ordinals-collection': { resetTo: '/nft-dashboard?tab=inscriptions' }, + '/verify-ledger': { resetTo: '/verify-ledger?mismatch=true' }, + '/add-stx-address-ledger': { resetTo: '/add-stx-address-ledger?mismatch=true' }, }; type UserFlowConfigKey = keyof typeof userFlowConfig; diff --git a/src/app/layouts/sendLayout.tsx b/src/app/layouts/sendLayout.tsx new file mode 100644 index 000000000..8a077ce8b --- /dev/null +++ b/src/app/layouts/sendLayout.tsx @@ -0,0 +1,101 @@ +import AccountHeaderComponent from '@components/accountHeader'; +import BottomBar, { Tab } from '@components/tabBar'; +import TopRow from '@components/topRow'; +import { ArrowLeft } from '@phosphor-icons/react'; +import { StyledP } from '@ui-library/common.styled'; +import { PropsWithChildren } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { breakpoints, devices } from 'theme'; + +const ScrollContainer = styled.div((props) => ({ + display: 'flex', + flex: 1, + flexDirection: 'column', + ...props.theme.scrollbar, +})); + +const Container = styled.div` + display: flex; + flex-direction: column; + margin: auto; + margin-top: ${(props) => props.theme.space.xxs}; + padding: 0 ${(props) => props.theme.space.s}; + width: 100%; + height: 100%; + max-width: ${breakpoints.xs}px; + max-height: 600px; + + @media only screen and ${devices.min.s} { + flex: initial; + max-width: 588px; + border: 1px solid ${(props) => props.theme.colors.elevation3}; + border-radius: ${(props) => props.theme.space.s}; + padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; + padding-bottom: ${(props) => props.theme.space.xxl}; + margin-top: ${(props) => props.theme.space.xxxxl}; + } +`; + +const FooterContainer = styled.div` + display: flex; + justify-content: center; + margin-bottom: ${(props) => props.theme.space.xxl}; +`; + +const BottomBarContainer = styled.div({ + marginTop: 'auto', +}); + +const Button = styled.button` + display: flex; + background-color: transparent; + margin-bottom: ${(props) => props.theme.space.l}; +`; + +function SendLayout({ + children, + selectedBottomTab, + onClickBack, + hideBackButton = false, +}: PropsWithChildren<{ + selectedBottomTab: Tab; + onClickBack?: (e: React.MouseEvent) => void; + hideBackButton?: boolean; +}>) { + const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); + const isScreenLargerThanXs = document.documentElement.clientWidth > Number(breakpoints.xs); + const year = new Date().getFullYear(); + + return ( + <> + {isScreenLargerThanXs ? ( + + ) : ( + + )} + + + {isScreenLargerThanXs && !hideBackButton && onClickBack && ( + + )} + {children} + + {isScreenLargerThanXs && ( + + + {t('COPYRIGHT', { year })} + + + )} + + + {!isScreenLargerThanXs && } + + + ); +} + +export default SendLayout; diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index e88284df5..60dc5417b 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -33,6 +33,7 @@ import VerifyLedger from '@screens/ledger/verifyLedgerAccountAddress'; import LegalLinks from '@screens/legalLinks'; import Login from '@screens/login'; import ManageTokens from '@screens/manageTokens'; +import NftCollection from '@screens/nftCollection'; import NftDashboard from '@screens/nftDashboard'; import SupportedRarities from '@screens/nftDashboard/supportedRarities'; import NftDetailScreen from '@screens/nftDetail'; @@ -369,10 +370,6 @@ const router = createHashRouter([ ), }, - { - path: 'send-nft/:id', - element: , - }, { path: 'confirm-inscription-request', element: ( @@ -452,6 +449,14 @@ const router = createHashRouter([ path: 'nft-dashboard/nft-detail/:id/send-nft', element: , }, + { + path: 'nft-dashboard/nft-collection/:id', + element: ( + + + + ), + }, { path: 'nft-dashboard/ordinal-detail/:id/send-ordinal', element: ( diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 0360603be..5825b3721 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -10,7 +10,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { FungibleToken, microstacksToStx, satsToBtc } from '@secretkeylabs/xverse-core'; import { currencySymbolMap } from '@secretkeylabs/xverse-core/types/currency'; import { CurrencyTypes } from '@utils/constants'; -import { isLedgerAccount } from '@utils/helper'; +import { isInOptions, isLedgerAccount } from '@utils/helper'; import { getFtBalance, getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { useState } from 'react'; @@ -262,7 +262,7 @@ export default function CoinHeader(props: CoinBalanceProps) { }; const goToSendScreen = async () => { - if (isLedgerAccount(selectedAccount)) { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { switch (coin) { case 'BTC': await chrome.tabs.create({ diff --git a/src/app/screens/confirmNftTransaction/index.tsx b/src/app/screens/confirmNftTransaction/index.tsx index fa4ee2b76..874247a41 100644 --- a/src/app/screens/confirmNftTransaction/index.tsx +++ b/src/app/screens/confirmNftTransaction/index.tsx @@ -7,13 +7,12 @@ import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import TransactionDetailComponent from '@components/transactionDetailComponent'; import useStxWalletData from '@hooks/queries/useStxWalletData'; -import useNftDataSelector from '@hooks/stores/useNftDataSelector'; import useNetworkSelector from '@hooks/useNetwork'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; +import useNftDetail from '@hooks/queries/useNftDetail'; import useWalletSelector from '@hooks/useWalletSelector'; import NftImage from '@screens/nftDashboard/nftImage'; -import { broadcastSignedTransaction } from '@secretkeylabs/xverse-core/transactions'; -import { StacksTransaction } from '@secretkeylabs/xverse-core/types'; +import { broadcastSignedTransaction, StacksTransaction } from '@secretkeylabs/xverse-core'; import { deserializeTransaction } from '@stacks/transactions'; import { useMutation } from '@tanstack/react-query'; import { isLedgerAccount } from '@utils/helper'; @@ -43,7 +42,7 @@ const Container = styled.div({ alignItems: 'center', }); -const NFtContainer = styled.div((props) => ({ +const NftContainer = styled.div((props) => ({ maxWidth: 120, maxHeight: 120, width: '60%', @@ -57,7 +56,7 @@ const NFtContainer = styled.div((props) => ({ })); const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.headline_s, + ...props.theme.typography.headline_s, color: props.theme.colors.white_0, marginBottom: props.theme.spacing(16), textAlign: 'center', @@ -70,9 +69,10 @@ function ConfirmNftTransaction() { const navigate = useNavigate(); const location = useLocation(); const { id } = useParams(); - const { nftData } = useNftDataSelector(); - const nftIdDetails = id!.split('::'); - const nft = nftData.find((nftItem) => nftItem?.asset_id === nftIdDetails[1]); + + const nftDetailQuery = useNftDetail(id!); + const nft = nftDetailQuery.data?.data + const { unsignedTx: unsignedTxHex, recipientAddress } = location.state; const unsignedTx = deserializeTransaction(unsignedTxHex); const { network } = useWalletSelector(); @@ -168,9 +168,9 @@ function ConfirmNftTransaction() { skipModal={isLedgerAccount(selectedAccount)} > - + - + {t('REVIEW_TRANSACTION')} { diff --git a/src/app/screens/createInscription/CompleteScreen.tsx b/src/app/screens/createInscription/CompleteScreen.tsx index 3d41793c4..381963b20 100644 --- a/src/app/screens/createInscription/CompleteScreen.tsx +++ b/src/app/screens/createInscription/CompleteScreen.tsx @@ -6,16 +6,16 @@ import Success from '@assets/img/send/check_circle.svg'; import ActionButton from '@components/button'; import CopyButton from '@components/copyButton'; -import type { SettingsNetwork } from '@secretkeylabs/xverse-core/types'; +import type { SettingsNetwork } from '@secretkeylabs/xverse-core'; import { getBtcTxStatusUrl } from '@utils/helper'; -const TxStatusContainer = styled.div({ - background: 'rgba(25, 25, 48, 0.74)', +const TxStatusContainer = styled.div((props) => ({ + background: props.theme.colors.elevation0, display: 'flex', flexDirection: 'column', height: '100%', backdropFilter: 'blur(16px)', -}); +})); const Container = styled.div({ display: 'flex', @@ -77,15 +77,15 @@ const Image = styled.img({ }); const HeadingText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white['0'], + ...props.theme.typography.headline_s, + color: props.theme.colors.white_0, textAlign: 'center', marginTop: props.theme.spacing(8), })); -const BodyText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white['400'], +const BodyText = styled.p((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_400, marginTop: props.theme.spacing(8), textAlign: 'center', overflowWrap: 'break-word', @@ -95,29 +95,29 @@ const BodyText = styled.h1((props) => ({ marginRight: props.theme.spacing(5), })); -const TxIDText = styled.h1((props) => ({ +const TxIDText = styled.p((props) => ({ ...props.theme.headline_category_s, - color: props.theme.colors.white['400'], + color: props.theme.colors.white_400, marginTop: props.theme.spacing(8), textTransform: 'uppercase', })); -const BeforeButtonText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white['400'], +const BeforeButtonText = styled.p((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_400, })); -const IDText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white['0'], +const IDText = styled.p((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_0, marginTop: props.theme.spacing(2), wordBreak: 'break-all', })); -const ButtonText = styled.h1((props) => ({ - ...props.theme.body_m, +const ButtonText = styled.p((props) => ({ + ...props.theme.typography.body_m, marginRight: props.theme.spacing(2), - color: props.theme.colors.white['0'], + color: props.theme.colors.white_0, })); const ButtonImage = styled.img((props) => ({ diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index b32baf195..c7d8cf147 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -28,7 +28,7 @@ import CoinSelectModal from '@screens/home/coinSelectModal'; import type { FungibleToken } from '@secretkeylabs/xverse-core'; import { changeShowDataCollectionAlertAction } from '@stores/wallet/actions/actionCreators'; import { CurrencyTypes } from '@utils/constants'; -import { isLedgerAccount } from '@utils/helper'; +import { isInOptions, isLedgerAccount } from '@utils/helper'; import { optInMixPanel, optOutMixPanel } from '@utils/mixpanel'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -250,7 +250,7 @@ function Home() { }; const onStxSendClick = async () => { - if (isLedgerAccount(selectedAccount)) { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ url: chrome.runtime.getURL('options.html#/send-stx'), }); @@ -260,7 +260,7 @@ function Home() { }; const onBtcSendClick = async () => { - if (isLedgerAccount(selectedAccount)) { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ url: chrome.runtime.getURL('options.html#/send-btc'), }); @@ -280,10 +280,14 @@ function Home() { const onSendFtSelect = async (coin: FungibleToken) => { if (coin.protocol === 'brc-20') { if (isLedgerAccount(selectedAccount)) { - await chrome.tabs.create({ - // TODO replace with send-brc20-one-step route, when ledger support is ready - url: chrome.runtime.getURL(`options.html#/send-brc20?coinName=${coin.name}`), - }); + if (!isInOptions()) { + await chrome.tabs.create({ + // TODO replace with send-brc20-one-step route, when ledger support is ready + url: chrome.runtime.getURL(`options.html#/send-brc20?coinName=${coin.name}`), + }); + return; + } + navigate(`send-brc20?coinName=${coin.name}`); return; } navigate('send-brc20-one-step', { @@ -293,7 +297,7 @@ function Home() { }); return; } - if (isLedgerAccount(selectedAccount)) { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ url: chrome.runtime.getURL(`options.html#/send-ft?coinTicker=${coin.ticker}`), }); @@ -392,9 +396,13 @@ function Home() { icon={} text={t('ADD_STACKS_ADDRESS')} onPress={async () => { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/add-stx-address-ledger`), - }); + if (!isInOptions()) { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#/add-stx-address-ledger`), + }); + } else { + navigate('/add-stx-address-ledger'); + } }} /> )} diff --git a/src/app/screens/nftCollection/index.tsx b/src/app/screens/nftCollection/index.tsx new file mode 100644 index 000000000..ae55ecaec --- /dev/null +++ b/src/app/screens/nftCollection/index.tsx @@ -0,0 +1,270 @@ +import AccountHeaderComponent from '@components/accountHeader'; +import CollectibleCollectionGridItem from '@components/collectibleCollectionGridItem'; +import CollectibleDetailTile from '@components/collectibleDetailTile'; +import BottomTabBar from '@components/tabBar'; +import { StyledBarLoader, TilesSkeletonLoader } from '@components/tilesSkeletonLoader'; +import TopRow from '@components/topRow'; +import WebGalleryButton from '@components/webGalleryButton'; +import WrenchErrorMessage from '@components/wrenchErrorMessage'; +import useNftDetail from '@hooks/queries/useNftDetail'; +import { ArrowLeft } from '@phosphor-icons/react'; +import { GridContainer } from '@screens/nftDashboard/collectiblesTabs'; +import Nft from '@screens/nftDashboard/nft'; +import NftImage from '@screens/nftDashboard/nftImage'; +import { NonFungibleToken, StacksCollectionData } from '@secretkeylabs/xverse-core'; +import SnackBar from '@ui-library/snackBar'; +import { getFullyQualifiedKey, getNftCollectionsGridItemId, isBnsCollection } from '@utils/nfts'; +import { PropsWithChildren, useRef } from 'react'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import { useIsVisible } from 'react-is-visible'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import useNftCollection from './useNftCollection'; + +interface Props { + isGalleryOpen?: boolean; +} +const Container = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + flex: 1, +})); + +const NoCollectiblesText = styled.p((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_200, + marginTop: props.theme.spacing(16), + marginBottom: 'auto', + textAlign: 'center', +})); + +const HeadingText = styled.p((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_400, +})); + +const CollectionText = styled.p((props) => ({ + ...props.theme.typography.headline_s, + color: props.theme.colors.white_0, + marginTop: props.theme.spacing(1), + marginBottom: props.theme.spacing(4), +})); + +const BottomBarContainer = styled.div({ + marginTop: 'auto', +}); + +const PageHeader = styled.div` + padding: ${(props) => props.theme.space.xs}; + padding-top: 0; + max-width: 1224px; + margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +const PageHeaderContent = styled.div` + display: flex; + flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; + justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; + row-gap: ${(props) => props.theme.space.xl}; +`; + +const NftContainer = styled.div` + display: flex; + flex-direction: ${(props) => (props.isGalleryOpen ? 'column' : 'row')}; + justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; + column-gap: ${(props) => props.theme.space.m}; +`; + +const BackButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + width: 800, + marginTop: props.theme.spacing(40), +})); + +const BackButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + background: 'transparent', + marginBottom: props.theme.spacing(12), +})); + +const AssetDeatilButtonText = styled.div((props) => ({ + ...props.theme.typography.body_m, + fontWeight: 400, + fontSize: 14, + marginLeft: 2, + color: props.theme.colors.white_0, + textAlign: 'center', +})); + +const StyledGridContainer = styled(GridContainer)` + margin-top: ${(props) => props.theme.space.s}; + padding: 0 ${(props) => props.theme.space.xs}; + padding-bottom: ${(props) => props.theme.space.xl}; + max-width: 1224px; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +/* + * component to virtualise the grid item if not in window + * placeholder is required to match grid item size, in order to negate scroll jank + */ +function IsVisibleOrPlaceholder({ children }: PropsWithChildren) { + const nodeRef = useRef(null); + const isVisible = useIsVisible(nodeRef, { once: false }); + + return ( +
+ {isVisible ? ( + children + ) : ( + + + + )} +
+ ); +} + +/* + * component to load nft detail which contains image url + */ +function CollectionGridItemWithData({ + nft, + collectionData, + isGalleryOpen, +}: { + nft: NonFungibleToken; + collectionData: StacksCollectionData; + isGalleryOpen: boolean; +}) { + const { data: nftData } = useNftDetail(nft.identifier); + const navigate = useNavigate(); + const { t } = useTranslation('translation', { keyPrefix: 'COLLECTIBLE_COLLECTION_SCREEN' }); + + const handleClickItem = isBnsCollection(collectionData.collection_id) + ? undefined + : () => { + if (nftData?.data?.token_metadata) { + navigate(`/nft-dashboard/nft-detail/${getFullyQualifiedKey(nft.identifier)}`); + } else { + toast.custom(); + } + }; + + return ( + + + + ); +} + +function NftCollection() { + const { t } = useTranslation('translation', { keyPrefix: 'COLLECTIBLE_COLLECTION_SCREEN' }); + const { + collectionData, + portfolioValue, + isLoading, + isError, + isEmpty, + isGalleryOpen, + handleBackButtonClick, + openInGalleryView, + } = useNftCollection(); + + return ( + <> + {isGalleryOpen ? ( + + ) : ( + + )} + + + {isGalleryOpen && ( + + + <> + + {t('BACK_TO_GALLERY')} + + + + )} + +
+ {t('COLLECTION')} + + {collectionData?.collection_name || } + + {!isGalleryOpen && } +
+ + + + +
+
+ <> + {isEmpty && {t('NO_COLLECTIBLES')}} + {!!isError && } + + {isLoading ? ( + + ) : ( + collectionData?.all_nfts + .sort((a, b) => (a.value.repr > b.value.repr ? 1 : -1)) + .map((nft) => ( + + + + )) + )} + + +
+ {!isGalleryOpen && ( + + + + )} + + ); +} + +export default NftCollection; diff --git a/src/app/screens/nftCollection/useNftCollection.ts b/src/app/screens/nftCollection/useNftCollection.ts new file mode 100644 index 000000000..6124a3825 --- /dev/null +++ b/src/app/screens/nftCollection/useNftCollection.ts @@ -0,0 +1,44 @@ +import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; +import useResetUserFlow from '@hooks/useResetUserFlow'; +import { useMemo } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +export default function useNftCollection() { + const navigate = useNavigate(); + useResetUserFlow('/nft-collection'); + + const { id: collectionId } = useParams(); + const { data, isLoading, error } = useStacksCollectibles(); + + const collectionData = data?.results.find( + (collection) => collection.collection_id === collectionId, + ); + + const portfolioValue = + collectionData?.floor_price && !Number.isNaN(collectionData?.all_nfts?.length) + ? collectionData.floor_price * collectionData.all_nfts.length + : null; + + const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); + + const handleBackButtonClick = () => { + navigate('/nft-dashboard?tab=nfts'); + }; + + const openInGalleryView = async () => { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#/nft-dashboard/nft-collection/${collectionId}`), + }); + }; + + return { + collectionData, + portfolioValue, + isLoading, + isError: error, + isEmpty: !isLoading && !error && collectionData?.all_nfts.length === 0, + isGalleryOpen, + handleBackButtonClick, + openInGalleryView, + }; +} diff --git a/src/app/screens/nftDashboard/collectiblesTabs.tsx b/src/app/screens/nftDashboard/collectiblesTabs.tsx index a1ab7d94c..39b7111e7 100644 --- a/src/app/screens/nftDashboard/collectiblesTabs.tsx +++ b/src/app/screens/nftDashboard/collectiblesTabs.tsx @@ -7,10 +7,10 @@ import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { TabPanel, Tabs } from 'react-tabs'; import styled from 'styled-components'; -import type { NftDashboardState } from '.'; import { StyledBarLoader, TilesSkeletonLoader } from '../../components/tilesSkeletonLoader'; import Notice from './notice'; import RareSatsTabGridItem from './rareSatsTabGridItem'; +import type { NftDashboardState } from './useNftDashboard'; export const GridContainer = styled.div<{ isGalleryOpen: boolean; @@ -24,6 +24,14 @@ export const GridContainer = styled.div<{ : 'repeat(auto-fill,minmax(150px,1fr))', })); +const StickyStyledTabList = styled(StyledTabList)` + position: sticky; + background: ${(props) => props.theme.colors.elevation0}; + top: -1px; + z-index: 1; + padding: ${(props) => props.theme.space.m} 0; +`; + const StyledTotalItems = styled(StyledP)` margin-top: ${(props) => props.theme.space.s}; `; @@ -155,11 +163,11 @@ export default function CollectiblesTabs({ return ( {visibleTabButtons.length > 1 && ( - + {visibleTabButtons.map(({ key, label }) => ( {t(label)} ))} - + )} {hasActivatedOrdinalsKey && ( @@ -169,7 +177,7 @@ export default function CollectiblesTabs({ <> {totalInscriptions > 0 && ( - {t('TOTAL_ITEMS', { total: totalInscriptions || 0 })} + {t('TOTAL_ITEMS', { count: totalInscriptions })} )} {inscriptionListView} @@ -184,7 +192,7 @@ export default function CollectiblesTabs({ <> {totalNfts > 0 && ( - {t('TOTAL_ITEMS', { total: totalNfts || 0 })} + {t('TOTAL_ITEMS', { count: totalNfts })} )} {nftListView} @@ -195,7 +203,7 @@ export default function CollectiblesTabs({ {!rareSatsQuery.isLoading && ordinalBundleCount > 0 && ( - {t('TOTAL_ITEMS', { total: ordinalBundleCount })} + {t('TOTAL_ITEMS', { count: ordinalBundleCount })} )} diff --git a/src/app/screens/nftDashboard/index.tsx b/src/app/screens/nftDashboard/index.tsx index 6a9345813..7600d04ad 100644 --- a/src/app/screens/nftDashboard/index.tsx +++ b/src/app/screens/nftDashboard/index.tsx @@ -4,29 +4,16 @@ import ActionButton from '@components/button'; import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; import BottomTabBar from '@components/tabBar'; import WebGalleryButton from '@components/webGalleryButton'; -import useAddressInscriptionCollections from '@hooks/queries/ordinals/useAddressInscriptionCollections'; -import { useAddressRareSats } from '@hooks/queries/ordinals/useAddressRareSats'; -import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { ArrowDown, Wrench } from '@phosphor-icons/react'; -import type { InscriptionCollectionsData } from '@secretkeylabs/xverse-core'; -import { - ChangeActivateOrdinalsAction, - ChangeActivateRareSatsAction, - SetRareSatsNoticeDismissedAction, -} from '@stores/wallet/actions/actionCreators'; + +import { ArrowDown } from '@phosphor-icons/react'; import { StyledHeading } from '@ui-library/common.styled'; import Dialog from '@ui-library/dialog'; -import { getCollectionKey } from '@utils/inscriptions'; -import { InvalidParamsError } from '@utils/query'; -import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import CollectiblesTabs, { GridContainer } from './collectiblesTabs'; -import { InscriptionsTabGridItem } from './inscriptionsTabGridItem'; -import Nft from './nft'; +import CollectiblesTabs from './collectiblesTabs'; + import ReceiveNftModal from './receiveNft'; +import { useNftDashboard } from './useNftDashboard'; const Container = styled.div` display: flex; @@ -47,7 +34,6 @@ const PageHeader = styled.div` `; const StyledCollectiblesTabs = styled(CollectiblesTabs)` - margin-top: ${(props) => props.theme.spacing(8)}px; padding: 0 ${(props) => props.theme.space.s}; padding-bottom: ${(props) => props.theme.space.xl}; max-width: 1224px; @@ -86,259 +72,6 @@ const ReceiveButtonContainer = styled.div(() => ({ width: '100%', })); -const NoCollectiblesText = styled.h1((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), - marginBottom: 'auto', - textAlign: 'center', -})); - -const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(20), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', -})); - -const ErrorTextContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(8), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', -})); - -const ErrorText = styled.div((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_200, -})); - -const LoadMoreButtonContainer = styled.div((props) => ({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - marginBottom: props.theme.spacing(30), - marginTop: props.theme.space.xl, - button: { - width: 156, - }, -})); - -export type NftDashboardState = { - openReceiveModal: boolean; - showNewFeatureAlert: boolean; - isOrdinalReceiveAlertVisible: boolean; - stacksNftsQuery: ReturnType; - inscriptionsQuery: ReturnType; - rareSatsQuery: ReturnType; - openInGalleryView: () => void; - onReceiveModalOpen: () => void; - onReceiveModalClose: () => void; - onOrdinalReceiveAlertOpen: () => void; - onOrdinalReceiveAlertClose: () => void; - InscriptionListView: () => JSX.Element; - NftListView: () => JSX.Element; - onActivateRareSatsAlertCrossPress: () => void; - onActivateRareSatsAlertDenyPress: () => void; - onActivateRareSatsAlertEnablePress: () => void; - onDismissRareSatsNotice: () => void; - isGalleryOpen: boolean; - hasActivatedOrdinalsKey?: boolean; - hasActivatedRareSatsKey?: boolean; - showNoticeAlert?: boolean; - totalNfts: number; - totalInscriptions: number; -}; - -const useNftDashboard = (): NftDashboardState => { - const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); - const dispatch = useDispatch(); - const { hasActivatedOrdinalsKey, hasActivatedRareSatsKey, rareSatsNoticeDismissed } = - useWalletSelector(); - const [openReceiveModal, setOpenReceiveModal] = useState(false); - const [showNewFeatureAlert, setShowNewFeatureAlert] = useState(false); - const [showNoticeAlert, setShowNoticeAlert] = useState(false); - const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false); - const stacksNftsQuery = useStacksCollectibles(); - const inscriptionsQuery = useAddressInscriptionCollections(); - const rareSatsQuery = useAddressRareSats(); - - const ordinalsLength = inscriptionsQuery.data?.pages?.[0]?.total ?? 0; - const totalNfts = stacksNftsQuery.data?.pages?.[0]?.total ?? 0; - - const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); - - useEffect(() => { - if ( - (hasActivatedOrdinalsKey === undefined && ordinalsLength) || - hasActivatedRareSatsKey === undefined - ) { - setShowNewFeatureAlert(true); - } - }, [hasActivatedOrdinalsKey, hasActivatedRareSatsKey, ordinalsLength]); - - useEffect(() => { - setShowNoticeAlert(rareSatsNoticeDismissed === undefined); - }, [rareSatsNoticeDismissed]); - - const openInGalleryView = async () => { - await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/nft-dashboard'), - }); - }; - - const onReceiveModalOpen = () => { - setOpenReceiveModal(true); - }; - - const onReceiveModalClose = () => { - setOpenReceiveModal(false); - }; - - const onOrdinalReceiveAlertOpen = () => { - setIsOrdinalReceiveAlertVisible(true); - }; - - const onOrdinalReceiveAlertClose = () => { - setIsOrdinalReceiveAlertVisible(false); - }; - - const InscriptionListView = useCallback(() => { - if (inscriptionsQuery.error && !(inscriptionsQuery.error instanceof InvalidParamsError)) { - return ( - - - - {t('ERROR_RETRIEVING')} - {t('TRY_AGAIN')} - - - ); - } - - if (ordinalsLength === 0) { - return {t('NO_COLLECTIBLES')}; - } - - return ( - <> - - {inscriptionsQuery.data?.pages - ?.map((page) => page?.results) - .flat() - .map((collection: InscriptionCollectionsData) => ( - - ))} - - {inscriptionsQuery.hasNextPage && ( - - - - )} - - ); - }, [inscriptionsQuery, isGalleryOpen, ordinalsLength, t]); - - const NftListView = useCallback(() => { - if (stacksNftsQuery.error && !(stacksNftsQuery.error instanceof InvalidParamsError)) { - return ( - - - - {t('ERROR_RETRIEVING')} - {t('TRY_AGAIN')} - - - ); - } - - if (totalNfts === 0) { - return {t('NO_COLLECTIBLES')}; - } - - return ( - <> - - {stacksNftsQuery.data?.pages - ?.map((page) => page.nftsList) - .flat() - .map((nft) => ( - - ))} - - {stacksNftsQuery.hasNextPage && ( - - - - )} - - ); - }, [stacksNftsQuery, isGalleryOpen, totalNfts, t]); - - const onActivateRareSatsAlertCrossPress = () => { - setShowNewFeatureAlert(false); - }; - - const onActivateRareSatsAlertDenyPress = () => { - setShowNewFeatureAlert(false); - dispatch(ChangeActivateOrdinalsAction(true)); - dispatch(ChangeActivateRareSatsAction(false)); - }; - - const onActivateRareSatsAlertEnablePress = () => { - setShowNewFeatureAlert(false); - dispatch(ChangeActivateOrdinalsAction(true)); - dispatch(ChangeActivateRareSatsAction(true)); - }; - - const onDismissRareSatsNotice = () => { - setShowNoticeAlert(false); - dispatch(SetRareSatsNoticeDismissedAction(true)); - }; - - return { - openReceiveModal, - showNewFeatureAlert, - isOrdinalReceiveAlertVisible, - stacksNftsQuery, - inscriptionsQuery, - openInGalleryView, - onReceiveModalOpen, - onReceiveModalClose, - onOrdinalReceiveAlertOpen, - onOrdinalReceiveAlertClose, - InscriptionListView, - NftListView, - onActivateRareSatsAlertCrossPress, - onActivateRareSatsAlertDenyPress, - onActivateRareSatsAlertEnablePress, - onDismissRareSatsNotice, - isGalleryOpen, - hasActivatedOrdinalsKey, - hasActivatedRareSatsKey, - showNoticeAlert, - rareSatsQuery, - totalNfts, - totalInscriptions: ordinalsLength, - }; -}; - function NftDashboard() { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); const nftDashboard = useNftDashboard(); diff --git a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx index 91a273407..518c43b26 100644 --- a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx +++ b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx @@ -1,7 +1,7 @@ -import RareSatsCollage from '@components/bundleAsset/rareSatsCollage'; +import CollectibleCollage from '@components/collectibleCollage/collectibleCollage'; import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; import OrdinalImage from '@screens/ordinals/ordinalImage'; -import { InscriptionCollectionsData } from '@secretkeylabs/xverse-core/types'; +import { InscriptionCollectionsData } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { getCollectionKey, @@ -35,6 +35,7 @@ const StyledItemId = styled(StyledP)` text-wrap: nowrap; overflow: hidden; width: 100%; + text-overflow: ellipsis; `; const StyledItemSub = styled(StyledP)` @@ -73,7 +74,7 @@ export function InscriptionsTabGridItem({ onClick={isCollection(collection) ? handleClickCollectionId : handleClickInscriptionId} > {!collection.thumbnail_inscriptions ? ( // eslint-disable-line no-nested-ternary - + ) : !isCollection(collection) || collection.thumbnail_inscriptions.length === 1 ? ( // eslint-disable-line no-nested-ternary ) : ( - )} diff --git a/src/app/screens/nftDashboard/nft.tsx b/src/app/screens/nftDashboard/nft.tsx index fc879fe8c..e72e24bc3 100644 --- a/src/app/screens/nftDashboard/nft.tsx +++ b/src/app/screens/nftDashboard/nft.tsx @@ -1,103 +1,59 @@ -import styled from 'styled-components'; -import { NonFungibleToken, getBnsNftName } from '@secretkeylabs/xverse-core/types/index'; +import NftUser from '@assets/img/nftDashboard/bns.svg'; +import useNftDetail from '@hooks/queries/useNftDetail'; +import { NonFungibleToken } from '@secretkeylabs/xverse-core'; import { BNS_CONTRACT } from '@utils/constants'; -import NftUser from '@assets/img/nftDashboard/nft_user.svg'; -import { useNavigate } from 'react-router-dom'; -import useNftDataReducer from '@hooks/stores/useNftReducer'; +import styled from 'styled-components'; import NftImage from './nftImage'; interface Props { asset: NonFungibleToken; isGalleryOpen: boolean; } - -const NftNameText = styled.h1((props) => ({ - ...props.theme.body_bold_m, - textAlign: 'left', -})); - -const NftNameTextContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - justifyContent: 'flex-start', - marginTop: props.theme.spacing(6), -})); - interface ContainerProps { isGalleryView: boolean; } -const GradientContainer = styled.div((props) => ({ - display: 'flex', - width: props.isGalleryView ? '100%' : 150, - minHeight: props.isGalleryView ? 200 : 150, - height: props.isGalleryView ? '100%' : 150, - justifyContent: 'center', - alignItems: 'center', - borderRadius: 8, - background: props.theme.colors.elevation1, -})); - const NftImageContainer = styled.div((props) => ({ display: 'flex', justifyContent: 'center', alignItems: 'flex-start', width: '100%', + aspectRatio: '1', overflow: 'hidden', borderRadius: 8, background: props.theme.colors.elevation1, + '> img': { + width: '100%', + }, flexGrow: props.isGalleryView ? 1 : 'initial', })); -const GridItemContainer = styled.button((props) => ({ +const GridItemContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', - color: props.theme.colors.white['0'], + color: props.theme.colors.white_0, padding: 0, borderRadius: props.theme.radius(3), - background: 'transparent', + flex: 1, + aspectRatio: '1', })); -function Nft({ asset, isGalleryOpen }: Props) { - const navigate = useNavigate(); - const { storeNftData } = useNftDataReducer(); - - function getName() { - if (asset?.data?.token_metadata) { - return asset?.data.token_metadata?.name.length <= 35 - ? `${asset?.data.token_metadata?.name} ` - : `${asset?.data.token_metadata?.name.substring(0, 35)}...`; - } - - if (asset.asset_identifier === BNS_CONTRACT) { - return getBnsNftName(asset); - } - } - - const handleOnClick = () => { - const url = `${asset.asset_identifier}::${asset.value.repr}`; - storeNftData(asset?.data!); - if (asset.asset_identifier !== BNS_CONTRACT) { - navigate(`nft-detail/${url}`); - } - }; +const BnsImage = styled.img({ + width: '100%', + height: '100%', +}); +function Nft({ asset, isGalleryOpen }: Props) { + const { data } = useNftDetail(asset.identifier); return ( - - {asset.asset_identifier === BNS_CONTRACT ? ( - - - user - - - ) : ( - - - - )} - - {getName()} - + + + {asset.asset_identifier === BNS_CONTRACT ? ( + + ) : ( + + )} + ); } diff --git a/src/app/screens/nftDashboard/nftImage.tsx b/src/app/screens/nftDashboard/nftImage.tsx index 553581172..c8ddbe4a7 100644 --- a/src/app/screens/nftDashboard/nftImage.tsx +++ b/src/app/screens/nftDashboard/nftImage.tsx @@ -1,22 +1,22 @@ -import NftPlaceholderImage from '@assets/img/nftDashboard/ic_nft_diamond.svg'; import { BetterBarLoader } from '@components/barLoader'; -import { TokenMetaData } from '@secretkeylabs/xverse-core/types/api/stacks/assets'; +import { SquareLogo } from '@phosphor-icons/react'; +import { TokenMetaData } from '@secretkeylabs/xverse-core'; import { getFetchableUrl } from '@utils/helper'; import Image from 'rc-image'; -import { Suspense } from 'react'; -import { MoonLoader } from 'react-spinners'; +import { Suspense, useState } from 'react'; import styled from 'styled-components'; +import Theme from 'theme'; interface ContainerProps { isGalleryOpen: boolean; } -const ImageContainer = styled.div((props) => ({ +const ImageContainer = styled.div(() => ({ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', - height: props.isGalleryOpen ? '100%' : 150, + height: '100%', overflow: 'hidden', position: 'relative', borderRadius: 8, @@ -50,33 +50,39 @@ const Video = styled.video({ const StyledImg = styled(Image)` border-radius: 8px; object-fit: contain; - height: 150; `; interface Props { - metadata: TokenMetaData; + metadata?: TokenMetaData; + isInCollage?: boolean; +} + +function ErrorStateImg() { + return ; } -function NftImage({ metadata }: Props) { +function NftImage({ metadata, isInCollage = false }: Props) { + const [error, setError] = useState(false); const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; if (metadata?.image_protocol) { return ( - - - - - - } - fallback={NftPlaceholderImage} - /> - + + {error ? ( + + ) : ( + + + + + } + onError={() => setError(true)} + /> + + )} ); } @@ -94,7 +100,7 @@ function NftImage({ metadata }: Props) { return ( - + ); } diff --git a/src/app/screens/nftDashboard/nftTabGridItem.tsx b/src/app/screens/nftDashboard/nftTabGridItem.tsx new file mode 100644 index 000000000..f5b49a3bb --- /dev/null +++ b/src/app/screens/nftDashboard/nftTabGridItem.tsx @@ -0,0 +1,81 @@ +import CollectibleCollage from '@components/collectibleCollage/collectibleCollage'; +import { StacksCollectionData } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { getNftsTabGridItemSubText } from '@utils/nfts'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import Nft from './nft'; +import NftImage from './nftImage'; + +const CollectionContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.s, +})); + +const ThumbnailContainer = styled.button` + background: transparent; +`; + +const InfoContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; +`; + +const StyledItemId = styled(StyledP)` + text-align: left; + text-wrap: nowrap; + overflow: hidden; + width: 100%; + text-overflow: ellipsis; +`; + +const StyledItemSub = styled(StyledP)` + text-align: left; + text-overflow: ellipsis; + text-wrap: nowrap; + overflow: hidden; + width: 100%; +`; + +export function NftTabGridItem({ + item: collection, + isLoading = false, +}: { + item: StacksCollectionData; + isLoading?: boolean; +}) { + const navigate = useNavigate(); + + const handleClickCollection = () => { + navigate(`nft-collection/${collection.collection_id}`); + }; + + const itemId = collection.collection_name; + const itemSubText = getNftsTabGridItemSubText(collection); + + return ( + + + {isLoading ? ( + + ) : collection?.all_nfts?.length > 1 ? ( + + ) : ( + + )} + + + + {itemId} + + + {itemSubText} + + + + ); +} +export default NftTabGridItem; diff --git a/src/app/screens/nftDashboard/receiveNft/index.tsx b/src/app/screens/nftDashboard/receiveNft/index.tsx index 4da8532ba..99929009d 100644 --- a/src/app/screens/nftDashboard/receiveNft/index.tsx +++ b/src/app/screens/nftDashboard/receiveNft/index.tsx @@ -6,7 +6,7 @@ import ActionButton from '@components/button'; import UpdatedBottomModal from '@components/updatedBottomModal'; import useWalletSelector from '@hooks/useWalletSelector'; import { Plus } from '@phosphor-icons/react'; -import { isLedgerAccount } from '@utils/helper'; +import { isInOptions, isLedgerAccount } from '@utils/helper'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -171,9 +171,13 @@ function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAle src={plusIcon} text={t('ADD_STACKS_ADDRESS')} onPress={async () => { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/add-stx-address-ledger`), - }); + if (!isInOptions()) { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#/add-stx-address-ledger`), + }); + } else { + navigate('/add-stx-address-ledger'); + } }} /> )} diff --git a/src/app/screens/nftDashboard/useNftDashboard.tsx b/src/app/screens/nftDashboard/useNftDashboard.tsx new file mode 100644 index 000000000..7476b8e84 --- /dev/null +++ b/src/app/screens/nftDashboard/useNftDashboard.tsx @@ -0,0 +1,270 @@ +import ActionButton from '@components/button'; +import useAddressInscriptionCollections from '@hooks/queries/ordinals/useAddressInscriptionCollections'; +import { useAddressRareSats } from '@hooks/queries/ordinals/useAddressRareSats'; +import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { Wrench } from '@phosphor-icons/react'; +import type { InscriptionCollectionsData, StacksCollectionData } from '@secretkeylabs/xverse-core'; +import { + ChangeActivateOrdinalsAction, + ChangeActivateRareSatsAction, + SetRareSatsNoticeDismissedAction, +} from '@stores/wallet/actions/actionCreators'; +import { getCollectionKey } from '@utils/inscriptions'; +import { InvalidParamsError } from '@utils/query'; +import { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useIsVisible } from 'react-is-visible'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { GridContainer } from './collectiblesTabs'; +import { InscriptionsTabGridItem } from './inscriptionsTabGridItem'; +import { NftTabGridItem } from './nftTabGridItem'; + +const NoCollectiblesText = styled.h1((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_200, + marginTop: props.theme.spacing(16), + marginBottom: 'auto', + textAlign: 'center', +})); + +const ErrorContainer = styled.div((props) => ({ + marginTop: props.theme.spacing(20), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +})); + +const ErrorTextContainer = styled.div((props) => ({ + marginTop: props.theme.spacing(8), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +})); + +const ErrorText = styled.div((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_200, +})); + +const LoadMoreButtonContainer = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginBottom: props.theme.spacing(30), + marginTop: props.theme.space.xl, + button: { + width: 156, + }, +})); + +function IsVisibleOrPlaceholder({ children }: PropsWithChildren) { + const nodeRef = useRef(null); + const isVisible = useIsVisible(nodeRef, { once: false }); + + return ( +
+ {isVisible ? children : } +
+ ); +} + +export type NftDashboardState = { + openReceiveModal: boolean; + showNewFeatureAlert: boolean; + isOrdinalReceiveAlertVisible: boolean; + stacksNftsQuery: ReturnType; + inscriptionsQuery: ReturnType; + rareSatsQuery: ReturnType; + openInGalleryView: () => void; + onReceiveModalOpen: () => void; + onReceiveModalClose: () => void; + onOrdinalReceiveAlertOpen: () => void; + onOrdinalReceiveAlertClose: () => void; + InscriptionListView: () => JSX.Element; + NftListView: () => JSX.Element; + onActivateRareSatsAlertCrossPress: () => void; + onActivateRareSatsAlertDenyPress: () => void; + onActivateRareSatsAlertEnablePress: () => void; + onDismissRareSatsNotice: () => void; + isGalleryOpen: boolean; + hasActivatedOrdinalsKey?: boolean; + hasActivatedRareSatsKey?: boolean; + showNoticeAlert?: boolean; + totalNfts: number; + totalInscriptions: number; +}; + +export const useNftDashboard = (): NftDashboardState => { + const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); + const dispatch = useDispatch(); + const { hasActivatedOrdinalsKey, hasActivatedRareSatsKey, rareSatsNoticeDismissed } = + useWalletSelector(); + const [openReceiveModal, setOpenReceiveModal] = useState(false); + const [showNewFeatureAlert, setShowNewFeatureAlert] = useState(false); + const [showNoticeAlert, setShowNoticeAlert] = useState(false); + const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false); + const stacksNftsQuery = useStacksCollectibles(); + const inscriptionsQuery = useAddressInscriptionCollections(); + const rareSatsQuery = useAddressRareSats(); + + const totalInscriptions = inscriptionsQuery.data?.pages?.[0]?.total_inscriptions ?? 0; + const totalNfts = stacksNftsQuery.data?.total_nfts ?? 0; + + const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); + + useEffect(() => { + if ( + (hasActivatedOrdinalsKey === undefined && totalInscriptions) || + hasActivatedRareSatsKey === undefined + ) { + setShowNewFeatureAlert(true); + } + }, [hasActivatedOrdinalsKey, hasActivatedRareSatsKey, totalInscriptions]); + + useEffect(() => { + setShowNoticeAlert(rareSatsNoticeDismissed === undefined); + }, [rareSatsNoticeDismissed]); + + const openInGalleryView = async () => { + await chrome.tabs.create({ + url: chrome.runtime.getURL('options.html#/nft-dashboard'), + }); + }; + + const onReceiveModalOpen = () => { + setOpenReceiveModal(true); + }; + + const onReceiveModalClose = () => { + setOpenReceiveModal(false); + }; + + const onOrdinalReceiveAlertOpen = () => { + setIsOrdinalReceiveAlertVisible(true); + }; + + const onOrdinalReceiveAlertClose = () => { + setIsOrdinalReceiveAlertVisible(false); + }; + + const InscriptionListView = useCallback(() => { + if (inscriptionsQuery.error && !(inscriptionsQuery.error instanceof InvalidParamsError)) { + return ( + + + + {t('ERROR_RETRIEVING')} + {t('TRY_AGAIN')} + + + ); + } + + if (totalInscriptions === 0) { + return {t('NO_COLLECTIBLES')}; + } + + return ( + <> + + {inscriptionsQuery.data?.pages + ?.map((page) => page?.results) + .flat() + .map((collection: InscriptionCollectionsData) => ( + + ))} + + {inscriptionsQuery.hasNextPage && ( + + + + )} + + ); + }, [inscriptionsQuery, isGalleryOpen, totalInscriptions, t]); + + const NftListView = useCallback(() => { + if (stacksNftsQuery.error && !(stacksNftsQuery.error instanceof InvalidParamsError)) { + return ( + + + + {t('ERROR_RETRIEVING')} + {t('TRY_AGAIN')} + + + ); + } + + if (totalNfts === 0) { + return {t('NO_COLLECTIBLES')}; + } + + return ( + + {stacksNftsQuery.data?.results.map((collection: StacksCollectionData) => ( + + + + ))} + + ); + }, [stacksNftsQuery, isGalleryOpen, totalNfts, t]); + + const onActivateRareSatsAlertCrossPress = () => { + setShowNewFeatureAlert(false); + }; + + const onActivateRareSatsAlertDenyPress = () => { + setShowNewFeatureAlert(false); + dispatch(ChangeActivateOrdinalsAction(true)); + dispatch(ChangeActivateRareSatsAction(false)); + }; + + const onActivateRareSatsAlertEnablePress = () => { + setShowNewFeatureAlert(false); + dispatch(ChangeActivateOrdinalsAction(true)); + dispatch(ChangeActivateRareSatsAction(true)); + }; + + const onDismissRareSatsNotice = () => { + setShowNoticeAlert(false); + dispatch(SetRareSatsNoticeDismissedAction(true)); + }; + + return { + openReceiveModal, + showNewFeatureAlert, + isOrdinalReceiveAlertVisible, + stacksNftsQuery, + inscriptionsQuery, + openInGalleryView, + onReceiveModalOpen, + onReceiveModalClose, + onOrdinalReceiveAlertOpen, + onOrdinalReceiveAlertClose, + InscriptionListView, + NftListView, + onActivateRareSatsAlertCrossPress, + onActivateRareSatsAlertDenyPress, + onActivateRareSatsAlertEnablePress, + onDismissRareSatsNotice, + isGalleryOpen, + hasActivatedOrdinalsKey, + hasActivatedRareSatsKey, + showNoticeAlert, + rareSatsQuery, + totalNfts, + totalInscriptions, + }; +}; + +export default useNftDashboard; diff --git a/src/app/screens/nftDetail/index.tsx b/src/app/screens/nftDetail/index.tsx index 5497fc607..1c1c6866d 100644 --- a/src/app/screens/nftDetail/index.tsx +++ b/src/app/screens/nftDetail/index.tsx @@ -1,47 +1,30 @@ -import ArrowSquareOut from '@assets/img/arrow_square_out.svg'; -import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; -import ArrowUp from '@assets/img/dashboard/arrow_up.svg'; -import ShareNetwork from '@assets/img/nftDashboard/share.svg'; import SquaresFour from '@assets/img/nftDashboard/squares_four.svg'; import AccountHeaderComponent from '@components/accountHeader'; +import { BetterBarLoader } from '@components/barLoader'; import ActionButton from '@components/button'; import CollectibleDetailTile from '@components/collectibleDetailTile'; -import ShareDialog from '@components/shareNft'; -import SmallActionButton from '@components/smallActionButton'; +import Separator from '@components/separator'; +import SquareButton from '@components/squareButton'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import useNftDataSelector from '@hooks/stores/useNftDataSelector'; -import useNftDataReducer from '@hooks/stores/useNftReducer'; -import { useResetUserFlow } from '@hooks/useResetUserFlow'; -import useWalletSelector from '@hooks/useWalletSelector'; +import { ArrowLeft, ArrowUp, Share } from '@phosphor-icons/react'; import NftImage from '@screens/nftDashboard/nftImage'; -import { getNftDetail } from '@secretkeylabs/xverse-core/api'; -import { NftDetailResponse } from '@secretkeylabs/xverse-core/types'; -import { NftData } from '@secretkeylabs/xverse-core/types/api/stacks/assets'; -import { useMutation } from '@tanstack/react-query'; -import { GAMMA_URL } from '@utils/constants'; -import { getExplorerUrl, isLedgerAccount } from '@utils/helper'; -import { useEffect, useState } from 'react'; +import { Attribute } from '@secretkeylabs/xverse-core'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; -import { MoonLoader } from 'react-spinners'; +import { Tooltip } from 'react-tooltip'; import styled from 'styled-components'; import NftAttribute from './nftAttribute'; +import useNftDetailScreen from './useNftDetail'; -const Container = styled.div` - display: flex; - flex-direction: column; - flex: 1; - overflow-y: auto; - margin-left: 5%; - margin-right: 5%; - &::-webkit-scrollbar { - display: none; - } -`; - -const ReceiveButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(12), +const ExtensionContainer = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + marginTop: props.theme.spacing(4), + alignItems: 'center', + flex: 1, + paddingLeft: props.theme.spacing(4), + paddingRight: props.theme.spacing(4), })); const GalleryReceiveButtonContainer = styled.div((props) => ({ @@ -52,50 +35,40 @@ const GalleryReceiveButtonContainer = styled.div((props) => ({ const BackButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', + width: 800, marginTop: props.theme.spacing(40), })); const ButtonContainer = styled.div((props) => ({ display: 'flex', - position: 'relative', flexDirection: 'row', - maxWidth: 400, - marginBottom: props.theme.spacing(13.5), + justifyContent: 'center', + columnGap: props.theme.spacing(11), + paddingBottom: props.theme.spacing(16), + marginBottom: props.theme.spacing(4), + marginTop: props.theme.spacing(4), + width: '100%', + borderBottom: `1px solid ${props.theme.colors.elevation3}`, })); -const ShareDialogeContainer = styled.div({ - position: 'absolute', - top: 0, - right: 0, -}); - -const GalleryShareDialogeContainer = styled.div({ - position: 'absolute', - top: 0, - right: 0, - zIndex: 2000, -}); - -const ExtensionContainer = styled.div({ +const ColumnContainer = styled.div({ display: 'flex', flexDirection: 'column', - marginTop: 8, - alignItems: 'center', - flex: 1, }); -const NFtContainer = styled.div((props) => ({ - maxWidth: 450, - width: '60%', +const NftContainer = styled.div((props) => ({ + width: 376.5, + height: 376.5, display: 'flex', - aspectRatio: 1, - justifyContent: 'center', + aspectRatio: '1', + flexDirection: 'column', + justifyContent: 'flex-start', alignItems: 'flex-start', borderRadius: 8, marginBottom: props.theme.spacing(12), })); -const ExtensionNFtContainer = styled.div((props) => ({ +const ExtensionNftContainer = styled.div((props) => ({ maxHeight: 148, width: 148, display: 'flex', @@ -107,39 +80,31 @@ const ExtensionNFtContainer = styled.div((props) => ({ })); const NftTitleText = styled.h1((props) => ({ - ...props.theme.headline_m, + ...props.theme.typography.headline_m, color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(12), textAlign: 'center', })); -const CollectibleText = styled.h1((props) => ({ - ...props.theme.body_bold_m, +const CollectibleText = styled.p((props) => ({ + ...props.theme.typography.body_bold_m, color: props.theme.colors.white_400, textAlign: 'center', })); const NftGalleryTitleText = styled.h1((props) => ({ - ...props.theme.headline_l, + ...props.theme.typography.headline_l, color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(12), -})); - -const DescriptionText = styled.h1((props) => ({ - ...props.theme.headline_l, - color: props.theme.colors.white_0, - fontSize: 24, - marginBottom: props.theme.spacing(16), + marginBottom: props.theme.spacing(4), })); -const NftOwnedByText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const NftOwnedByText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_400, textAlign: 'center', })); -const OwnerAddressText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const OwnerAddressText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, textAlign: 'center', marginLeft: props.theme.spacing(3), })); @@ -148,21 +113,19 @@ const BottomBarContainer = styled.div({ marginTop: 'auto', }); -const RowContainer = styled.h1((props) => ({ +const RowContainer = styled.div({ display: 'flex', - marginTop: props.theme.spacing(3), flexDirection: 'row', -})); +}); const GridContainer = styled.div((props) => ({ display: 'grid', + width: '100%', marginTop: props.theme.spacing(6), - columnGap: props.theme.spacing(8), - rowGap: props.theme.spacing(6), + columnGap: props.theme.spacing(4), + rowGap: props.theme.spacing(4), gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))', - paddingBottom: props.theme.spacing(16), - marginBottom: props.theme.spacing(12), - borderBottom: `1px solid ${props.theme.colors.elevation2}`, + marginBottom: props.theme.spacing(8), })); const ShareButtonContainer = styled.div((props) => ({ @@ -170,19 +133,16 @@ const ShareButtonContainer = styled.div((props) => ({ width: '100%', })); -const DescriptionContainer = styled.h1((props) => ({ +const DescriptionContainer = styled.div((props) => ({ display: 'flex', marginLeft: props.theme.spacing(20), flexDirection: 'column', marginBottom: props.theme.spacing(30), })); -const AttributeText = styled.h1((props) => ({ - ...props.theme.headline_category_s, +const AttributeText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_400, - marginBottom: props.theme.spacing(2), - letterSpacing: '0.02em', - textTransform: 'uppercase', })); const WebGalleryButton = styled.button((props) => ({ @@ -193,12 +153,12 @@ const WebGalleryButton = styled.button((props) => ({ borderRadius: props.theme.radius(1), backgroundColor: 'transparent', width: '100%', - marginTop: props.theme.spacing(29), + marginTop: props.theme.spacing(4), + marginBottom: props.theme.spacing(12), })); const WebGalleryButtonText = styled.div((props) => ({ - ...props.theme.body_m, - fontWeight: 700, + ...props.theme.typography.body_bold_m, color: props.theme.colors.white_200, textAlign: 'center', })); @@ -209,7 +169,7 @@ const ButtonImage = styled.img((props) => ({ transform: 'all', })); -const Button = styled.button((props) => ({ +const BackButton = styled.button((props) => ({ display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', @@ -218,232 +178,395 @@ const Button = styled.button((props) => ({ marginBottom: props.theme.spacing(12), })); -const ButtonText = styled.h1((props) => ({ - ...props.theme.body_m, +const ExtensionLoaderContainer = styled.div({ + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + alignItems: 'center', +}); + +const SeeDetailsButtonContainer = styled.div((props) => ({ + marginBottom: props.theme.spacing(27), + marginTop: props.theme.spacing(4), +})); + +const Button = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'transparent', + width: props.isGallery ? 400 : 328, + height: 44, + padding: 12, + borderRadius: 12, + marginTop: props.theme.spacing(6), + border: `1px solid ${props.theme.colors.white_800}`, +})); + +const ButtonText = styled.p((props) => ({ + ...props.theme.typography.body_m, color: props.theme.colors.white_400, })); const AssetDeatilButtonText = styled.div((props) => ({ - ...props.theme.body_xs, - fontWeight: 400, - fontSize: 14, + ...props.theme.typography.body_m, color: props.theme.colors.white_0, + marginLeft: 2, textAlign: 'center', })); -const ButtonHiglightedText = styled.h1((props) => ({ - ...props.theme.body_m, +const GalleryCollectibleText = styled.p((props) => ({ + ...props.theme.typography.body_bold_l, + color: props.theme.colors.white_400, +})); + +const GalleryScrollContainer = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + flex: 1, + alignItems: 'center', +})); + +const ButtonHiglightedText = styled.p((props) => ({ + ...props.theme.typography.body_m, color: props.theme.colors.white_0, marginLeft: props.theme.spacing(2), marginRight: props.theme.spacing(2), })); -const LoaderContainer = styled.div({ +const GalleryRowContainer = styled.div<{ + withGap?: boolean; +}>((props) => ({ display: 'flex', - flex: 1, + alignItems: 'flex-start', + marginTop: props.theme.spacing(8), + marginBottom: props.theme.spacing(12), + flexDirection: 'row', + columnGap: props.withGap ? props.theme.spacing(20) : 0, +})); + +const StyledTooltip = styled(Tooltip)` + &&& { + font-size: 12px; + background-color: #ffffff; + color: #12151e; + border-radius: 8px; + padding: 7px; + } +`; + +const StyledBarLoader = styled(BetterBarLoader)<{ + withMarginBottom?: boolean; +}>((props) => ({ + padding: 0, + borderRadius: props.theme.radius(1), + marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0, +})); + +const GalleryContainer = styled.div({ + marginLeft: 'auto', + marginRight: 'auto', + display: 'flex', + flexDirection: 'column', justifyContent: 'center', alignItems: 'center', + width: '100%', + maxWidth: 1224, +}); + +const ActionButtonLoader = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + rowGap: props.theme.spacing(4), +})); + +const ActionButtonsLoader = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + columnGap: props.theme.spacing(11), +})); + +const GalleryLoaderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', }); +const StyledSeparator = styled(Separator)` + width: 100%; +`; + +const TitleLoader = styled.div` + display: flex; + flex-direction: column; +`; +interface DetailSectionProps { + isGallery: boolean; +} + +const NftDetailsContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'flex-start', + flexDirection: 'column', + wordBreak: 'break-all', + whiteSpace: 'pre-wrap', + width: props.isGallery ? 400 : '100%', + marginTop: props.theme.spacing(8), +})); + +const DetailSection = styled.div((props) => ({ + display: 'flex', + flexDirection: !props.isGallery ? 'row' : 'column', + justifyContent: 'center', + width: '100%', + columnGap: props.theme.spacing(8), +})); + +const InfoContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + padding: `0 ${props.theme.spacing(8)}px`, +})); + function NftDetailScreen() { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DETAIL_SCREEN' }); - const navigate = useNavigate(); - const { stxAddress, selectedAccount } = useWalletSelector(); - const { id } = useParams(); - const nftIdDetails = id!.split('::'); - const { nftData } = useNftDataSelector(); - const { storeNftData } = useNftDataReducer(); - const [nft, setNft] = useState(undefined); const { + nft, + nftData, + collection, + stxAddress, isLoading, - data: nftDetailsData, - mutate, - } = useMutation({ - mutationFn: async ({ principal }) => { - const contractInfo: string[] = principal.split('.'); - return getNftDetail(nftIdDetails[2].replace('u', ''), contractInfo[0], contractInfo[1]); - }, - }); - - const [showShareNftOptions, setShowNftOptions] = useState(false); - const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; - - useResetUserFlow('/nft-detail'); - - useEffect(() => { - const data = nftData.find( - (nftItem) => Number(nftItem?.token_id) === Number(nftIdDetails[2].slice(1)), - ); - if (!data) { - mutate({ principal: nftIdDetails[0] }); - } else { - setNft(data); - } - }, []); - - useEffect(() => { - if (nftDetailsData) { - storeNftData(nftDetailsData.data); - setNft(nftDetailsData?.data); - } - }, [nftDetailsData]); - - const handleBackButtonClick = () => { - navigate('/nft-dashboard?tab=nfts'); - }; - - const onSharePress = () => { - setShowNftOptions(true); - }; - - const onCrossPress = () => { - setShowNftOptions(false); - }; - - const onGammaPress = () => { - window.open(`${GAMMA_URL}collections/${nft?.token_metadata?.contract_id}`); - }; - - const onExplorerPress = () => { - const address = nft?.token_metadata?.contract_id?.split('.')!; - window.open(getExplorerUrl(address[0])); - }; - - const openInGalleryView = async () => { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/nft-dashboard/nft-detail/${id}`), - }); - }; - - const handleOnSendClick = async () => { - if (isLedgerAccount(selectedAccount)) { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/send-nft/${id}`), - }); - return; - } - - navigate('send-nft', { - state: { - nft, - }, - }); - }; - - const ownedByView = ( - - {t('OWNED_BY')} - - {`${stxAddress.substring(0, 4)}...${stxAddress.substring( - stxAddress.length - 4, - stxAddress.length, - )}`} - - + isGalleryOpen, + onSharePress, + handleBackButtonClick, + onGammaPress, + onExplorerPress, + openInGalleryView, + handleOnSendClick, + galleryTitle, + } = useNftDetailScreen(); + + const nftAttributes = nftData?.nft_token_attributes?.length !== 0 && ( + <> + {t('ATTRIBUTES')} + + {nftData?.nft_token_attributes?.map((attribute: Attribute) => ( + + ))} + + + ); + const nftDetails = ( + + {collection?.collection_name && ( + + + + + )} + {!isGalleryOpen && nftAttributes} + + + {nftData?.rarity_score && ( + + )} + + + ); - const extensionView = ( + const extensionView = isLoading ? ( + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+
+
+ ) : ( {t('COLLECTIBLE')} - {nft?.token_metadata.name} - - - - - - - - - - {showShareNftOptions && ( - - )} - - - {ownedByView} + {nftData?.token_metadata?.name} <> {t('WEB_GALLERY')} + + {nft && } + + + } + text={t('SEND')} + onPress={handleOnSendClick} + /> + } + text={t('SHARE')} + onPress={onSharePress} + hoverDialogId={`copy-nft-url-${nftData?.asset_id}`} + isTransparent + /> + + + {nftDetails} + + + + ); - const galleryView = - isLoading || !nft ? ( - - - - ) : ( - + const galleryView = isLoading ? ( + + - + - {nft?.token_metadata.name} - - - - - - - - - - {showShareNftOptions && ( - - )} - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + + + + + + {t('MOVE_TO_ASSET_DETAIL')} + + + + + {nft && } + {nftAttributes} + - {t('DESCRIPTION')} - - {nft?.rarity_score && ( - - )} - - {nft?.nft_token_attributes.length !== 0 && ( - <> - {t('ATTRIBUTES')} - - {nft?.nft_token_attributes.map((attribute) => ( - - ))} - - - )} - - - - - ); + + + + ); return ( <> @@ -452,7 +575,7 @@ function NftDetailScreen() { ) : ( )} - {isGalleryOpen ? galleryView : extensionView} + {isGalleryOpen ? galleryView : extensionView} {!isGalleryOpen && ( diff --git a/src/app/screens/nftDetail/nftAttribute.tsx b/src/app/screens/nftDetail/nftAttribute.tsx index 7f00fd22c..3d1b7eef0 100644 --- a/src/app/screens/nftDetail/nftAttribute.tsx +++ b/src/app/screens/nftDetail/nftAttribute.tsx @@ -1,25 +1,23 @@ import styled from 'styled-components'; -const Container = styled.h1((props) => ({ +const Container = styled.div((props) => ({ display: 'flex', - borderRadius: 20, + borderRadius: 12, border: `1px solid ${props.theme.colors.elevation3}`, - flexDirection: 'row', - marginEnd: 10, - padding: props.theme.spacing(5), - alignItems: 'center', + flexDirection: 'column', + padding: '8px 16px', + alignItems: 'flex-start', })); const TypeText = styled.h1((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, color: props.theme.colors.white_400, })); const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_0, wordBreak: 'break-all', - marginLeft: props.theme.spacing(3), })); interface Props { diff --git a/src/app/screens/nftDetail/useNftDetail.ts b/src/app/screens/nftDetail/useNftDetail.ts new file mode 100644 index 000000000..61443e77d --- /dev/null +++ b/src/app/screens/nftDetail/useNftDetail.ts @@ -0,0 +1,78 @@ +import useNftDetail from '@hooks/queries/useNftDetail'; +import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; +import useResetUserFlow from '@hooks/useResetUserFlow'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { GAMMA_URL } from '@utils/constants'; +import { getExplorerUrl, isInOptions, isLedgerAccount } from '@utils/helper'; +import { useMemo } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +export default function useNftDetailScreen() { + const navigate = useNavigate(); + const { stxAddress, selectedAccount } = useWalletSelector(); + const { id } = useParams(); + + const nftDetailQuery = useNftDetail(id!); + const nftCollectionsQuery = useStacksCollectibles(); + const collectionId = nftDetailQuery.data?.data.collection_contract_id; + const collection = nftCollectionsQuery.data?.results.find( + (c) => c.collection_id === collectionId, + ); + + const metadata = nftDetailQuery.data?.data?.token_metadata; + const gammaUrl = `${GAMMA_URL}collections/${metadata?.contract_id}/${nftDetailQuery.data?.data?.token_id}`; + + useResetUserFlow('/nft-detail'); + + const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); + const galleryTitle = metadata?.name; + + const onSharePress = () => { + navigator.clipboard.writeText(gammaUrl); + }; + + const handleBackButtonClick = () => { + navigate(`/nft-dashboard/nft-collection/${collectionId}`); + }; + + const onGammaPress = () => { + window.open(gammaUrl); + }; + + const onExplorerPress = () => { + const address = metadata?.contract_id?.split('.')!; + window.open(getExplorerUrl(address[0])); + }; + + const openInGalleryView = async () => { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#/nft-dashboard/nft-detail/${id}`), + }); + }; + + const handleOnSendClick = async () => { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#/nft-dashboard/nft-detail/${id}/send-nft`), + }); + return; + } + navigate(`/nft-dashboard/nft-detail/${id}/send-nft`); + }; + + return { + nft: nftDetailQuery.data, + nftData: nftDetailQuery.data?.data, + collection, + stxAddress, + isLoading: nftDetailQuery.isLoading, + isGalleryOpen, + onSharePress, + handleBackButtonClick, + onGammaPress, + onExplorerPress, + openInGalleryView, + handleOnSendClick, + galleryTitle, + }; +} diff --git a/src/app/screens/ordinalDetail/useOrdinalDetail.ts b/src/app/screens/ordinalDetail/useOrdinalDetail.ts index abd694b05..7704e5b78 100644 --- a/src/app/screens/ordinalDetail/useOrdinalDetail.ts +++ b/src/app/screens/ordinalDetail/useOrdinalDetail.ts @@ -8,7 +8,7 @@ import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; import useTextOrdinalContent from '@hooks/useTextOrdinalContent'; import useWalletSelector from '@hooks/useWalletSelector'; import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; -import { getBtcTxStatusUrl, isLedgerAccount } from '@utils/helper'; +import { getBtcTxStatusUrl, isInOptions, isLedgerAccount } from '@utils/helper'; import { getInscriptionsCollectionGridItemSubText, getInscriptionsCollectionGridItemSubTextColor, @@ -83,7 +83,7 @@ export default function useOrdinalDetail() { return; } if (ordinalData) setSelectedOrdinalDetails(ordinalData); - if (isLedgerAccount(selectedAccount)) { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ url: chrome.runtime.getURL(`options.html#/nft-dashboard/ordinal-detail/${id}/send-ordinal`), }); diff --git a/src/app/screens/ordinalsCollection/index.tsx b/src/app/screens/ordinalsCollection/index.tsx index 70017292e..51b28fe7e 100644 --- a/src/app/screens/ordinalsCollection/index.tsx +++ b/src/app/screens/ordinalsCollection/index.tsx @@ -1,6 +1,7 @@ import AccountHeaderComponent from '@components/accountHeader'; import { BetterBarLoader } from '@components/barLoader'; import ActionButton from '@components/button'; +import CollectibleCollectionGridItem from '@components/collectibleCollectionGridItem'; import CollectibleDetailTile from '@components/collectibleDetailTile'; import Separator from '@components/separator'; import BottomTabBar from '@components/tabBar'; @@ -10,15 +11,22 @@ import WebGalleryButton from '@components/webGalleryButton'; import WrenchErrorMessage from '@components/wrenchErrorMessage'; import useAddressInscriptions from '@hooks/queries/ordinals/useAddressInscriptions'; import useInscriptionCollectionMarketData from '@hooks/queries/ordinals/useCollectionMarketData'; +import useOrdinalDataReducer from '@hooks/stores/useOrdinalReducer'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import { ArrowLeft } from '@phosphor-icons/react'; import { GridContainer } from '@screens/nftDashboard/collectiblesTabs'; +import OrdinalImage from '@screens/ordinals/ordinalImage'; +import { Inscription } from '@secretkeylabs/xverse-core'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; +import { + getInscriptionsCollectionGridItemId, + getInscriptionsCollectionGridItemSubText, + getInscriptionsCollectionGridItemSubTextColor, +} from '@utils/inscriptions'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { OrdinalsCollectionGridItem } from './ordinalsCollectionGridItem'; interface DetailSectionProps { isGalleryOpen?: boolean; @@ -27,11 +35,9 @@ interface DetailSectionProps { /* layout */ // TODO tim: create a reusable layout const Container = styled.div` - overflow-y: auto; display: flex; flex-direction: column; flex: 1; - overflow-y: auto; ${(props) => props.theme.scrollbar} `; @@ -129,8 +135,9 @@ const StyledBarLoader = styled(BetterBarLoader)((props) => ({ })); function OrdinalsCollection() { - const { t } = useTranslation('translation', { keyPrefix: 'ORDINALS_COLLECTION_SCREEN' }); + const { t } = useTranslation('translation', { keyPrefix: 'COLLECTIBLE_COLLECTION_SCREEN' }); const navigate = useNavigate(); + const { setSelectedOrdinalDetails } = useOrdinalDataReducer(); const { id: collectionId } = useParams(); const { data, error, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } = useAddressInscriptions(collectionId); @@ -151,7 +158,7 @@ function OrdinalsCollection() { }); }; - const isEmpty = !isLoading && !error && data?.pages?.[0]?.data?.total === 0; + const isEmpty = !isLoading && !error && data?.pages?.[0]?.total === 0; const collectionHeading = data?.pages?.[0].collection_name; const estPortfolioValue = @@ -162,6 +169,11 @@ function OrdinalsCollection() { ? `${collectionMarketData?.floor_price?.toFixed(8)} BTC` : '--'; + const handleOnClick = (item: Inscription) => { + setSelectedOrdinalDetails(item); + navigate(`/nft-dashboard/ordinal-detail/${item.id}`); + }; + return ( <> {isGalleryOpen ? ( @@ -222,7 +234,16 @@ function OrdinalsCollection() { ?.map((page) => page?.data) .flat() .map((inscription) => ( - + + + )) )} diff --git a/src/app/screens/rareSatsBundle/index.tsx b/src/app/screens/rareSatsBundle/index.tsx index ed00dc4ef..985bf887b 100644 --- a/src/app/screens/rareSatsBundle/index.tsx +++ b/src/app/screens/rareSatsBundle/index.tsx @@ -14,7 +14,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowRight, ArrowUp } from '@phosphor-icons/react'; import { GridContainer } from '@screens/nftDashboard/collectiblesTabs'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; -import { getBtcTxStatusUrl, isLedgerAccount } from '@utils/helper'; +import { getBtcTxStatusUrl, isInOptions, isLedgerAccount } from '@utils/helper'; import { BundleItem } from '@utils/rareSats'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -171,7 +171,7 @@ function RareSatsBundle() { return setShowSendOrdinalsAlert(true); } - if (isLedgerAccount(selectedAccount)) { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ url: chrome.runtime.getURL('options.html#/nft-dashboard/send-rare-sat'), }); diff --git a/src/app/screens/rareSatsDetail/rareSatsDetail.tsx b/src/app/screens/rareSatsDetail/rareSatsDetail.tsx index 57e7bef4c..b5bb2b9a0 100644 --- a/src/app/screens/rareSatsDetail/rareSatsDetail.tsx +++ b/src/app/screens/rareSatsDetail/rareSatsDetail.tsx @@ -14,7 +14,12 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowRight, ArrowUp, Circle } from '@phosphor-icons/react'; import Callout from '@ui-library/callout'; import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; -import { getBtcTxStatusUrl, getTruncatedAddress, isLedgerAccount } from '@utils/helper'; +import { + getBtcTxStatusUrl, + getTruncatedAddress, + isInOptions, + isLedgerAccount, +} from '@utils/helper'; import { BundleItem, getBundleItemId, @@ -292,7 +297,7 @@ function RareSatsDetailScreen() { return showAlert(); } - if (isLedgerAccount(selectedAccount)) { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ url: chrome.runtime.getURL('options.html#/nft-dashboard/send-rare-sat'), }); diff --git a/src/app/screens/sendNft/index.tsx b/src/app/screens/sendNft/index.tsx index 7704b6672..84ff4a26e 100644 --- a/src/app/screens/sendNft/index.tsx +++ b/src/app/screens/sendNft/index.tsx @@ -1,146 +1,131 @@ -import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; -import AccountHeaderComponent from '@components/accountHeader'; -import SendForm from '@components/sendForm'; -import BottomBar from '@components/tabBar'; -import TopRow from '@components/topRow'; +import ActionButton from '@components/button'; +import { useBnsName, useBnsResolver } from '@hooks/queries/useBnsName'; +import useNftDetail from '@hooks/queries/useNftDetail'; import useStxPendingTxData from '@hooks/queries/useStxPendingTxData'; -import useNftDataSelector from '@hooks/stores/useNftDataSelector'; +import useDebounce from '@hooks/useDebounce'; import useNetworkSelector from '@hooks/useNetwork'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useWalletSelector from '@hooks/useWalletSelector'; -import NftImage from '@screens/nftDashboard/nftImage'; -import { validateStxAddress } from '@secretkeylabs/xverse-core'; -import { generateUnsignedTransaction } from '@secretkeylabs/xverse-core/transactions'; import { cvToHex, + generateUnsignedTransaction, StacksTransaction, uintCV, UnsignedStacksTransation, -} from '@secretkeylabs/xverse-core/types'; -import { NftData } from '@secretkeylabs/xverse-core/types/api/stacks/assets'; + validateStxAddress, +} from '@secretkeylabs/xverse-core'; import { useMutation } from '@tanstack/react-query'; -import { checkNftExists, isLedgerAccount } from '@utils/helper'; +import { StyledHeading, StyledP } from '@ui-library/common.styled'; +import { InputFeedback, InputFeedbackProps, isDangerFeedback } from '@ui-library/inputFeedback'; +import { checkNftExists } from '@utils/helper'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import styled from 'styled-components'; +import SendLayout from '../../layouts/sendLayout'; -const ScrollContainer = styled.div` +const Container = styled.div` display: flex; - flex: 1; flex-direction: column; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } - width: 360px; - margin: auto; + justify-content: space-between; + flex-grow: 1; `; -const Container = styled.div({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - flex: 1, -}); - -const NftContainer = styled.div((props) => ({ - maxHeight: 148, - width: 148, - display: 'flex', - aspectRatio: 1, - justifyContent: 'center', - alignItems: 'center', - borderRadius: 8, - marginTop: props.theme.spacing(16), - marginBottom: props.theme.spacing(12), -})); +const StyledSendTo = styled(StyledHeading)` + margin-bottom: ${(props) => props.theme.space.l}; +`; -const NftTitleText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white_0, - textAlign: 'center', +const NextButtonContainer = styled.div((props) => ({ + position: 'sticky', + bottom: 0, + paddingBottom: props.theme.spacing(12), + paddingTop: props.theme.spacing(12), + backgroundColor: props.theme.colors.elevation0, })); -const BottomBarContainer = styled.div({ - marginTop: 'auto', -}); +const InputGroup = styled.div` + margin-top: ${(props) => props.theme.spacing(8)}px; +`; -const ButtonContainer = styled.div((props) => ({ +const Label = styled.label((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, display: 'flex', - flexDirection: 'row', - marginLeft: '15%', - marginTop: props.theme.spacing(40), + flex: 1, })); -const Button = styled.button((props) => ({ +const AmountInputContainer = styled.div<{ error: boolean }>((props) => ({ display: 'flex', flexDirection: 'row', - justifyContent: 'flex-end', alignItems: 'center', + marginTop: props.theme.spacing(4), + marginBottom: props.theme.spacing(4), + border: props.error + ? `1px solid ${props.theme.colors.danger_dark_200}` + : `1px solid ${props.theme.colors.white_800}`, + backgroundColor: props.theme.colors.elevation_n1, borderRadius: props.theme.radius(1), - backgroundColor: 'transparent', - opacity: 0.8, - marginTop: props.theme.spacing(5), + paddingLeft: props.theme.spacing(5), + paddingRight: props.theme.spacing(5), + height: 44, })); -const ButtonText = styled.div((props) => ({ - ...props.theme.body_xs, - fontWeight: 400, - fontSize: 14, +const InputFieldContainer = styled.div(() => ({ + flex: 1, +})); + +const InputField = styled.input((props) => ({ + ...props.theme.typography.body_m, + backgroundColor: 'transparent', color: props.theme.colors.white_0, - textAlign: 'center', + width: '100%', + border: 'transparent', })); -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', +const ErrorContainer = styled.div((props) => ({ + marginTop: props.theme.spacing(3), + marginBottom: props.theme.spacing(12), })); +const RowContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + function SendNft() { const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); const navigate = useNavigate(); - const { id } = useParams(); const location = useLocation(); - let address: string | undefined; + const [recipientError, setRecipientError] = useState(null); + const [recipientAddress, setRecipientAddress] = useState(location.state?.recipientAddress ?? ''); - if (location.state) { - address = location.state.recipientAddress; - } - const { nftData } = useNftDataSelector(); - const nftIdDetails = id!.split('::'); - const [nft, setNft] = useState(undefined); + useResetUserFlow('/send-nft'); + + const { id } = useParams(); + const { data: nftDetail } = useNftDetail(id!); + const nft = nftDetail?.data; - useEffect(() => { - const data = nftData.find( - (nftItem) => Number(nftItem?.token_id) === Number(nftIdDetails[2].slice(1)), - ); - if (data) { - setNft(data); - } - }, []); const selectedNetwork = useNetworkSelector(); const { data: stxPendingTxData } = useStxPendingTxData(); - const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; - const { stxAddress, stxPublicKey, network, feeMultipliers, selectedAccount } = - useWalletSelector(); - const [error, setError] = useState(''); - const [recipientAddress, setRecipientAddress] = useState(''); + const { stxAddress, stxPublicKey, network, feeMultipliers } = useWalletSelector(); + const debouncedSearchTerm = useDebounce(recipientAddress, 300); + const associatedBnsName = useBnsName(debouncedSearchTerm); + const associatedAddress = useBnsResolver(debouncedSearchTerm, stxAddress); + const { isLoading, data, mutate } = useMutation< StacksTransaction, Error, - { tokenId: string; associatedAddress: string } + { tokenId: string; address: string } >({ - mutationFn: async ({ tokenId, associatedAddress }) => { + mutationFn: async ({ tokenId, address }) => { const principal = nft?.fully_qualified_token_id?.split('::')!; const name = principal[1].split(':')[0]; const contractInfo: string[] = principal[0].split('.'); const unsginedTx: UnsignedStacksTransation = { amount: tokenId, senderAddress: stxAddress, - recipientAddress: associatedAddress, + recipientAddress: address, contractAddress: contractInfo[0], contractName: contractInfo[1], assetName: name, @@ -156,7 +141,7 @@ function SendNft() { unsignedTx.auth.spendingCondition.fee * BigInt(feeMultipliers.stxSendTxMultiplier), ); } - setRecipientAddress(associatedAddress); + setRecipientAddress(address); return unsignedTx; }, }); @@ -172,81 +157,115 @@ function SendNft() { } }, [data]); - useResetUserFlow('/send-nft'); - const handleBackButtonClick = () => { navigate(-1); }; - function validateFields(associatedAddress: string): boolean { - if (!associatedAddress) { - setError(t('ERRORS.ADDRESS_REQUIRED')); - return false; - } - - if (!validateStxAddress({ stxAddress: associatedAddress, network: network.type })) { - setError(t('ERRORS.ADDRESS_INVALID')); - return false; - } - - if (associatedAddress === stxAddress) { - setError(t('ERRORS.SEND_TO_SELF')); - return false; - } - - return true; - } - - const onPressSendNFT = async (associatedAddress: string) => { + const onPressNext = async () => { if (stxPendingTxData) { if (checkNftExists(stxPendingTxData?.pendingTransactions, nft!)) { - setError(t('ERRORS.NFT_SEND_DETAIL')); + setRecipientError({ variant: 'danger', message: t('ERRORS.NFT_SEND_DETAIL') }); return; } } - if (validateFields(associatedAddress.trim()) && nft) { - setError(''); + if (!isDangerFeedback(recipientError) && nft) { const tokenId = cvToHex(uintCV(nft?.token_id.toString()!)); - mutate({ tokenId, associatedAddress }); + mutate({ tokenId, address: associatedAddress || recipientAddress }); } }; + + const handleAddressChange = (e: React.ChangeEvent) => { + setRecipientAddress(e.target.value.trim()); + }; + + useEffect(() => { + const validateRecipientAddress = (address: string): boolean => { + if (!address) { + setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_REQUIRED') }); + return false; + } + if (!validateStxAddress({ stxAddress: address, network: network.type })) { + setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_INVALID') }); + return false; + } + if (address === stxAddress) { + setRecipientError({ variant: 'info', message: t('YOU_ARE_TRANSFERRING_TO_YOURSELF') }); + return true; + } + setRecipientError(null); + return true; + }; + if (associatedAddress) { + validateRecipientAddress(associatedAddress); + } else if (recipientAddress) { + validateRecipientAddress(recipientAddress); + } + }, [associatedAddress, recipientAddress, network.type, stxAddress, t]); + + const isNextEnabled = !isDangerFeedback(recipientError) && !!recipientAddress; + + // hide back button if there is no history + const hideBackButton = location.key === 'default'; + return ( - <> - {isGalleryOpen && ( - <> - - {!isLedgerAccount(selectedAccount) && ( - - - - )} - - )} - - {!isGalleryOpen && } - - - - - - {nft?.token_metadata?.name} - - - {!isGalleryOpen && } - - + + +
+ + {t('SEND_TO')} + + + + + + + + + + + {associatedAddress && ( + <> + + {t('ASSOCIATED_ADDRESS')} + + + {associatedAddress} + + + )} + {associatedBnsName && ( + <> + + {t('ASSOCIATED_BNS_DOMAIN')} + + + {associatedBnsName} + + + )} + + {recipientError && } + + +
+ + + +
+
); } diff --git a/src/app/screens/sendOrdinal/index.tsx b/src/app/screens/sendOrdinal/index.tsx index b75aa30e2..75a49e6b8 100644 --- a/src/app/screens/sendOrdinal/index.tsx +++ b/src/app/screens/sendOrdinal/index.tsx @@ -1,66 +1,37 @@ -import AccountHeaderComponent from '@components/accountHeader'; import ActionButton from '@components/button'; -import BottomBar from '@components/tabBar'; -import TopRow from '@components/topRow'; import useNftDataSelector from '@hooks/stores/useNftDataSelector'; import useBtcClient from '@hooks/useBtcClient'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSeedVault from '@hooks/useSeedVault'; import useWalletSelector from '@hooks/useWalletSelector'; -import { ArrowLeft } from '@phosphor-icons/react'; -import { isOrdinalOwnedByAccount } from '@secretkeylabs/xverse-core'; -import { getBtcFiatEquivalent } from '@secretkeylabs/xverse-core/currency'; +import { + ErrorCodes, + getBtcFiatEquivalent, + isOrdinalOwnedByAccount, + ResponseError, + UTXO, + validateBtcAddress, +} from '@secretkeylabs/xverse-core'; import { SignedBtcTx, signOrdinalSendTransaction, } from '@secretkeylabs/xverse-core/transactions/btc'; -import { ErrorCodes, ResponseError, UTXO } from '@secretkeylabs/xverse-core/types'; -import { validateBtcAddress } from '@secretkeylabs/xverse-core/wallet'; import { useMutation } from '@tanstack/react-query'; import Callout from '@ui-library/callout'; -import { StyledHeading, StyledP } from '@ui-library/common.styled'; +import { StyledHeading } from '@ui-library/common.styled'; import { InputFeedback, InputFeedbackProps, isDangerFeedback } from '@ui-library/inputFeedback'; -import { isLedgerAccount } from '@utils/helper'; import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import { devices } from 'theme'; - -const ScrollContainer = styled.div((props) => ({ - display: 'flex', - flex: 1, - flexDirection: 'column', - ...props.theme.scrollbar, -})); +import SendLayout from '../../layouts/sendLayout'; const Container = styled.div` display: flex; - flex: 1; flex-direction: column; - margin: auto; - margin-top: ${(props) => props.theme.space.xxs}; - padding: 0 ${(props) => props.theme.space.s}; justify-content: space-between; - max-width: 360px; - - @media only screen and ${devices.min.s} { - flex: initial; - max-width: 588px; - border: 1px solid ${(props) => props.theme.colors.elevation3}; - border-radius: ${(props) => props.theme.space.s}; - padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; - padding-bottom: ${(props) => props.theme.space.xxl}; - margin-top: ${(props) => props.theme.space.xxxxl}; - min-height: 600px; - } -`; - -const FooterContainer = styled.div` - display: flex; - justify-content: center; - margin-bottom: ${(props) => props.theme.space.xxl}; + flex-grow: 1; `; const StyledSendTo = styled(StyledHeading)` @@ -80,7 +51,7 @@ const InputGroup = styled.div` `; const Label = styled.label((props) => ({ - ...props.theme.body_medium_m, + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_200, display: 'flex', flex: 1, @@ -107,9 +78,9 @@ const InputFieldContainer = styled.div(() => ({ })); const InputField = styled.input((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, backgroundColor: 'transparent', - color: props.theme.colors.white['0'], + color: props.theme.colors.white_0, width: '100%', border: 'transparent', })); @@ -129,16 +100,6 @@ const StyledCallout = styled(Callout)` margin-bottom: ${(props) => props.theme.spacing(14)}px; `; -const BottomBarContainer = styled.div({ - marginTop: 'auto', -}); - -const Button = styled.button` - display: flex; - background-color: transparent; - margin-bottom: ${(props) => props.theme.space.l}; -`; - function SendOrdinal() { const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); const navigate = useNavigate(); @@ -154,8 +115,6 @@ function SendOrdinal() { useResetUserFlow('/send-ordinal'); - const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); - const { isLoading, data, @@ -258,63 +217,50 @@ function SendOrdinal() { }; const isNextEnabled = !isDangerFeedback(recipientError) && !!recipientAddress; - const year = new Date().getFullYear(); + + // hide back button if there is no history + const hideBackButton = location.key === 'default'; return ( - <> - {isGalleryOpen && ( - - )} - {!isGalleryOpen && } - - -
- {isGalleryOpen && !isLedgerAccount(selectedAccount) && ( - - )} - - {t('SEND_TO')} - - - - - - - - - - - - {recipientError && } - - - -
- - - -
- {isGalleryOpen && ( - - - {t('COPYRIGHT', { year })} - - - )} -
- {!isGalleryOpen && } - + + +
+ + {t('SEND_TO')} + + + + + + + + + + + + {recipientError && } + + + +
+ + + +
+
); } diff --git a/src/app/screens/settings/index.tsx b/src/app/screens/settings/index.tsx index 839193c2c..12677354f 100644 --- a/src/app/screens/settings/index.tsx +++ b/src/app/screens/settings/index.tsx @@ -11,7 +11,7 @@ import { ChangeActivateRareSatsAction, } from '@stores/wallet/actions/actionCreators'; import { PRIVACY_POLICY_LINK, SUPPORT_LINK, TERMS_LINK } from '@utils/constants'; -import { isLedgerAccount } from '@utils/helper'; +import { isInOptions, isLedgerAccount } from '@utils/helper'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -136,7 +136,7 @@ function Setting() { }; const onRestoreFundClick = async () => { - if (isLedgerAccount(selectedAccount)) { + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ url: chrome.runtime.getURL('options.html#/restore-funds'), }); diff --git a/src/app/screens/transactionStatus/index.tsx b/src/app/screens/transactionStatus/index.tsx index 000f9480c..1e397eba8 100644 --- a/src/app/screens/transactionStatus/index.tsx +++ b/src/app/screens/transactionStatus/index.tsx @@ -10,13 +10,12 @@ import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -const TxStatusContainer = styled.div({ - background: 'rgba(25, 25, 48, 0.74)', +const TxStatusContainer = styled.div((props) => ({ + background: props.theme.colors.elevation0, display: 'flex', flexDirection: 'column', height: '100%', - backdropFilter: 'blur(16px)', -}); +})); const Container = styled.div({ display: 'flex', @@ -84,14 +83,14 @@ const Image = styled.img({ }); const HeadingText = styled.h1((props) => ({ - ...props.theme.headline_s, + ...props.theme.typography.headline_s, color: props.theme.colors.white_0, textAlign: 'center', marginTop: props.theme.spacing(8), })); const BodyText = styled.h1((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, color: props.theme.colors.white_400, marginTop: props.theme.spacing(8), textAlign: 'center', @@ -110,19 +109,19 @@ const TxIDText = styled.h1((props) => ({ })); const BeforeButtonText = styled.h1((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, color: props.theme.colors.white_400, })); const IDText = styled.h1((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, color: props.theme.colors.white_0, marginTop: props.theme.spacing(2), wordBreak: 'break-all', })); const ButtonText = styled.h1((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, marginRight: props.theme.spacing(2), color: props.theme.colors.white_0, })); @@ -152,6 +151,7 @@ function TransactionStatus() { browserTx, isOrdinal, isNft, + isRareSat, errorTitle, isBrc20TokenFlow, isSponsorServiceError, @@ -186,9 +186,10 @@ function TransactionStatus() { const onCloseClick = () => { if (browserTx) window.close(); - else if (isOrdinal) navigate(-4); - else if (isNft) navigate(-3); - else navigate(-3); + else if (isRareSat) navigate('/nft-dashboard?tab=rareSats'); + else if (isOrdinal) navigate('/nft-dashboard?tab=inscriptions'); + else if (isNft) navigate('/nft-dashboard?tab=nfts'); + else navigate('/'); }; const handleClickTrySwapAgain = () => { diff --git a/src/app/stores/nftData/actions/actionCreator.ts b/src/app/stores/nftData/actions/actionCreator.ts index 6c98635e9..e87b88878 100644 --- a/src/app/stores/nftData/actions/actionCreator.ts +++ b/src/app/stores/nftData/actions/actionCreator.ts @@ -1,15 +1,6 @@ -/* eslint-disable import/prefer-default-export */ -import { NftData } from '@secretkeylabs/xverse-core/types/api/stacks/assets'; -import { Inscription } from '@secretkeylabs/xverse-core/types/api/ordinals'; +import { Inscription } from '@secretkeylabs/xverse-core'; import * as actions from './types'; -export function setNftDataAction(nftData: NftData[]): actions.SetNftData { - return { - type: actions.SetNftDataKey, - nftData, - }; -} - export function setSelectedOrdinalAction( selectedOrdinal: Inscription | null, ): actions.SetSelectedOrdinal { diff --git a/src/app/stores/nftData/actions/types.ts b/src/app/stores/nftData/actions/types.ts index 7c76f3b7f..69056dd72 100644 --- a/src/app/stores/nftData/actions/types.ts +++ b/src/app/stores/nftData/actions/types.ts @@ -1,26 +1,16 @@ -import { Inscription } from '@secretkeylabs/xverse-core/types/api/ordinals'; -import { NftData } from '@secretkeylabs/xverse-core/types/api/stacks/assets'; +import type { Inscription } from '@secretkeylabs/xverse-core'; import { Bundle } from '@utils/rareSats'; export interface NftDataState { - nftData: NftData[]; selectedOrdinal: Inscription | null; selectedSatBundle: Bundle | null; selectedSatBundleItemIndex: number | null; } -export const SetNftDataKey = 'SetNftData'; - export const SetSelectedOrdinalKey = 'SetSelectedOrdinal'; - export const SetSelectedSatBundleKey = 'SetSelectedSatBundle'; export const SetSelectedSatBundleItemIndexKey = 'SetSelectedSatBundleItemIndex'; -export interface SetNftData { - type: typeof SetNftDataKey; - nftData: NftData[]; -} - export interface SetSelectedOrdinal { type: typeof SetSelectedOrdinalKey; selectedOrdinal: Inscription | null; @@ -36,7 +26,6 @@ export interface SetSelectedSatBundleItemIndex { } export type NftDataAction = - | SetNftData | SetSelectedOrdinal | SetSelectedSatBundle | SetSelectedSatBundleItemIndex; diff --git a/src/app/stores/nftData/reducer.ts b/src/app/stores/nftData/reducer.ts index 8d4c4701c..4d37c0bb7 100644 --- a/src/app/stores/nftData/reducer.ts +++ b/src/app/stores/nftData/reducer.ts @@ -1,14 +1,12 @@ import { NftDataAction, NftDataState, - SetNftDataKey, SetSelectedOrdinalKey, SetSelectedSatBundleItemIndexKey, SetSelectedSatBundleKey, } from './actions/types'; const initialNftDataState: NftDataState = { - nftData: [], selectedOrdinal: null, selectedSatBundle: null, selectedSatBundleItemIndex: null, @@ -20,11 +18,6 @@ const NftDataStateReducer = ( action: NftDataAction, ): NftDataState => { switch (action.type) { - case SetNftDataKey: - return { - ...state, - nftData: action.nftData, - }; case SetSelectedOrdinalKey: return { ...state, diff --git a/src/app/ui-library/snackBar.tsx b/src/app/ui-library/snackBar.tsx new file mode 100644 index 000000000..d546d399a --- /dev/null +++ b/src/app/ui-library/snackBar.tsx @@ -0,0 +1,75 @@ +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +type ToastType = 'success' | 'error' | 'neutral'; + +interface ToastProps { + text: string; + type: ToastType; +} + +const getBackgroundColor = (type: ToastType, theme: any): string => { + const colors = { + success: theme.colors.feedback.success, + error: theme.colors.feedback.error, + neutral: theme.colors.feedback.neutral, + }; + return colors[type] || theme.colors.feedback.neutral; +}; + +const getTextColor = (type: ToastType, theme: any): string => { + const colors = { + success: theme.colors.elevation0, + error: theme.colors.white_0, + neutral: theme.colors.white_0, + }; + return colors[type] || theme.colors.elevation0; +}; + +const ToastContainer = styled.div<{ type: ToastType }>` + display: flex; + flex-direction: row; + background: ${(props) => getBackgroundColor(props.type, props.theme)}; + border-radius: 12px; + box-shadow: 0px 7px 16px -4px rgba(25, 25, 48, 0.25); + height: 44px; + padding: 12px 20px; + width: auto; + max-width: 306px; + align-items: center; + justify-content: space-between; + margin-bottom: 80px; +`; + +const ToastMessage = styled.h1<{ type: ToastType }>` + ${({ theme }) => theme.typography.body_medium_m}; + color: ${(props) => getTextColor(props.type, props.theme)}; + margin-right: 24px; +`; + +const ToastDismissButton = styled.h1<{ type: ToastType }>` + ${({ theme }) => theme.typography.body_medium_m}; + color: ${(props) => getTextColor(props.type, props.theme)}; + background: transparent; + cursor: pointer; +`; + +export function SnackBar({ text, type }: ToastProps) { + const { t } = useTranslation('translation'); + + const dismissToast = () => { + toast.dismiss(); + }; + + return ( + + {text} + + {t('OK')} + + + ); +} + +export default SnackBar; diff --git a/src/app/utils/nfts.ts b/src/app/utils/nfts.ts new file mode 100644 index 000000000..5ab03f46b --- /dev/null +++ b/src/app/utils/nfts.ts @@ -0,0 +1,36 @@ +import { getBnsNftName, NonFungibleToken, StacksCollectionData } from '@secretkeylabs/xverse-core'; + +export const getNftsTabGridItemSubText = (collection: StacksCollectionData) => + collection?.all_nfts?.length > 1 ? `${collection.all_nfts.length} Items` : '1 Item'; + +export const isBnsCollection = (collectionId?: string | null): boolean => + collectionId === 'SP000000000000000000002Q6VF78.bns'; + +// fully_qualified_token_id like: +// SP1E1RNN4JZ7T6Y0JVCSY2TH4918Z590P8JAB9HZM.radboy-first-feat::radboy-first-feat:64 +export const getFullyQualifiedKey = ({ + tokenId, + contractName, + contractAddress, +}: NonFungibleToken['identifier']) => + `${contractAddress}.${contractName}::${contractName}:${tokenId}`; + +export const getIdentifier = (fullyQualifiedKey: string): NonFungibleToken['identifier'] => { + const [principal, , contractName, tokenId] = fullyQualifiedKey.split(':'); + const [contractAddress] = principal.split('.'); + return { + tokenId, + contractName, + contractAddress, + }; +}; + +export const getNftCollectionsGridItemId = ( + nft: NonFungibleToken, + collectionData: StacksCollectionData, +) => + isBnsCollection(collectionData?.collection_id) + ? getBnsNftName(nft) + : nft?.identifier.tokenId + ? `${collectionData?.collection_name} #${nft?.identifier.tokenId}` + : `${collectionData?.collection_name}`; diff --git a/src/assets/img/nftDashboard/bns.svg b/src/assets/img/nftDashboard/bns.svg new file mode 100644 index 000000000..5d77a17ba --- /dev/null +++ b/src/assets/img/nftDashboard/bns.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/ic_nft_diamond.svg b/src/assets/img/nftDashboard/ic_nft_diamond.svg deleted file mode 100644 index b78cce2ce..000000000 --- a/src/assets/img/nftDashboard/ic_nft_diamond.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/img/nftDashboard/nft_user.svg b/src/assets/img/nftDashboard/nft_user.svg deleted file mode 100644 index 0101a3c43..000000000 --- a/src/assets/img/nftDashboard/nft_user.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/locales/en.json b/src/locales/en.json index 4f09fcf17..140f535f4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -539,7 +539,8 @@ "ERROR_RETRIEVING": "We are having trouble retrieving data.", "TRY_AGAIN": "Please try again later.", "LOAD_MORE": "Load more", - "TOTAL_ITEMS": "{{total}} items", + "TOTAL_ITEMS_one": "{{count}} item", + "TOTAL_ITEMS_other": "{{count}} items", "WEB_GALLERY": "Open gallery", "RECEIVE": "Receive", "SEND": "Send", @@ -630,7 +631,7 @@ "RARITY": "Overall rarity", "CONTRACT_ID": "Contract ID", "ATTRIBUTES": "Attributes", - "VIEW_CONTRACT": "View the contract on", + "VIEW_CONTRACT": "View on", "STACKS_EXPLORER": "Stacks Explorer", "DETAILS": "See detail on", "GAMMA": "Gamma.io", @@ -912,7 +913,7 @@ }, "COMPLETE": { "INSCRIBED": "Ordinal inscribed", - "MESSAGE": "Your ordinal have been successfully inscribed and should appear in a few minutes.", + "MESSAGE": "Your ordinal has been successfully inscribed and should appear in a few minutes.", "SEE_ON": "See on", "BITCOIN_EXPLORER": "mempool", "TRANSACTION_ID": "Transaction ID", @@ -1107,11 +1108,14 @@ "RARE_SAT": "Rare Sat", "INSCRIBED_SAT": "Inscribed Sat" }, - "ORDINALS_COLLECTION_SCREEN": { + "COLLECTIBLE_COLLECTION_SCREEN": { "BACK_TO_GALLERY": "Back to gallery", "COLLECTION_FLOOR_PRICE": "Collection floor price", "EST_PORTFOLIO_VALUE": "Est. portfolio value", "COLLECTION": "Collection", - "LOAD_MORE": "Load more" + "LOAD_MORE": "Load more", + "ERRORS": { + "FAILED_TO_FETCH": "Failed to fetch data" + } } } diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index 47da6c8e9..e779d1399 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -7,3 +7,4 @@ declare module '*.jpeg'; declare module '*.jpg'; declare module '*.otf'; declare module '*.ttf'; +declare module 'react-is-visible';