diff --git a/apps/mobile/src/components/home/home.tsx b/apps/mobile/src/components/home/home.tsx index 6a5341d9..03adc73d 100644 --- a/apps/mobile/src/components/home/home.tsx +++ b/apps/mobile/src/components/home/home.tsx @@ -1,4 +1,6 @@ import { AccountsWidget } from '@/components/widgets/accounts/accounts-widget'; +import { CollectiblesWidget } from '@/components/widgets/collectibles/collectibles-widget'; +import { mockCollectibles } from '@/components/widgets/collectibles/collectibles.mocks'; import { TokensWidget } from '@/components/widgets/tokens/tokens-widget'; import { getMockTokens, mockTotalBalance } from '@/components/widgets/tokens/tokens.mocks'; import { useAccounts } from '@/store/accounts/accounts.read'; @@ -16,6 +18,10 @@ export function Home() { + ); } diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles-header.tsx b/apps/mobile/src/components/widgets/collectibles/collectibles-header.tsx new file mode 100644 index 00000000..9d8b8af7 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles-header.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { t } from '@lingui/macro'; + +import { Box, ChevronRightIcon, Chip, SheetRef, Text } from '@leather.io/ui/native'; + +import { FiatBalance } from '../components/balance/fiat-balance'; + +interface CollectiblesHeaderProps { + collectibleCount: number; + sheetRef: React.RefObject; + totalBalance: string; +} + +function CollectiblesHeaderText() { + return {t`My collectibles`}; +} + +export function CollectiblesHeader({ collectibleCount, totalBalance }: CollectiblesHeaderProps) { + const hasCollectibles = collectibleCount > 0; + if (!hasCollectibles) return ; + return ( + + + + + + + + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles-widget.layout.tsx b/apps/mobile/src/components/widgets/collectibles/collectibles-widget.layout.tsx new file mode 100644 index 00000000..b454fdca --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles-widget.layout.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { ScrollView } from 'react-native-gesture-handler'; + +import { useTheme } from '@shopify/restyle'; + +import { Box, SheetRef, Theme } from '@leather.io/ui/native'; + +import { Widget } from '../widget'; + +interface CollectiblesWidgetProps { + balance?: React.ReactNode; + children: React.ReactNode; + header?: React.ReactNode; + sheetRef?: React.RefObject; +} + +export function CollectiblesWidgetLayout({ children, header }: CollectiblesWidgetProps) { + const theme = useTheme(); + return ( + + {header} + + {children} + + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles-widget.tsx b/apps/mobile/src/components/widgets/collectibles/collectibles-widget.tsx new file mode 100644 index 00000000..c0cc0586 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles-widget.tsx @@ -0,0 +1,35 @@ +import React, { useRef } from 'react'; + +import { SheetRef } from '@leather.io/ui/native'; + +import { TokenBalance } from '../components/balance/token-balance'; +import { CollectiblesHeader } from './collectibles-header'; +import { CollectiblesWidgetLayout } from './collectibles-widget.layout'; +import { type Collectible } from './collectibles.mocks'; +import { CollectiblesCard } from './components/collectible-card'; + +interface CollectiblesWidgetProps { + collectibles: Collectible[]; + totalBalance: string; +} + +export function CollectiblesWidget({ collectibles, totalBalance }: CollectiblesWidgetProps) { + const sheetRef = useRef(null); + + return ( + + } + balance={collectibles.length > 0 && } + > + {collectibles.map((collectible: Collectible, index: number) => ( + + ))} + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles.mocks.tsx b/apps/mobile/src/components/widgets/collectibles/collectibles.mocks.tsx new file mode 100644 index 00000000..769ba58f --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles.mocks.tsx @@ -0,0 +1,293 @@ +/* eslint-disable */ + +// provided by useTotalBalance hook in extension/src/app/common/hooks/balance/use-total-balance.tsx + +export const mockTotalBalance = { + totalUsdBalance: '$126.74', + totalBtcBalance: '0.00215005', + totalStxBalance: '0.0024', +}; + +export interface Ordinal { + id: string; + number: number; + output: string; + txid: string; + offset: string; + address: string; + preview: string; + title: string; + genesisBlockHeight: number; + genesisBlockHash: string; + genesisTimestamp: number; + value: string; + mimeType: string; + name: string; + src: string; +} + +const mockOrdinals: Ordinal[] = [ + { + id: 'a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949i0', + number: 74703951, + output: '0', + txid: 'a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949', + offset: '0', + address: 'bc1pwz9n62p9dhjpqcpdmfcrewdnz3nk8jcved242vd2lj9fgvtvwnwscvdyre', + preview: + 'https://ordinals.hiro.so/inscription/a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949i0', + title: 'Inscription 74703951', + genesisBlockHeight: 857719, + genesisBlockHash: '00000000000000000002bc6789fc6742da4958d003d3abff740687a863613a46', + genesisTimestamp: 1724219117, + value: '546', + mimeType: 'html', + name: 'inscription', + src: 'https://ordinals.com/preview/a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949i0', + }, + { + id: '335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062i0', + number: 73858867, + output: '0', + txid: '335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062', + offset: '0', + address: 'bc1pwz9n62p9dhjpqcpdmfcrewdnz3nk8jcved242vd2lj9fgvtvwnwscvdyre', + preview: + 'https://ordinals.hiro.so/inscription/335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062i0', + title: 'Inscription 73858867', + genesisBlockHeight: 855754, + genesisBlockHash: '000000000000000000021972c2000a8d347dbac1a2540112fadf81219b188796', + genesisTimestamp: 1723027746, + value: '546', + mimeType: 'text', + name: 'inscription', + src: 'https://bis-ord-content.fra1.cdn.digitaloceanspaces.com/ordinals/335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062i0', + }, + // ... (other ordinals) + // on leather dev wallet all seem to be mimeType: 'text' / 'html +]; + +export interface StacksNft { + token_uri: string; + metadata: { + sip: number; + name: string; + description: string; + image: string; + cached_image: string; + cached_thumbnail_image: string; + attributes?: Array<{ + trait_type: string; + value: string; + display_type?: string; + }>; + properties?: { + collection?: string; + collectionId?: string; + [key: string]: any; + }; + }; +} + +const mockStacksNfts: StacksNft[] = [ + { + token_uri: 'ipfs://ipfs/QmQ63rXC9F7GyLYoYNyqxeiYvbBUvmHmL36PrfYNxpw5sT/90.json', + metadata: { + sip: 16, + name: 'BlockSurvey #90', + description: 'Worlds First Software License as an NFT', + image: 'ipfs://QmZXkLMrN2ejpzGv1wk4HgcuL6XbyLVieW3Zm9wyAoDk18/90.png', + cached_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.blocksurvey/90.png', + cached_thumbnail_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.blocksurvey/90-thumb.png', + attributes: [ + { + trait_type: 'NightBackground', + value: 'MidnightMoss', + display_type: '', + }, + { + trait_type: 'NightLogo', + value: 'AtomicTangerine', + display_type: '', + }, + { + trait_type: 'SignatureWhite', + value: 'SignatureWhite', + display_type: '', + }, + ], + }, + }, + { + token_uri: 'ipfs://ipfs/QmWRQyaVxUjHGjBUoZqGcNjL37VN99jcFwmoB1wZnpjJEg/', + metadata: { + sip: 16, + name: 'Portals-ALEX-Anniversary-Series', + description: 'From proof-of-concept to bringing Bitcoin value to the multi-chain.\n', + image: 'ipfs://ipfs/QmUgHdbTy5LYi4wijf9YJgGs89SCAKNbXascvzuFgAsMB9', + cached_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP3N7Y3K01Y24G9JC1XXA13RQXXCY721WAVBMMD38.alex-anniversary-series/1452.png', + cached_thumbnail_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP3N7Y3K01Y24G9JC1XXA13RQXXCY721WAVBMMD38.alex-anniversary-series/1452-thumb.png', + properties: { + collection: 'ALEX Anniversary Series', + }, + }, + }, + { + token_uri: 'ipfs://QmYTX3u58v2Ero2drdtqhL6rPE5qnv51EJZ6WSu3LKqUBN/crashpunks-5559.json', + metadata: { + sip: 16, + name: 'Crash Punk 5559', + description: '', + image: 'ipfs://Qmb84UcaMr1MUwNbYBnXWHM3kEaDcYrKuPWwyRLVTNKELC/5559.png', + cached_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP3QSAJQ4EA8WXEDSRRKMZZ29NH91VZ6C5X88FGZQ.crashpunks-v2/5559.png', + cached_thumbnail_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP3QSAJQ4EA8WXEDSRRKMZZ29NH91VZ6C5X88FGZQ.crashpunks-v2/5559-thumb.png', + attributes: [ + { + trait_type: 'Background', + value: 'Blue', + display_type: 'string', + }, + { + trait_type: 'Outfit Back', + value: 'Stacks Hoodie Back', + display_type: 'string', + }, + { + trait_type: 'Neck', + value: 'Neck Metal', + display_type: 'string', + }, + { + trait_type: 'Outfit Front', + value: 'Stacks Hoodie', + display_type: 'string', + }, + { + trait_type: 'Head', + value: 'Head Tan', + display_type: 'string', + }, + { + trait_type: 'Piercings', + value: 'Piercings', + display_type: 'string', + }, + { + trait_type: 'Mouth', + value: 'Lips Bare', + display_type: 'string', + }, + { + trait_type: 'Eyes', + value: 'RoboEyes Blue', + display_type: 'string', + }, + { + trait_type: 'Hair', + value: 'Bob Silver', + display_type: 'string', + }, + ], + properties: { + collection: 'Crash Punks', + collectionId: 'grace.btc/crash_punks', + dna: '5c2f54662bb494b5e4ebc195070d9ce624c5a849', + total_supply: '9216', + external_url: + 'https://thisisnumberone.com/nfts/SP3QSAJQ4EA8WXEDSRRKMZZ29NH91VZ6C5X88FGZQ.crashpunks-v2/5559', + }, + }, + }, + { + token_uri: 'ipfs://ipfs/QmZYoSr94MKdarScJZSsyBYxBgMJchUQqqbtLxxxR86wZN/', + metadata: { + sip: 16, + name: 'WORRY - NFT - MUSIC', + description: + 'Musical NFT Collection \nWorry is a self-reflective song done by Brythreesixty also known as 3hunnatheartist. Worry is an emotional state of being anxious and troubled over actual or potential problems. The greatest weapon is positivity. Welcome to my Bullish state of Mind. This collection is a gift to the community. Enjoy \n\nhttps://gamma.io/3hunnatheartist.btc\nhttps://gamma.io/brythreesixty\n\nhttps://twitter.com/brythreesixty\nhttps://twitter.com/3hunnatheartist\n\nhttps://discord.gg/hRqeVRFG', + image: '', + cached_image: '', + cached_thumbnail_image: '', + }, + }, + { + token_uri: 'ipfs://QmbMdASbHZb5XHizZJsFPL9hdmuDgekUHH9Ya1DnuSxfHj/1547.json', + metadata: { + sip: 16, + name: 'StacksMFers #1547', + description: 'Just a bunch of mfers on stacks', + image: 'ipfs://QmUL7yELAmF1wnbqt6yaNLmCVbBa7BSbSNXYKijpku2r45/1547.png', + cached_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP2N3BAG4GBF8NHRPH6AY4YYH1SP6NK5TGCY7RDFA.stacks-mfers/1547.png', + cached_thumbnail_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP2N3BAG4GBF8NHRPH6AY4YYH1SP6NK5TGCY7RDFA.stacks-mfers/1547-thumb.png', + attributes: [ + { + trait_type: 'BG', + value: 'Pixels', + display_type: '', + }, + { + trait_type: 'Type', + value: 'Plain', + display_type: '', + }, + { + trait_type: 'Eyes', + value: 'Greenglasses', + display_type: '', + }, + { + trait_type: 'Mouth', + value: 'Smile', + display_type: '', + }, + { + trait_type: 'Beard', + value: 'None', + display_type: '', + }, + { + trait_type: 'Shirt', + value: 'GreenHoodie', + display_type: '', + }, + { + trait_type: 'Accessory', + value: 'None', + display_type: '', + }, + { + trait_type: 'LongHair', + value: 'LongBlue', + display_type: '', + }, + { + trait_type: 'Hat under', + value: 'RedBandana', + display_type: '', + }, + { + trait_type: 'Headphones', + value: 'Red', + display_type: '', + }, + { + trait_type: 'Smoke', + value: 'None', + display_type: '', + }, + ], + }, + }, +]; + +export type Collectible = Ordinal | StacksNft; + +export const mockCollectibles: (Ordinal | StacksNft)[] = [...mockOrdinals, ...mockStacksNfts]; diff --git a/apps/mobile/src/components/widgets/collectibles/components/collectible-card-layout.tsx b/apps/mobile/src/components/widgets/collectibles/components/collectible-card-layout.tsx new file mode 100644 index 00000000..b620d95c --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/components/collectible-card-layout.tsx @@ -0,0 +1,15 @@ +import { BaseTheme, BoxProps } from '@shopify/restyle'; + +import { Box, Theme } from '@leather.io/ui/native'; + +type CollectiblesCardLayoutProps = BoxProps & { + children: React.ReactNode; +}; + +export function CollectiblesCardLayout({ children, ...props }: CollectiblesCardLayoutProps) { + return ( + + {children} + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/components/collectible-card.tsx b/apps/mobile/src/components/widgets/collectibles/components/collectible-card.tsx new file mode 100644 index 00000000..04ff622a --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/components/collectible-card.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { t } from '@lingui/macro'; + +import { type Collectible } from '../collectibles.mocks'; +import { CollectibleHtml } from './collectible-html'; +import { CollectibleImage } from './collectible-image'; +import { CollectibleText } from './collectible-text'; + +interface CollectiblesCardProps { + collectible: Collectible; +} + +export function CollectiblesCard({ collectible }: CollectiblesCardProps) { + const isOrdinal = 'name' in collectible; + + const imageSource = isOrdinal ? collectible.src : collectible.metadata.cached_thumbnail_image; + + if (isOrdinal) { + switch (collectible.mimeType) { + case 'text': + return ; + case 'html': + return ; + // Add more cases for other mimeTypes as needed + } + } + + return ( + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/components/collectible-html.tsx b/apps/mobile/src/components/widgets/collectibles/components/collectible-html.tsx new file mode 100644 index 00000000..27c7f3b5 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/components/collectible-html.tsx @@ -0,0 +1,15 @@ +import { WebView } from 'react-native-webview'; + +import { CollectiblesCardLayout } from './collectible-card-layout'; + +interface CollectibleHtmlProps { + src: string; +} + +export function CollectibleHtml({ src }: CollectibleHtmlProps) { + return ( + + + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/components/collectible-image.tsx b/apps/mobile/src/components/widgets/collectibles/components/collectible-image.tsx new file mode 100644 index 00000000..d74e18be --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/components/collectible-image.tsx @@ -0,0 +1,15 @@ +import { Image } from 'react-native'; + +import { Box } from '@leather.io/ui/native'; + +interface CollectibleImageProps { + alt: string; + source: string; +} +export function CollectibleImage({ alt, source }: CollectibleImageProps) { + return ( + + {alt} + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/components/collectible-text.tsx b/apps/mobile/src/components/widgets/collectibles/components/collectible-text.tsx new file mode 100644 index 00000000..a0239ac8 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/components/collectible-text.tsx @@ -0,0 +1,45 @@ +// FIXME: Lifted from extension +// // TODO: migrate `collectible-type` components to monorepo +// FIXME when using real data sanitization is needed +// import { sanitize } from 'dompurify'; +import { useEffect, useState } from 'react'; + +import { Text } from '@leather.io/ui/native'; + +import { CollectiblesCardLayout } from './collectible-card-layout'; + +interface CollectibleTextProps { + src: string; +} + +export function CollectibleText({ src }: CollectibleTextProps) { + const [content, setContent] = useState(null); + + useEffect(() => { + const fetchContent = async () => { + try { + const response = await fetch(src); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + setContent(JSON.stringify(data, null, 2)); + } catch (err) { + // eslint-disable-next-line no-console, lingui/no-unlocalized-strings + console.log('Error fetching content:', err); + } + }; + + fetchContent(); + }, [src]); + + return ( + + + {/* FIXME when using real data sanitization is needed */} + {/* {sanitize(children)} */} + {content} + + + ); +} diff --git a/apps/mobile/src/locales/en/messages.po b/apps/mobile/src/locales/en/messages.po index 2b38aad8..631b18ae 100644 --- a/apps/mobile/src/locales/en/messages.po +++ b/apps/mobile/src/locales/en/messages.po @@ -172,7 +172,7 @@ msgstr "Avatar" #: src/components/create-new-wallet/mnemonic-display.tsx:13 #: src/components/recover-wallet/recover-wallet-sheet.tsx:28 msgid "BIP39 passphrase" -msgstr "BIP39 passphrase<<<<<<< HEAD" +msgstr "BIP39 passphrase" #: src/components/widgets/tokens/tokens.mocks.tsx:50 msgid "Bitcoin" @@ -180,11 +180,11 @@ msgstr "Bitcoin" #: src/components/widgets/tokens/tokens.mocks.tsx:27 msgid "Bitcoin blockchain" -msgstr "Bitcoin blockchain>>>>>>> bbb518c (feat: add tokens widget, ref leather-io/issues#221)" +msgstr "Bitcoin blockchain" #: src/app/wallet/developer-console/index.tsx:90 msgid "Bitcoin Scrach Pad" -msgstr "Bitcoin Scrach Pad=======" +msgstr "Bitcoin Scrach Pad" #: src/app/wallet/settings/display/index.tsx:44 #: src/features/settings/bitcoin-unit-sheet/bitcoin-unit-sheet.tsx:29 @@ -224,6 +224,10 @@ msgstr "Choose an image" msgid "Clear" msgstr "Clear" +#: src/components/widgets/collectibles/components/collectible-card.tsx:32 +msgid "Collectible" +msgstr "Collectible" + #: src/app/wallet/(tabs)/_layout.tsx:79 #: src/app/wallet/(tabs)/_layout.tsx:120 msgid "Collectibles" @@ -490,7 +494,11 @@ msgstr "Must use physical device for Push Notifications" #: src/components/widgets/accounts/accounts-header.tsx:14 msgid "My accounts" -msgstr "My accounts<<<<<<< HEAD" +msgstr "My account" + +#: src/components/widgets/collectibles/collectibles-header.tsx:16 +msgid "My collectibles" +msgstr "My collectibles" #: src/components/widgets/tokens/tokens-header.tsx:18 msgid "My tokens" diff --git a/apps/mobile/src/locales/pseudo-locale/messages.po b/apps/mobile/src/locales/pseudo-locale/messages.po index 08a40a2f..9274dbd9 100644 --- a/apps/mobile/src/locales/pseudo-locale/messages.po +++ b/apps/mobile/src/locales/pseudo-locale/messages.po @@ -172,7 +172,7 @@ msgstr "" #: src/components/create-new-wallet/mnemonic-display.tsx:13 #: src/components/recover-wallet/recover-wallet-sheet.tsx:28 msgid "BIP39 passphrase" -msgstr "<<<<<<< HEAD" +msgstr "" #: src/components/widgets/tokens/tokens.mocks.tsx:50 msgid "Bitcoin" @@ -224,6 +224,10 @@ msgstr "" msgid "Clear" msgstr "" +#: src/components/widgets/collectibles/components/collectible-card.tsx:32 +msgid "Collectible" +msgstr "" + #: src/app/wallet/(tabs)/_layout.tsx:79 #: src/app/wallet/(tabs)/_layout.tsx:120 msgid "Collectibles" @@ -490,7 +494,11 @@ msgstr "" #: src/components/widgets/accounts/accounts-header.tsx:14 msgid "My accounts" -msgstr "<<<<<<< HEAD" +msgstr "" + +#: src/components/widgets/collectibles/collectibles-header.tsx:16 +msgid "My collectibles" +msgstr "" #: src/components/widgets/tokens/tokens-header.tsx:18 msgid "My tokens" diff --git a/packages/ui/native.ts b/packages/ui/native.ts index 06696ec4..4bd3942c 100644 --- a/packages/ui/native.ts +++ b/packages/ui/native.ts @@ -3,7 +3,7 @@ export { Avatar } from './src/components/avatar/avatar.native'; export { BulletSeparator } from './src/components/bullet-separator/bullet-separator.native'; export { BtcAvatarIcon } from './src/components/avatar/btc-avatar-icon.native'; export { StxAvatarIcon } from './src/components/avatar/stx-avatar-icon.native'; -export { Box } from './src/components/box/box.native'; +export { Box, type BoxProps } from './src/components/box/box.native'; export { BlurView } from './src/components/blur-view/blur-view.native'; export { Chip } from './src/components/chip/chip.native'; export { HStack } from './src/components/box/hstack.native'; diff --git a/packages/ui/src/components/box/box.native.tsx b/packages/ui/src/components/box/box.native.tsx index bfa5e734..7ac8e9a2 100644 --- a/packages/ui/src/components/box/box.native.tsx +++ b/packages/ui/src/components/box/box.native.tsx @@ -2,4 +2,6 @@ import { createBox } from '@shopify/restyle'; import { type Theme } from '../../theme-native'; +export type { BoxProps } from '@shopify/restyle'; + export const Box = createBox();