diff --git a/package.json b/package.json index 6ee7f72..87a3e00 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "*.css" ], "dependencies": { - "@hot-wallet/sdk": "^1.0.11" + "@hot-wallet/sdk": "^1.0.11", + "qrcode.react": "^4.2.0" } } diff --git a/src/assets/Icons.tsx b/src/assets/Icons.tsx index 5276c99..b64e4df 100644 --- a/src/assets/Icons.tsx +++ b/src/assets/Icons.tsx @@ -215,30 +215,57 @@ export const LogOut = ({ fill = '#666666' }: { fill?: string }) => ( ); export const Copy = ({ fill = '#4D4D4D' }: { fill?: string }) => ( + + + + + + + +); +export const LargeCopy = ({ fill = '#0C1083' }: { fill?: string }) => ( + - + - + ); + export const History = ({ fill = '#0C1083' }: { fill?: string }) => ( ( ); + +export const ArrowDropDown = ({ fill = '#0C1083' }: { fill?: string }) => ( + + + + + + + + +); + export const RedAlert = () => ( ( ); +export const SwapIcon = ({ fill = '#0C1083' }: { fill?: string }) => ( + + + + + + + + +); +export const ReceiveIcon = ({ fill = '#0C1083' }: { fill?: string }) => ( + + + + + + + + +); +export const BalancesIcon = ({ fill = '#0C1083' }: { fill?: string }) => ( + + + + + + + + +); + +export const TokenIcon = ({ fill = '#0C1083' }: { fill?: string }) => ( + + + + + + + + +); diff --git a/src/assets/bluxLogo.tsx b/src/assets/bluxLogo.tsx index 6d00b72..c219006 100644 --- a/src/assets/bluxLogo.tsx +++ b/src/assets/bluxLogo.tsx @@ -71,3 +71,28 @@ const BluxLogo = ({ fill = 'black' }: { fill?: string }) => ( ); export default BluxLogo; + +export const SmallBlux = ({ + fill = 'black', + background = 'transparent', +}: { + fill?: string; + background?: string; +}) => ( + + + + + + + +); diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 697cdca..b90eae4 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { useProvider } from '../../context/provider'; import getContrastColor from '../../utils/getContrastColor'; +import hexToRgba from '../../utils/hexToRgba'; type ButtonSize = 'small' | 'medium' | 'large'; -type ButtonVariant = 'outline' | 'text' | 'fill'; +type ButtonVariant = 'outline' | 'text' | 'fill' | 'tonal'; type ButtonState = 'enabled' | 'disabled' | 'selected'; interface ButtonProps { @@ -68,6 +69,11 @@ const Button = ({ color: appearance.accent, backgroundColor: 'transparent', }); + } else if (variant === 'tonal') { + Object.assign(baseStyle, { + color: appearance.accent, + backgroundColor: hexToRgba(appearance.accent, 0.1), + }); } if (state === 'selected') { diff --git a/src/components/CardItem/index.tsx b/src/components/CardItem/index.tsx index 3797bfd..328c1a9 100644 --- a/src/components/CardItem/index.tsx +++ b/src/components/CardItem/index.tsx @@ -6,6 +6,7 @@ import { useLang } from '../../hooks/useLang'; type CardItemProps = { variant?: 'social' | 'default' | 'input'; + size?: 'small' | 'medium'; startIcon: React.ReactNode; endArrow?: boolean; isRecent?: boolean; @@ -19,6 +20,7 @@ type CardItemProps = { const CardItem = ({ variant = 'default', + size = 'medium', startIcon, endArrow, isRecent, @@ -72,8 +74,12 @@ const CardItem = ({ return (
{startIcon} -
+
{variant === 'input' ? ( <> ) : ( - {label} + + {label} + )}
{isRecent && ( @@ -157,7 +173,7 @@ const CardItem = ({
)} - {endArrow && ( + {endArrow && size === 'medium' && ( diff --git a/src/components/QRCode/index.tsx b/src/components/QRCode/index.tsx new file mode 100644 index 0000000..b387725 --- /dev/null +++ b/src/components/QRCode/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { QRCodeCanvas } from 'qrcode.react'; + +interface QRCodeCanvasProps { + value: string; + title?: string; + size?: number; + bgColor?: string; + fgColor?: string; + level?: 'L' | 'M' | 'Q' | 'H'; +} + +const QRCode = ({ + value, + title = '', + size = 184, + bgColor = '#ffffff', + fgColor = '#00020f', + level = 'Q', + ...rest +}: QRCodeCanvasProps) => { + return ( +
+ +
+ ); +}; + +export default QRCode; diff --git a/src/components/TabBox/index.tsx b/src/components/TabBox/index.tsx new file mode 100644 index 0000000..b2f82f4 --- /dev/null +++ b/src/components/TabBox/index.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import hexToRgba from '../../utils/hexToRgba'; +import { useProvider } from '../../context/provider'; + +type Tab = { + label: string; + icon: React.JSX.Element; + content: React.ReactNode; +}; + +type TabsProps = { + tabs: Tab[]; +}; +const TabBox = ({ tabs }: TabsProps) => { + const context = useProvider(); + const { appearance } = context.value.config; + const [activeTab, setActiveTab] = useState(0); + + return ( + <> +
+ {tabs.map((tab, index) => { + const isActive = activeTab === index; + + return ( +
setActiveTab(index)} + role="tab" + aria-label={tab.label} + aria-selected={activeTab === index} + tabIndex={activeTab === index ? 0 : -1} + className="bluxcc:flex bluxcc:h-20 bluxcc:max-w-[96px] bluxcc:cursor-pointer bluxcc:flex-col bluxcc:items-center bluxcc:justify-center bluxcc:gap-2 bluxcc:px-7 bluxcc:py-4 bluxcc:text-sm bluxcc:font-medium bluxcc:transition-all bluxcc:duration-300" + style={{ + background: isActive + ? hexToRgba(appearance.accent, 0.1) + : appearance.background, + color: isActive ? appearance.accent : appearance.textColor, + borderRadius: appearance.borderRadius, + }} + > +
{tab.icon}
+
{tab.label}
+
+ ); + })} +
+ +
+ {tabs[activeTab]?.content} +
+ + ); +}; +export default TabBox; diff --git a/src/constants/index.ts b/src/constants/index.ts index 89d0f09..0df8497 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -9,7 +9,7 @@ export const defaultLightTheme: IAppearance = { accent: '#0c1083', borderWidth: '1px', bgField: '#ffffff', - borderRadius: '24px', + borderRadius: '16px', textColor: '#000000', background: '#ffffff', includeBorders: true, @@ -23,7 +23,7 @@ export const defaultDarkTheme: IAppearance = { accent: '#ffffff', borderWidth: '1px', bgField: '#1a1a1a', - borderRadius: '24px', + borderRadius: '16px', textColor: '#ffffff', background: '#000000', includeBorders: true, diff --git a/src/constants/locales.ts b/src/constants/locales.ts index b3c6dfe..4983dd6 100644 --- a/src/constants/locales.ts +++ b/src/constants/locales.ts @@ -38,6 +38,10 @@ const translations: Translations = { en: 'Receive', es: 'Recibir', }, + balances: { + en: 'Balances', + es: 'Saldos', + }, wrongNetwork: { en: 'Wrong Network', es: 'Red incorrecta', diff --git a/src/containers/BluxModal/content.tsx b/src/containers/BluxModal/content.tsx index 0d98a1b..e6a0ee9 100644 --- a/src/containers/BluxModal/content.tsx +++ b/src/containers/BluxModal/content.tsx @@ -1,5 +1,8 @@ -import { Routes } from '../../types'; +import React from 'react'; + +import Swap from '../Pages/Swap'; import Send from '../Pages/Send'; +import Receive from '../Pages/Receive'; import Profile from '../Pages/Profile'; import Waiting from '../Pages/Waiting'; import Activity from '../Pages/Activity'; @@ -8,9 +11,11 @@ import OnBoarding from '../Pages/OnBoarding'; import ConfirmCode from '../Pages/ConfirmCode'; import WrongNetwork from '../Pages/WrongNetwork'; import SignTransaction from '../Pages/SignTransaction'; + +import { Routes } from '../../types'; import { LanguageKey } from '../../constants/locales'; -import React from 'react'; import { translate } from '../../utils/translate'; +import Balances from '../Pages/Balances'; type RouteContent = { title: string; @@ -53,6 +58,18 @@ export const getModalContent = ( title: '', Component: , }, + [Routes.RECEIVE]: { + title: 'Receive address', + Component: , + }, + [Routes.SWAP]: { + title: 'Swap', + Component: , + }, + [Routes.BALANCES]: { + title: 'Balances', + Component: , + }, [Routes.WRONG_NETWORK]: { isSticky: true, title: translate('wrongNetwork', lang), diff --git a/src/containers/BluxModal/index.tsx b/src/containers/BluxModal/index.tsx index 95af3a0..600b31b 100644 --- a/src/containers/BluxModal/index.tsx +++ b/src/containers/BluxModal/index.tsx @@ -21,7 +21,10 @@ export default function BluxModal({ isOpen, closeModal }: BluxModalProps) { (route === Routes.ONBOARDING && value.showAllWallets) || route === Routes.ACTIVITY || route === Routes.SEND || - route === Routes.OTP; + route === Routes.OTP || + route === Routes.BALANCES || + route === Routes.RECEIVE || + route === Routes.SWAP; let modalIcon: 'back' | 'info' | undefined; @@ -39,7 +42,13 @@ export default function BluxModal({ isOpen, closeModal }: BluxModalProps) { setRoute(Routes.ONBOARDING); } else if (value.showAllWallets) { setValue((prev) => ({ ...prev, showAllWallets: false })); - } else if (route === Routes.SEND || route === Routes.ACTIVITY) { + } else if ( + route === Routes.SEND || + route === Routes.ACTIVITY || + route === Routes.BALANCES || + route === Routes.RECEIVE || + route === Routes.SWAP + ) { setRoute(Routes.PROFILE); } }; diff --git a/src/containers/Pages/Balances/Assets/index.tsx b/src/containers/Pages/Balances/Assets/index.tsx new file mode 100644 index 0000000..f62c562 --- /dev/null +++ b/src/containers/Pages/Balances/Assets/index.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import humanizeAmount from '../../../../utils/humanizeAmount'; +import { IAsset } from '../../../../types'; +import { useLang } from '../../../../hooks/useLang'; +import { useProvider } from '../../../../context/provider'; + +type AssetsProps = { + assets: IAsset[]; +}; + +const Assets = ({ assets }: AssetsProps) => { + const [hoveredIndex, setHoveredIndex] = useState(null); + const t = useLang(); + const context = useProvider(); + const { appearance } = context.value.config; + + return ( +
+ {assets.map((asset, index) => ( +
setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + className="bluxcc:flex bluxcc:cursor-pointer bluxcc:items-center bluxcc:justify-between bluxcc:px-4 bluxcc:py-3" + style={{ + background: + hoveredIndex === index ? appearance.bgField : 'transparent', + color: appearance.textColor, + borderBottomStyle: 'dashed', + borderBottomWidth: + index < assets.length - 1 + ? appearance.includeBorders + ? appearance.borderWidth + : '1px' + : '0px', + borderBottomColor: appearance.borderColor, + transition: 'all 0.2s ease-in-out', + }} + > +
+ {asset.logo} +
+ + {asset.assetCode} + + {asset.assetCode} +
+
+ + + {humanizeAmount(asset.balance)} + +
+ ))} + + {assets.length === 0 && ( +
+ {t('noAssetsFound')} +
+ )} +
+ ); +}; + +export default Assets; diff --git a/src/containers/Pages/Balances/index.tsx b/src/containers/Pages/Balances/index.tsx new file mode 100644 index 0000000..d85f3dd --- /dev/null +++ b/src/containers/Pages/Balances/index.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { TokenIcon } from '../../../assets/Icons'; +import TabBox from '../../../components/TabBox'; +import Assets from './Assets'; +import { IAsset } from '../../../types'; + +const Balances = () => { + const mockAssets: IAsset[] = [ + { + assetCode: 'XLM', + assetIssuer: 'Stellar Foundation', + assetType: 'native', + balance: '1000.1234', + logo: '🌟', + }, + { + assetCode: 'USDC', + assetIssuer: 'Centre Consortium', + assetType: 'credit_alphanum4', + balance: '500.5', + logo: '💵', + }, + ]; + + const tabsContent = [ + { + label: 'Assets', + icon: , + content: , + }, + { + label: 'Tokens', + icon: , + + content: 'token', + }, + + { + label: 'NFTs', + icon: , + + content: 'nfts', + }, + ]; + + return ; +}; + +export default Balances; diff --git a/src/containers/Pages/Profile/index.tsx b/src/containers/Pages/Profile/index.tsx index ed61ebf..ec784fd 100644 --- a/src/containers/Pages/Profile/index.tsx +++ b/src/containers/Pages/Profile/index.tsx @@ -3,15 +3,24 @@ import React, { useState } from 'react'; import { Routes } from '../../../types'; import copyText from '../../../utils/copyText'; import { useLang } from '../../../hooks/useLang'; -import { useBlux } from '../../../hooks/useBlux'; +// import { useBlux } from '../../../hooks/useBlux'; import CardItem from '../../../components/CardItem'; import { useProvider } from '../../../context/provider'; import shortenAddress from '../../../utils/shortenAddress'; import humanizeAmount from '../../../utils/humanizeAmount'; -import { Copy, History, LogOut, Send } from '../../../assets/Icons'; +import { + BalancesIcon, + Copy, + History, + // LogOut, + ReceiveIcon, + Send, + SwapIcon, +} from '../../../assets/Icons'; +import hexToRgba from '../../../utils/hexToRgba'; const Profile = () => { - const { logout } = useBlux(); + // const { logout } = useBlux(); const t = useLang(); const context = useProvider(); const [copied, setCopied] = useState(false); @@ -19,9 +28,9 @@ const Profile = () => { const appearance = context.value.config.appearance; const address = context.value.user.wallet?.address as string; - const handleLogout = () => { - logout(); - }; + // const handleLogout = () => { + // logout(); + // }; const handleCopyAddress = () => { copyText(address) @@ -30,7 +39,7 @@ const Profile = () => { setTimeout(() => { setCopied(false); - }, 1000); + }, 2000); }) .catch(() => {}); }; @@ -43,40 +52,68 @@ const Profile = () => { return (
-
-

- {copied ? ( - t('copied') - ) : ( - - {address ? shortenAddress(address, 5) : ''} - - - )} -

-

- {balance ? `${humanizeAmount(balance)} XLM` : t('loading')} -

+
+
+

+ {balance ? `${humanizeAmount(balance)} XLM` : t('loading')} +

+

+ {copied ? ( + t('copied') + ) : ( + + {address ? shortenAddress(address, 5) : ''} + + + )} +

+
-
+
} onClick={() => { context.setRoute(Routes.SEND); }} /> + } + onClick={() => { + context.setRoute(Routes.RECEIVE); + }} + /> + } + onClick={() => { + context.setRoute(Routes.SWAP); + }} + /> +
+
+ } + onClick={() => { + context.setRoute(Routes.BALANCES); + }} + /> { />
-
+ {/*
{ > {t('logout')} -
+
*/}
); }; diff --git a/src/containers/Pages/Receive/index.tsx b/src/containers/Pages/Receive/index.tsx new file mode 100644 index 0000000..84152b2 --- /dev/null +++ b/src/containers/Pages/Receive/index.tsx @@ -0,0 +1,104 @@ +import React from 'react'; + +import Button from '../../../components/Button'; +import { useProvider } from '../../../context/provider'; +import { LargeCopy } from '../../../assets/Icons'; +import QRCode from '../../../components/QRCode'; +import { SmallBlux } from '../../../assets/bluxLogo'; +import hexToRgba from '../../../utils/hexToRgba'; + +const Receive = () => { + const context = useProvider(); + const appearance = context.value.config.appearance; + const address = context.value.user.wallet?.address as string; + + return ( +
+
+ +
+ +
+
+ +
+

Your Address

+
+
+ {address} +
+
+
+ + {/* divider */} +
+
+
+ + +
+ ); +}; + +export default Receive; diff --git a/src/containers/Pages/Swap/AssetBox/index.tsx b/src/containers/Pages/Swap/AssetBox/index.tsx new file mode 100644 index 0000000..385b773 --- /dev/null +++ b/src/containers/Pages/Swap/AssetBox/index.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import getContrastColor from '../../../../utils/getContrastColor'; +import { StellarLogo } from '../../../../assets/logos'; +import { ArrowDropDown } from '../../../../assets/Icons'; +import { useProvider } from '../../../../context/provider'; + +const AssetBox = ({ handleOpenAssets }: { handleOpenAssets: () => void }) => { + const context = useProvider(); + const appearance = context.value.config.appearance; + + return ( +
+
+ +
+ XLM + +
+ ); +}; + +export default AssetBox; diff --git a/src/containers/Pages/Swap/index.tsx b/src/containers/Pages/Swap/index.tsx new file mode 100644 index 0000000..698f283 --- /dev/null +++ b/src/containers/Pages/Swap/index.tsx @@ -0,0 +1,202 @@ +import React, { useState } from 'react'; +import { useProvider } from '../../../context/provider'; +import Button from '../../../components/Button'; +import hexToRgba from '../../../utils/hexToRgba'; +import { ArrowDropUp, SwapIcon } from '../../../assets/Icons'; + +import { useLang } from '../../../hooks/useLang'; +import { IAsset } from '../../../types'; +import SelectAssets from '../SelectAsset'; +import AssetBox from './AssetBox'; + +const mockAssets: IAsset[] = [ + { + assetCode: 'XLM', + assetIssuer: 'Stellar Foundation', + assetType: 'native', + balance: '1000.1234', + logo: '🌟', + }, + { + assetCode: 'USDC', + assetIssuer: 'Centre Consortium', + assetType: 'credit_alphanum4', + balance: '500.5', + logo: '💵', + }, +]; +const Swap = () => { + const [showSelectAssetPage, setShowSelectAssetPage] = useState(false); + const [selectedAsset, setSelectedAsset] = useState(mockAssets[0]); + + const context = useProvider(); + const appearance = context.value.config.appearance; + const t = useLang(); + + const handleOpenAssets = () => { + setShowSelectAssetPage(true); + }; + if (showSelectAssetPage) { + return ( + + ); + } + return ( +
+
+
+ + From + + + 345.00{' '} + + {t('max')} + + +
+
+ + +
+
+ ≈ $23.74 USD +
+ {/* Swap Icon */} +
+
+ +
+ +
+
+ {/* To Input */} + +
+ + To + +
+
+ + +
+
+ + {/* Price Impact */} +
+ Price Impact +
+ %0.2 + +
+
+
+ The estimated effect of your swap on the market price.{' '} + + learn more + +
+ + {/* divider */} +
+
+
+ + {/* Swap Button */} + +
+ ); +}; + +export default Swap; diff --git a/src/types/index.ts b/src/types/index.ts index c3e802e..c70c563 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -173,6 +173,9 @@ export enum Routes { SEND = 'SEND', // User sign transaction view ACTIVITY = 'ACTIVITY', // User sign transaction view OTP = 'OTP', // User Login with Phone ot email + RECEIVE = 'RECEIVE', // View for receive page + BALANCES = 'BALANCES', // View for balances + SWAP = 'SWAP', // View for swap assets } /**