From f0385081af5a474f33f7bcbde0e9da20b2baa2f1 Mon Sep 17 00:00:00 2001 From: musidlo Date: Sat, 25 Jan 2025 18:47:51 +0100 Subject: [PATCH 1/5] Added first part of dummy UI for earn --- apps/next/pages/earn/index.tsx | 22 ++ packages/app/components/icons/IconStack.tsx | 27 ++ packages/app/components/icons/index.tsx | 1 + .../app/components/sidebar/HomeSideBar.tsx | 6 + packages/app/features/earn/screen.tsx | 242 ++++++++++++++++++ 5 files changed, 298 insertions(+) create mode 100644 apps/next/pages/earn/index.tsx create mode 100644 packages/app/components/icons/IconStack.tsx create mode 100644 packages/app/features/earn/screen.tsx diff --git a/apps/next/pages/earn/index.tsx b/apps/next/pages/earn/index.tsx new file mode 100644 index 000000000..1b68942ec --- /dev/null +++ b/apps/next/pages/earn/index.tsx @@ -0,0 +1,22 @@ +import { EarnScreen } from 'app/features/earn/screen' +import Head from 'next/head' +import type { NextPageWithLayout } from '../_app' +import { HomeLayout } from 'app/features/home/layout.web' +import { TopNav } from 'app/components/TopNav' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Earn + + + + ) +} + +Page.getLayout = (children) => ( + }>{children} +) + +export default Page diff --git a/packages/app/components/icons/IconStack.tsx b/packages/app/components/icons/IconStack.tsx new file mode 100644 index 000000000..0b1a8740f --- /dev/null +++ b/packages/app/components/icons/IconStack.tsx @@ -0,0 +1,27 @@ +import type { ColorTokens } from '@my/ui' +import { Path, Svg } from 'react-native-svg' +import * as React from 'react' +import { memo } from 'react' +import { type IconProps, themed } from '@tamagui/helpers-icon' + +const Stacks = (props) => { + const { size, color, ...rest } = props + + return ( + + + + ) +} + +const IconStacks = memo(themed(Stacks)) +export { IconStacks } diff --git a/packages/app/components/icons/index.tsx b/packages/app/components/icons/index.tsx index 4233c1f95..e671223e5 100644 --- a/packages/app/components/icons/index.tsx +++ b/packages/app/components/icons/index.tsx @@ -61,3 +61,4 @@ export { IconSlash } from './IconSlash' export { IconUpgrade } from './IconUpgrade' export { IconKey } from './IconKey' export { IconIdCard } from './IconIdCard' +export { IconStacks } from './IconStack' diff --git a/packages/app/components/sidebar/HomeSideBar.tsx b/packages/app/components/sidebar/HomeSideBar.tsx index 11b93bb09..777bfe5c6 100644 --- a/packages/app/components/sidebar/HomeSideBar.tsx +++ b/packages/app/components/sidebar/HomeSideBar.tsx @@ -19,6 +19,7 @@ import { IconDeviceReset, IconHome, IconSendLogo, + IconStacks, } from 'app/components/icons' import { SideBarNavLink } from 'app/components/sidebar/SideBarNavLink' @@ -40,6 +41,11 @@ const links = [ text: 'Send', href: '/send', }, + { + icon: , + text: 'Earn', + href: '/earn', + }, { icon: , text: 'Activity', diff --git a/packages/app/features/earn/screen.tsx b/packages/app/features/earn/screen.tsx new file mode 100644 index 000000000..3e41adf2d --- /dev/null +++ b/packages/app/features/earn/screen.tsx @@ -0,0 +1,242 @@ +import { Button, Card, Fade, Paragraph, Separator, XStack, YStack, LinearGradient } from '@my/ui' +import type { ReactNode } from 'react' +import { useThemeSetting } from '@tamagui/next-theme' +import { IconArrowRight, IconStacks } from 'app/components/icons' +import { IconCoin } from 'app/components/icons/IconCoin' + +export const EarnScreen = () => { + return ( + + + + + ) +} + +const ListItem = ({ children }: { children: ReactNode }) => { + return ( + + + • + + + {children} + + + ) +} + +const Badge = ({ text }: { text: string }) => { + const { resolvedTheme } = useThemeSetting() + + const badgeBackgroundColor = resolvedTheme?.startsWith('dark') + ? 'rgba(255,255,255, 0.1)' + : 'rgba(0,0,0, 0.1)' + + return ( + + + {text} + + ) +} + +const Row = ({ label, value }: { label: string; value: string }) => { + return ( + + + {label} + + {value} + + ) +} + +const SectionButton = ({ text }: { text: string }) => { + return ( + + ) +} + +// TODO plug on press handler +const LearnSection = () => { + return ( + + + + + + + + + Deposits + + + + + Start Growing + + + Your USDC Saving + + + + Learn How It Works + + + + + + + ) +} + +// TODO plug on press handler +const EarningsCallToAction = () => { + return ( + + + + + + Boost Your Savings Instantly + + + + High APY: up to 12% on your deposits + Full Flexibility: Access your funds anytime + Rewards: Bonus SEND tokens + + + + + + ) +} + +// TODO plug real values +// TODO plug on press handler +const EarningsSummary = () => { + const totalValue = '123,123,123,233,123' + + return ( + + + + + + + Total Value + + + { + switch (true) { + case totalValue.length > 14: + return '$8' + case totalValue.length > 8: + return '$9' + default: + return '$11' + } + })()} + $gtLg={{ + size: (() => { + switch (true) { + case totalValue.length > 16: + return '$9' + case totalValue.length > 8: + return '$10' + default: + return '$11' + } + })(), + }} + > + {totalValue} + + + 16 ? '$1.5' : '$2.5'} /> + USDC + + + + + + + + + + + + + + ) +} + +const DetailsSection = () => { + // TODO fetch real data + const areEarningsActive = false + + return areEarningsActive ? : +} From 136a33fb4d39a867beed9bee046cbab890a09968 Mon Sep 17 00:00:00 2001 From: musidlo Date: Tue, 28 Jan 2025 19:00:45 +0100 Subject: [PATCH 2/5] Added earning form screen --- apps/next/pages/earn/earning-form.tsx | 22 ++ packages/app/features/earn/EarningForm.tsx | 350 ++++++++++++++++++ packages/app/features/earn/components/Row.tsx | 16 + .../earn/components/SectionButton.tsx | 18 + packages/app/features/earn/screen.tsx | 46 +-- packages/app/routers/params.tsx | 13 + 6 files changed, 430 insertions(+), 35 deletions(-) create mode 100644 apps/next/pages/earn/earning-form.tsx create mode 100644 packages/app/features/earn/EarningForm.tsx create mode 100644 packages/app/features/earn/components/Row.tsx create mode 100644 packages/app/features/earn/components/SectionButton.tsx diff --git a/apps/next/pages/earn/earning-form.tsx b/apps/next/pages/earn/earning-form.tsx new file mode 100644 index 000000000..ef9a230e1 --- /dev/null +++ b/apps/next/pages/earn/earning-form.tsx @@ -0,0 +1,22 @@ +import type { NextPageWithLayout } from '../_app' +import Head from 'next/head' +import { HomeLayout } from 'app/features/home/layout.web' +import { TopNav } from 'app/components/TopNav' +import { EarningForm } from '../../../../packages/app/features/earn/EarningForm' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Start Earning + + + + ) +} + +Page.getLayout = (children) => ( + }>{children} +) + +export default Page diff --git a/packages/app/features/earn/EarningForm.tsx b/packages/app/features/earn/EarningForm.tsx new file mode 100644 index 000000000..a55dd4e0d --- /dev/null +++ b/packages/app/features/earn/EarningForm.tsx @@ -0,0 +1,350 @@ +import { + Button, + Card, + Fade, + Paragraph, + Separator, + XStack, + YStack, + SubmitButton, + Stack, + Anchor, + Spinner, +} from '@my/ui' +import { z } from 'zod' +import { FormProvider, useForm } from 'react-hook-form' +import { formFields, SchemaForm } from 'app/utils/SchemaForm' +import { useRouter } from 'solito/router' +import formatAmount, { localizeAmount, sanitizeAmount } from 'app/utils/formatAmount' +import { formatUnits } from 'viem' +import { useCoin, useCoins } from 'app/provider/coins' +import { useEffect, useState } from 'react' +import { IconCoin } from 'app/components/icons/IconCoin' +import { useEarnScreenParams } from 'app/routers/params' +import { Row } from 'app/features/earn/components/Row' + +const StartEarningSchema = z.object({ + amount: formFields.text, + areTermsAccepted: formFields.boolean_checkbox, +}) + +export const EarningForm = () => { + const form = useForm>() + const router = useRouter() + const { coin, isLoading: isUSDCLoading } = useCoin('USDC') + const { isLoading: isLoadingCoins } = useCoins() + const [isInputFocused, setIsInputFocused] = useState(false) + const [earnParams, setEarnParams] = useEarnScreenParams() + + const parsedAmount = BigInt(earnParams.amount ?? '0') + const formAmount = form.watch('amount') + const areTermsAccepted = form.watch('areTermsAccepted') + + const canSubmit = + !isUSDCLoading && + coin?.balance !== undefined && + coin.balance >= parsedAmount && + parsedAmount > BigInt(0) && + areTermsAccepted + + const insufficientAmount = + coin?.balance !== undefined && earnParams.amount !== undefined && parsedAmount > coin?.balance + + const onSubmit = async () => { + if (!canSubmit) return + + // TODO logic for creating vault + + router.push({ + pathname: '/earn', + }) + } + + useEffect(() => { + const subscription = form.watch(({ amount: _amount }) => { + const sanitizedAmount = sanitizeAmount(_amount, coin?.decimals) + + setEarnParams( + { + ...earnParams, + amount: sanitizedAmount.toString(), + }, + { webBehavior: 'replace' } + ) + }) + + return () => subscription.unsubscribe() + }, [form.watch, setEarnParams, earnParams, coin?.decimals]) + + if (isLoadingCoins || !coin || (!coin.balance && coin.balance !== BigInt(0))) { + return + } + + return ( + + + Deposit Amount + + + { + switch (true) { + case formAmount?.length > 12: + return '$7' + default: + return '$9' + } + })(), + color: '$color12', + fontWeight: '500', + bw: 0, + br: 0, + p: 1, + focusStyle: { + outlineWidth: 0, + }, + placeholder: '0', + fontFamily: '$mono', + '$theme-dark': { + placeholderTextColor: '$darkGrayTextField', + }, + '$theme-light': { + placeholderTextColor: '$darkGrayTextField', + }, + inputMode: coin?.decimals ? 'decimal' : 'numeric', + onChangeText: (amount) => { + const localizedAmount = localizeAmount(amount) + form.setValue('amount', localizedAmount) + }, + onFocus: () => setIsInputFocused(true), + onBlur: () => setIsInputFocused(false), + fieldsetProps: { + width: '70%', + }, + $gtSm: { + fontSize: (() => { + switch (true) { + case formAmount?.length > 14: + return '$7' + default: + return '$9' + } + })(), + }, + }, + }} + formProps={{ + testID: 'SendForm', + $gtSm: { + maxWidth: '100%', + }, + // using tamagui props there is bug with justify content set to center after refreshing the page + style: { justifyContent: 'space-between' }, + }} + defaultValues={{ + amount: earnParams.amount + ? localizeAmount(formatUnits(BigInt(earnParams.amount), coin?.decimals)) + : undefined, + areTermsAccepted: false, + }} + renderAfter={({ submit }) => ( + + + + CONFIRM DEPOSIT + + + + )} + > + {({ amount, areTermsAccepted }) => ( + + + + + {amount} + + + USDC + + + + + + {(() => { + switch (true) { + case isUSDCLoading: + return + case !coin?.balance && coin?.balance !== BigInt(0): + return null + default: + return ( + + + + Balance: + + + {formatAmount(formatUnits(coin.balance, coin?.decimals), 12, 4)} + + + {insufficientAmount && ( + + Insufficient funds + + )} + + ) + } + })()} + + + + + {parsedAmount > 0 ? ( + // TODO calculate real values + + ) : ( + + )} + + {areTermsAccepted} + + I accept{' '} + + , &{' '} + + + + + )} + + + + ) +} + +const TermsLink = ({ text, href }: { text: string; href: string }) => { + return ( + + {text} + + ) +} + +const CalculatedBenefits = ({ + apy, + monthlyEarning, + rewards, +}: { + apy: number + monthlyEarning: number + rewards: number +}) => { + return ( + + + + Benefits + + + + + Deposit APY + {apy}% + + + + + + + + + + + ) +} + +const StaticBenefits = () => { + return ( + + + + Benefits + + + + + APY + up to 12% + + + + + + + + + + + + ) +} diff --git a/packages/app/features/earn/components/Row.tsx b/packages/app/features/earn/components/Row.tsx new file mode 100644 index 000000000..0945bf60c --- /dev/null +++ b/packages/app/features/earn/components/Row.tsx @@ -0,0 +1,16 @@ +import { Paragraph, XStack } from '@my/ui' + +export const Row = ({ label, value }: { label: string; value: string }) => { + return ( + + + {label} + + {value} + + ) +} diff --git a/packages/app/features/earn/components/SectionButton.tsx b/packages/app/features/earn/components/SectionButton.tsx new file mode 100644 index 000000000..52ae23426 --- /dev/null +++ b/packages/app/features/earn/components/SectionButton.tsx @@ -0,0 +1,18 @@ +import { Button } from '@my/ui' + +export const SectionButton = ({ text, onPress }: { text: string; onPress: () => void }) => { + return ( + + ) +} diff --git a/packages/app/features/earn/screen.tsx b/packages/app/features/earn/screen.tsx index 3e41adf2d..19f387073 100644 --- a/packages/app/features/earn/screen.tsx +++ b/packages/app/features/earn/screen.tsx @@ -1,14 +1,19 @@ -import { Button, Card, Fade, Paragraph, Separator, XStack, YStack, LinearGradient } from '@my/ui' +import { Card, Fade, LinearGradient, Paragraph, Separator, XStack, YStack } from '@my/ui' import type { ReactNode } from 'react' import { useThemeSetting } from '@tamagui/next-theme' import { IconArrowRight, IconStacks } from 'app/components/icons' import { IconCoin } from 'app/components/icons/IconCoin' +import { useRouter } from 'solito/router' +import { Row } from 'app/features/earn/components/Row' +import { SectionButton } from 'app/features/earn/components/SectionButton' export const EarnScreen = () => { return ( + {/*// TODO remove this line*/} + ) } @@ -57,37 +62,6 @@ const Badge = ({ text }: { text: string }) => { ) } -const Row = ({ label, value }: { label: string; value: string }) => { - return ( - - - {label} - - {value} - - ) -} - -const SectionButton = ({ text }: { text: string }) => { - return ( - - ) -} - // TODO plug on press handler const LearnSection = () => { return ( @@ -148,6 +122,8 @@ const LearnSection = () => { // TODO plug on press handler const EarningsCallToAction = () => { + const { push } = useRouter() + return ( @@ -163,7 +139,7 @@ const EarningsCallToAction = () => { Rewards: Bonus SEND tokens - + push('/earn/earning-form')} /> ) @@ -172,7 +148,7 @@ const EarningsCallToAction = () => { // TODO plug real values // TODO plug on press handler const EarningsSummary = () => { - const totalValue = '123,123,123,233,123' + const totalValue = '2,267.50' return ( @@ -228,7 +204,7 @@ const EarningsSummary = () => { - + {}} /> ) diff --git a/packages/app/routers/params.tsx b/packages/app/routers/params.tsx index 18b1d9a74..4a3225f47 100644 --- a/packages/app/routers/params.tsx +++ b/packages/app/routers/params.tsx @@ -212,3 +212,16 @@ export const useAuthScreenParams = () => { setParams, ] as const } + +export type EarnScreenParams = { + amount?: string +} + +const { useParam: useEarnParam, useParams: useEarnParams } = createParam() + +export const useEarnScreenParams = () => { + const { setParams } = useEarnParams() + const [amount] = useEarnParam('amount') + + return [{ amount }, setParams] as const +} From 7b9b62b56079fe824682b60057a67fd3e7569255 Mon Sep 17 00:00:00 2001 From: musidlo Date: Wed, 29 Jan 2025 16:56:27 +0100 Subject: [PATCH 3/5] Added active earnings screen --- apps/next/pages/earn/active-earnings.tsx | 24 +++ apps/next/pages/earn/earning-form.tsx | 6 +- .../components/icons/IconSendSingleLetter.tsx | 27 +++ packages/app/components/icons/index.tsx | 1 + packages/app/features/earn/ActiveEarnings.tsx | 156 ++++++++++++++++++ packages/app/features/earn/screen.tsx | 5 +- packages/app/utils/useHoverStyles.ts | 1 + 7 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 apps/next/pages/earn/active-earnings.tsx create mode 100644 packages/app/components/icons/IconSendSingleLetter.tsx create mode 100644 packages/app/features/earn/ActiveEarnings.tsx diff --git a/apps/next/pages/earn/active-earnings.tsx b/apps/next/pages/earn/active-earnings.tsx new file mode 100644 index 000000000..b415ec60d --- /dev/null +++ b/apps/next/pages/earn/active-earnings.tsx @@ -0,0 +1,24 @@ +import type { NextPageWithLayout } from '../_app' +import Head from 'next/head' +import { HomeLayout } from 'app/features/home/layout.web' +import { TopNav } from 'app/components/TopNav' +import { ActiveEarnings } from 'app/features/earn/ActiveEarnings' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Active Earnings + + + + ) +} + +Page.getLayout = (children) => ( + }> + {children} + +) + +export default Page diff --git a/apps/next/pages/earn/earning-form.tsx b/apps/next/pages/earn/earning-form.tsx index ef9a230e1..58bcba2bd 100644 --- a/apps/next/pages/earn/earning-form.tsx +++ b/apps/next/pages/earn/earning-form.tsx @@ -2,7 +2,7 @@ import type { NextPageWithLayout } from '../_app' import Head from 'next/head' import { HomeLayout } from 'app/features/home/layout.web' import { TopNav } from 'app/components/TopNav' -import { EarningForm } from '../../../../packages/app/features/earn/EarningForm' +import { EarningForm } from 'app/features/earn/EarningForm' export const Page: NextPageWithLayout = () => { return ( @@ -16,7 +16,9 @@ export const Page: NextPageWithLayout = () => { } Page.getLayout = (children) => ( - }>{children} + }> + {children} + ) export default Page diff --git a/packages/app/components/icons/IconSendSingleLetter.tsx b/packages/app/components/icons/IconSendSingleLetter.tsx new file mode 100644 index 000000000..22260efca --- /dev/null +++ b/packages/app/components/icons/IconSendSingleLetter.tsx @@ -0,0 +1,27 @@ +import { memo } from 'react' +import { type IconProps, themed } from '@tamagui/helpers-icon' +import { Path, Svg } from 'react-native-svg' +import type { ColorTokens } from '@my/ui' + +const SendSingleLetter = (props) => { + const { size, color, ...rest } = props + + return ( + + + + + ) +} + +const IconSendSingleLetter = memo(themed(SendSingleLetter)) +export { IconSendSingleLetter } diff --git a/packages/app/components/icons/index.tsx b/packages/app/components/icons/index.tsx index e671223e5..150ca9b83 100644 --- a/packages/app/components/icons/index.tsx +++ b/packages/app/components/icons/index.tsx @@ -62,3 +62,4 @@ export { IconUpgrade } from './IconUpgrade' export { IconKey } from './IconKey' export { IconIdCard } from './IconIdCard' export { IconStacks } from './IconStack' +export { IconSendSingleLetter } from './IconSendSingleLetter' diff --git a/packages/app/features/earn/ActiveEarnings.tsx b/packages/app/features/earn/ActiveEarnings.tsx new file mode 100644 index 000000000..fa8224b51 --- /dev/null +++ b/packages/app/features/earn/ActiveEarnings.tsx @@ -0,0 +1,156 @@ +import { Card, Fade, Paragraph, Separator, Stack, XStack, YStack } from '@my/ui' +import { IconCoin } from 'app/components/icons/IconCoin' +import { SectionButton } from 'app/features/earn/components/SectionButton' +import { useRouter } from 'solito/router' +import type { ReactNode } from 'react' +import { ArrowDown } from '@tamagui/lucide-icons' +import { IconSendSingleLetter, IconStacks } from 'app/components/icons' +import { useHoverStyles } from 'app/utils/useHoverStyles' + +export const ActiveEarnings = () => { + const { push } = useRouter() + + return ( + + + + + + + + + + + push('/earn/earning-form')} /> + + ) +} + +// TODO plug real total value +export const TotalValue = () => { + const totalValue = '2,780.50' + + return ( + + + + + + USDC + + + { + switch (true) { + case totalValue.length > 16: + return '$9' + default: + return '$11' + } + })()} + $gtLg={{ + size: (() => { + switch (true) { + case totalValue.length > 16: + return '$9' + case totalValue.length > 8: + return '$10' + default: + return '$11' + } + })(), + }} + > + {totalValue} + + + + + Total Value + + + + + ) +} + +// TODO plug real values +export const ActiveEarningBreakdown = () => { + return ( + + + + + + + + ) +} + +export const BreakdownRow = ({ + symbol, + value, + label, +}: { + symbol: string + label: string + value: string +}) => { + return ( + + + + {label} + + {value} + + ) +} + +export const EarningButton = ({ + Icon, + label, + href, +}: { + label: string + Icon: () => ReactNode + href: string +}) => { + const hoverStyles = useHoverStyles() + + return ( + + + + + + {label} + + + + + ) +} diff --git a/packages/app/features/earn/screen.tsx b/packages/app/features/earn/screen.tsx index 19f387073..41a883416 100644 --- a/packages/app/features/earn/screen.tsx +++ b/packages/app/features/earn/screen.tsx @@ -12,7 +12,7 @@ export const EarnScreen = () => { - {/*// TODO remove this line*/} + {/*// TODO remove this line when pluging in real data*/} ) @@ -148,6 +148,7 @@ const EarningsCallToAction = () => { // TODO plug real values // TODO plug on press handler const EarningsSummary = () => { + const { push } = useRouter() const totalValue = '2,267.50' return ( @@ -204,7 +205,7 @@ const EarningsSummary = () => { - {}} /> + push('/earn/active-earnings')} /> ) diff --git a/packages/app/utils/useHoverStyles.ts b/packages/app/utils/useHoverStyles.ts index 565c7b5fe..552ac7a92 100644 --- a/packages/app/utils/useHoverStyles.ts +++ b/packages/app/utils/useHoverStyles.ts @@ -10,5 +10,6 @@ export const useHoverStyles = () => { return { background: rowHoverBC, transition: 'background 0.2s ease-in-out', + cursor: 'pointer', } } From 25471f105d551e970ec5010ca0a24c8c25237b1a Mon Sep 17 00:00:00 2001 From: musidlo Date: Wed, 29 Jan 2025 20:06:46 +0100 Subject: [PATCH 4/5] Added withdraw form --- apps/next/pages/earn/withdraw-form.tsx | 24 ++ packages/app/features/earn/ActiveEarnings.tsx | 20 +- packages/app/features/earn/EarningForm.tsx | 78 +----- packages/app/features/earn/WithdrawForm.tsx | 255 ++++++++++++++++++ .../earn/components/CalculatedBenefits.tsx | 60 +++++ .../features/earn/components/EarnTerms.tsx | 30 +++ packages/app/features/earn/components/Row.tsx | 23 +- 7 files changed, 415 insertions(+), 75 deletions(-) create mode 100644 apps/next/pages/earn/withdraw-form.tsx create mode 100644 packages/app/features/earn/WithdrawForm.tsx create mode 100644 packages/app/features/earn/components/CalculatedBenefits.tsx create mode 100644 packages/app/features/earn/components/EarnTerms.tsx diff --git a/apps/next/pages/earn/withdraw-form.tsx b/apps/next/pages/earn/withdraw-form.tsx new file mode 100644 index 000000000..5647f413e --- /dev/null +++ b/apps/next/pages/earn/withdraw-form.tsx @@ -0,0 +1,24 @@ +import type { NextPageWithLayout } from '../_app' +import Head from 'next/head' +import { HomeLayout } from 'app/features/home/layout.web' +import { TopNav } from 'app/components/TopNav' +import { WithdrawForm } from 'app/features/earn/WithdrawForm' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Withdraw Deposit + + + + ) +} + +Page.getLayout = (children) => ( + }> + {children} + +) + +export default Page diff --git a/packages/app/features/earn/ActiveEarnings.tsx b/packages/app/features/earn/ActiveEarnings.tsx index fa8224b51..965709f51 100644 --- a/packages/app/features/earn/ActiveEarnings.tsx +++ b/packages/app/features/earn/ActiveEarnings.tsx @@ -2,20 +2,26 @@ import { Card, Fade, Paragraph, Separator, Stack, XStack, YStack } from '@my/ui' import { IconCoin } from 'app/components/icons/IconCoin' import { SectionButton } from 'app/features/earn/components/SectionButton' import { useRouter } from 'solito/router' -import type { ReactNode } from 'react' +import type { NamedExoticComponent } from 'react' import { ArrowDown } from '@tamagui/lucide-icons' import { IconSendSingleLetter, IconStacks } from 'app/components/icons' import { useHoverStyles } from 'app/utils/useHoverStyles' +import type { IconProps } from '@tamagui/helpers-icon' export const ActiveEarnings = () => { const { push } = useRouter() + // TODO loader when deposit balances are loading + // if (false) { + // return + // } + return ( - - + + @@ -118,11 +124,16 @@ export const EarningButton = ({ href, }: { label: string - Icon: () => ReactNode + Icon: NamedExoticComponent href: string }) => { + const router = useRouter() const hoverStyles = useHoverStyles() + const handleOnPress = () => { + router.push(href) + } + return ( { }, }} formProps={{ - testID: 'SendForm', + testID: 'earning-form', $gtSm: { maxWidth: '100%', }, @@ -216,7 +217,7 @@ export const EarningForm = () => { > { size={'$5'} fontWeight={'600'} > - {formatAmount(formatUnits(coin.balance, coin?.decimals), 12, 4)} + {formatAmount(formatUnits(coin.balance, coin?.decimals), 12, 2)} {insufficientAmount && ( @@ -248,25 +249,13 @@ export const EarningForm = () => { {parsedAmount > 0 ? ( // TODO calculate real values - + ) : ( )} {areTermsAccepted} - - I accept{' '} - - , &{' '} - - + )} @@ -276,53 +265,6 @@ export const EarningForm = () => { ) } -const TermsLink = ({ text, href }: { text: string; href: string }) => { - return ( - - {text} - - ) -} - -const CalculatedBenefits = ({ - apy, - monthlyEarning, - rewards, -}: { - apy: number - monthlyEarning: number - rewards: number -}) => { - return ( - - - - Benefits - - - - - Deposit APY - {apy}% - - - - - - - - - - - ) -} - const StaticBenefits = () => { return ( diff --git a/packages/app/features/earn/WithdrawForm.tsx b/packages/app/features/earn/WithdrawForm.tsx new file mode 100644 index 000000000..dfd73ef5d --- /dev/null +++ b/packages/app/features/earn/WithdrawForm.tsx @@ -0,0 +1,255 @@ +import { Button, Fade, Paragraph, Spinner, Stack, SubmitButton, XStack, YStack } from '@my/ui' +import { z } from 'zod' +import { FormProvider, useForm } from 'react-hook-form' +import { formFields, SchemaForm } from 'app/utils/SchemaForm' +import { useRouter } from 'solito/router' +import { useEffect, useState } from 'react' +import { useEarnScreenParams } from 'app/routers/params' +import formatAmount, { localizeAmount, sanitizeAmount } from 'app/utils/formatAmount' +import { usdcCoin } from 'app/data/coins' +import { formatUnits } from 'viem' +import { IconCoin } from 'app/components/icons/IconCoin' +import { CalculatedBenefits } from 'app/features/earn/components/CalculatedBenefits' +import { EarnTerms } from 'app/features/earn/components/EarnTerms' + +const WithdrawDepositForm = z.object({ + amount: formFields.text, + areTermsAccepted: formFields.boolean_checkbox, +}) + +export const WithdrawForm = () => { + const form = useForm>() + const router = useRouter() + const [isInputFocused, setIsInputFocused] = useState(false) + const [earnParams, setEarnParams] = useEarnScreenParams() + const [isFormInitializedFromParams, setIsFormInitializedFromParams] = useState(false) + + // TODO fetch real balance + const depositBalance = BigInt(2780500000) + + const parsedAmount = BigInt(earnParams.amount ?? '0') + const formAmount = form.watch('amount') + const areTermsAccepted = form.watch('areTermsAccepted') + + const canSubmit = depositBalance >= parsedAmount && parsedAmount > BigInt(0) && areTermsAccepted + + const insufficientAmount = earnParams.amount !== undefined && parsedAmount > depositBalance + + const onSubmit = async () => { + if (!canSubmit) return + + // TODO logic for withdrawing from vault + + router.push('/earn/active-earnings') + } + + useEffect(() => { + const subscription = form.watch(({ amount: _amount }) => { + const sanitizedAmount = sanitizeAmount(_amount, usdcCoin.decimals) + + setEarnParams( + { + ...earnParams, + amount: sanitizedAmount.toString(), + }, + { webBehavior: 'replace' } + ) + }) + + return () => subscription.unsubscribe() + }, [form.watch, setEarnParams, earnParams]) + + useEffect(() => { + if (!isFormInitializedFromParams && earnParams.amount) { + form.setValue( + 'amount', + localizeAmount(formatUnits(BigInt(earnParams.amount), usdcCoin.decimals)) + ) + + setIsFormInitializedFromParams(true) + } + }, [earnParams.amount, isFormInitializedFromParams, form.setValue]) + + // TODO loader when deposit balance is loading + // if (false) { + // return + // } + + return ( + + + Withdraw Amount + + + { + switch (true) { + case formAmount?.length > 12: + return '$7' + default: + return '$9' + } + })(), + color: '$color12', + fontWeight: '500', + bw: 0, + br: 0, + p: 1, + focusStyle: { + outlineWidth: 0, + }, + placeholder: '0', + fontFamily: '$mono', + '$theme-dark': { + placeholderTextColor: '$darkGrayTextField', + }, + '$theme-light': { + placeholderTextColor: '$darkGrayTextField', + }, + inputMode: 'decimal', + onChangeText: (amount) => { + const localizedAmount = localizeAmount(amount) + form.setValue('amount', localizedAmount) + }, + onFocus: () => setIsInputFocused(true), + onBlur: () => setIsInputFocused(false), + fieldsetProps: { + width: '70%', + }, + $gtSm: { + fontSize: (() => { + switch (true) { + case formAmount?.length > 14: + return '$7' + default: + return '$9' + } + })(), + }, + }, + }} + formProps={{ + testID: 'withdraw-deposit-form', + $gtSm: { + maxWidth: '100%', + }, + // using tamagui props there is bug with justify content set to center after refreshing the page + style: { justifyContent: 'space-between' }, + }} + defaultValues={{ + amount: undefined, + areTermsAccepted: false, + }} + renderAfter={({ submit }) => ( + + + + CONFIRM WITHDRAW + + + + )} + > + {({ amount, areTermsAccepted }) => ( + + + + + {amount} + + + USDC + + + + + + + + + Deposit Balance: + + + {formatAmount(formatUnits(depositBalance, usdcCoin.decimals), 12, 2)} + + + USDC + + + {insufficientAmount && ( + + Insufficient funds + + )} + + + + + + {/*TODO plug real current and override values*/} + BigInt(0) ? '8' : undefined} + overrideMonthlyEarning={parsedAmount > BigInt(0) ? '7' : undefined} + overrideRewards={parsedAmount > BigInt(0) ? '2,100' : undefined} + /> + + {areTermsAccepted} + + + + )} + + + + ) +} diff --git a/packages/app/features/earn/components/CalculatedBenefits.tsx b/packages/app/features/earn/components/CalculatedBenefits.tsx new file mode 100644 index 000000000..e1a5553f8 --- /dev/null +++ b/packages/app/features/earn/components/CalculatedBenefits.tsx @@ -0,0 +1,60 @@ +import { Card, Fade, Paragraph, Separator, XStack, YStack } from '@my/ui' +import { Row } from 'app/features/earn/components/Row' + +export const CalculatedBenefits = ({ + apy, + monthlyEarning, + rewards, + overrideApy, + overrideMonthlyEarning, + overrideRewards, +}: { + apy: string + monthlyEarning: string + rewards: string + overrideApy?: string + overrideMonthlyEarning?: string + overrideRewards?: string +}) => { + return ( + + + + Benefits + + + + + Deposit APY + + + {apy}% + + {overrideApy && ( + + {overrideApy}% + + )} + + + + + + + + + + + + ) +} diff --git a/packages/app/features/earn/components/EarnTerms.tsx b/packages/app/features/earn/components/EarnTerms.tsx new file mode 100644 index 000000000..a9ed8ae0f --- /dev/null +++ b/packages/app/features/earn/components/EarnTerms.tsx @@ -0,0 +1,30 @@ +import { Anchor, Paragraph } from '@my/ui' + +export const EarnTerms = () => { + return ( + + I accept{' '} + + , &{' '} + + + ) +} + +const TermsLink = ({ text, href }: { text: string; href: string }) => { + return ( + + {text} + + ) +} diff --git a/packages/app/features/earn/components/Row.tsx b/packages/app/features/earn/components/Row.tsx index 0945bf60c..efacfad17 100644 --- a/packages/app/features/earn/components/Row.tsx +++ b/packages/app/features/earn/components/Row.tsx @@ -1,8 +1,16 @@ import { Paragraph, XStack } from '@my/ui' -export const Row = ({ label, value }: { label: string; value: string }) => { +export const Row = ({ + label, + value, + overrideValue, +}: { + label: string + value: string + overrideValue?: string +}) => { return ( - + { > {label} - {value} + + + {value} + + {overrideValue && ( + + {overrideValue} + + )} + ) } From 4c60a0683ce32524f7a7a25d8c0fefdfbc51e450 Mon Sep 17 00:00:00 2001 From: musidlo Date: Fri, 31 Jan 2025 17:05:48 +0100 Subject: [PATCH 5/5] Added earnings and rewards balance --- apps/next/pages/earn/earnings-balance.tsx | 24 +++ apps/next/pages/earn/rewards-balance.tsx | 24 +++ packages/app/features/earn/ActiveEarnings.tsx | 18 +- packages/app/features/earn/EarningForm.tsx | 2 +- .../app/features/earn/EarningsBalance.tsx | 175 ++++++++++++++++++ packages/app/features/earn/RewardsBalance.tsx | 171 +++++++++++++++++ packages/app/features/earn/WithdrawForm.tsx | 4 +- packages/app/features/earn/screen.tsx | 7 +- packages/app/features/home/TokenDetails.tsx | 4 +- 9 files changed, 416 insertions(+), 13 deletions(-) create mode 100644 apps/next/pages/earn/earnings-balance.tsx create mode 100644 apps/next/pages/earn/rewards-balance.tsx create mode 100644 packages/app/features/earn/EarningsBalance.tsx create mode 100644 packages/app/features/earn/RewardsBalance.tsx diff --git a/apps/next/pages/earn/earnings-balance.tsx b/apps/next/pages/earn/earnings-balance.tsx new file mode 100644 index 000000000..5c495faf1 --- /dev/null +++ b/apps/next/pages/earn/earnings-balance.tsx @@ -0,0 +1,24 @@ +import type { NextPageWithLayout } from '../_app' +import Head from 'next/head' +import { HomeLayout } from 'app/features/home/layout.web' +import { TopNav } from 'app/components/TopNav' +import { EarningsBalance } from 'app/features/earn/EarningsBalance' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Earnings Balance + + + + ) +} + +Page.getLayout = (children) => ( + } fullHeight> + {children} + +) + +export default Page diff --git a/apps/next/pages/earn/rewards-balance.tsx b/apps/next/pages/earn/rewards-balance.tsx new file mode 100644 index 000000000..fb892702a --- /dev/null +++ b/apps/next/pages/earn/rewards-balance.tsx @@ -0,0 +1,24 @@ +import type { NextPageWithLayout } from '../_app' +import Head from 'next/head' +import { HomeLayout } from 'app/features/home/layout.web' +import { TopNav } from 'app/components/TopNav' +import { RewardsBalance } from 'app/features/earn/RewardsBalance' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Rewards Balance + + + + ) +} + +Page.getLayout = (children) => ( + } fullHeight> + {children} + +) + +export default Page diff --git a/packages/app/features/earn/ActiveEarnings.tsx b/packages/app/features/earn/ActiveEarnings.tsx index 965709f51..7f7d7b5ec 100644 --- a/packages/app/features/earn/ActiveEarnings.tsx +++ b/packages/app/features/earn/ActiveEarnings.tsx @@ -17,13 +17,17 @@ export const ActiveEarnings = () => { // } return ( - + - - + + @@ -33,7 +37,7 @@ export const ActiveEarnings = () => { } // TODO plug real total value -export const TotalValue = () => { +const TotalValue = () => { const totalValue = '2,780.50' return ( @@ -86,7 +90,7 @@ export const TotalValue = () => { } // TODO plug real values -export const ActiveEarningBreakdown = () => { +const ActiveEarningBreakdown = () => { return ( @@ -98,7 +102,7 @@ export const ActiveEarningBreakdown = () => { ) } -export const BreakdownRow = ({ +const BreakdownRow = ({ symbol, value, label, @@ -118,7 +122,7 @@ export const BreakdownRow = ({ ) } -export const EarningButton = ({ +const EarningButton = ({ Icon, label, href, diff --git a/packages/app/features/earn/EarningForm.tsx b/packages/app/features/earn/EarningForm.tsx index 8bbe2e9a3..e6e9f92a9 100644 --- a/packages/app/features/earn/EarningForm.tsx +++ b/packages/app/features/earn/EarningForm.tsx @@ -82,7 +82,7 @@ export const EarningForm = () => { } return ( - + Deposit Amount diff --git a/packages/app/features/earn/EarningsBalance.tsx b/packages/app/features/earn/EarningsBalance.tsx new file mode 100644 index 000000000..c79806286 --- /dev/null +++ b/packages/app/features/earn/EarningsBalance.tsx @@ -0,0 +1,175 @@ +import { + Button, + Card, + Fade, + Paragraph, + ScrollView, + Separator, + Spinner, + XStack, + YGroup, + YStack, +} from '@my/ui' +import { useRouter } from 'solito/router' +import { SectionButton } from 'app/features/earn/components/SectionButton' +import { IconCoin } from 'app/components/icons/IconCoin' +import { TokenActivityRow } from 'app/features/home/TokenActivityRow' +import { useActivityFeed } from 'app/features/activity/utils/useActivityFeed' + +export const EarningsBalance = () => { + const { push } = useRouter() + + // TODO loader when deposit balances are loading + // if (false) { + // return + // } + + const handleClaimPress = () => { + // TODO plug claim rewards logic + + push('/earn/active-earnings') + } + + return ( + + + + + + Earnings History + + + + + + + ) +} + +// TODO fetch activities that are earning related, here are all ATM +// TODO add support for activity row and details for earnings related activities +const EarningsFeed = () => { + const { + data, + isLoading: isLoadingActivities, + error: activitiesError, + isFetching: isFetchingActivities, + isFetchingNextPage: isFetchingNextPageActivities, + fetchNextPage, + hasNextPage, + } = useActivityFeed() + + const { pages } = data ?? {} + + return ( + <> + {(() => { + switch (true) { + case isLoadingActivities: + return + case activitiesError !== null: + return ( + + {activitiesError?.message.split('.').at(0) ?? `${activitiesError}`} + + ) + case pages?.length === 0: + return ( + <> + No earnings activities + + ) + default: { + const activities = (pages || []).flatMap((activity) => activity) + + return ( + + + {activities.map((activity) => ( + + + + ))} + + + ) + } + } + })()} + + {!isLoadingActivities && (isFetchingNextPageActivities || hasNextPage) ? ( + <> + {isFetchingNextPageActivities && } + {hasNextPage && ( + + )} + + ) : null} + + + ) +} + +// TODO plug read total earnings value +const TotalEarning = () => { + const totalValue = '484.50' + + return ( + + + + + + USDC + + + { + switch (true) { + case totalValue.length > 16: + return '$9' + default: + return '$11' + } + })()} + $gtLg={{ + size: (() => { + switch (true) { + case totalValue.length > 16: + return '$9' + case totalValue.length > 8: + return '$10' + default: + return '$11' + } + })(), + }} + > + {totalValue} + + + + + ${totalValue} + + + + + ) +} diff --git a/packages/app/features/earn/RewardsBalance.tsx b/packages/app/features/earn/RewardsBalance.tsx new file mode 100644 index 000000000..0f3febcc4 --- /dev/null +++ b/packages/app/features/earn/RewardsBalance.tsx @@ -0,0 +1,171 @@ +import { + Button, + Card, + Fade, + Paragraph, + ScrollView, + Separator, + Spinner, + XStack, + YGroup, + YStack, +} from '@my/ui' +import { SectionButton } from 'app/features/earn/components/SectionButton' +import { useRouter } from 'solito/router' +import { IconCoin } from 'app/components/icons/IconCoin' +import { TokenDetailsMarketData } from 'app/features/home/TokenDetails' +import { sendCoin } from 'app/data/coins' +import { TokenActivityRow } from 'app/features/home/TokenActivityRow' +import { useActivityFeed } from 'app/features/activity/utils/useActivityFeed' + +export const RewardsBalance = () => { + const { push } = useRouter() + + // TODO loader when deposit balances are loading + // if (false) { + // return + // } + + const handleClaimPress = () => { + // TODO plug claim rewards logic + + push('/earn/active-earnings') + } + + return ( + + + + + + Rewards History + + + + + + + ) +} + +// TODO fetch activities that are rewards related, here are all ATM +// TODO add support for activity row and details for rewqrds related activities +const RewardsFeed = () => { + const { + data, + isLoading: isLoadingActivities, + error: activitiesError, + isFetching: isFetchingActivities, + isFetchingNextPage: isFetchingNextPageActivities, + fetchNextPage, + hasNextPage, + } = useActivityFeed() + + const { pages } = data ?? {} + + return ( + <> + {(() => { + switch (true) { + case isLoadingActivities: + return + case activitiesError !== null: + return ( + + {activitiesError?.message.split('.').at(0) ?? `${activitiesError}`} + + ) + case pages?.length === 0: + return ( + <> + No rewards activities + + ) + default: { + const activities = (pages || []).flatMap((activity) => activity) + + return ( + + + {activities.map((activity) => ( + + + + ))} + + + ) + } + } + })()} + + {!isLoadingActivities && (isFetchingNextPageActivities || hasNextPage) ? ( + <> + {isFetchingNextPageActivities && } + {hasNextPage && ( + + )} + + ) : null} + + + ) +} + +// TODO plug read total rewards value +const TotalRewards = () => { + const totalValue = '15,000' + + return ( + + + + + + SEND + + + { + switch (true) { + case totalValue.length > 16: + return '$9' + default: + return '$11' + } + })()} + $gtLg={{ + size: (() => { + switch (true) { + case totalValue.length > 16: + return '$9' + case totalValue.length > 8: + return '$10' + default: + return '$11' + } + })(), + }} + > + {totalValue} + + + + + + + + ) +} diff --git a/packages/app/features/earn/WithdrawForm.tsx b/packages/app/features/earn/WithdrawForm.tsx index dfd73ef5d..468a1a89c 100644 --- a/packages/app/features/earn/WithdrawForm.tsx +++ b/packages/app/features/earn/WithdrawForm.tsx @@ -1,4 +1,4 @@ -import { Button, Fade, Paragraph, Spinner, Stack, SubmitButton, XStack, YStack } from '@my/ui' +import { Button, Fade, Paragraph, Stack, SubmitButton, XStack, YStack } from '@my/ui' import { z } from 'zod' import { FormProvider, useForm } from 'react-hook-form' import { formFields, SchemaForm } from 'app/utils/SchemaForm' @@ -76,7 +76,7 @@ export const WithdrawForm = () => { // } return ( - + Withdraw Amount diff --git a/packages/app/features/earn/screen.tsx b/packages/app/features/earn/screen.tsx index 41a883416..5afec1352 100644 --- a/packages/app/features/earn/screen.tsx +++ b/packages/app/features/earn/screen.tsx @@ -8,8 +8,13 @@ import { Row } from 'app/features/earn/components/Row' import { SectionButton } from 'app/features/earn/components/SectionButton' export const EarnScreen = () => { + // TODO loader when deposit balances are loading + // if (false) { + // return + // } + return ( - + {/*// TODO remove this line when pluging in real data*/} diff --git a/packages/app/features/home/TokenDetails.tsx b/packages/app/features/home/TokenDetails.tsx index 79ca9ec3f..372f2ebd8 100644 --- a/packages/app/features/home/TokenDetails.tsx +++ b/packages/app/features/home/TokenDetails.tsx @@ -9,7 +9,7 @@ import { XStack, YStack, } from '@my/ui' -import type { CoinWithBalance } from 'app/data/coins' +import type { allCoins, CoinWithBalance } from 'app/data/coins' import { ArrowDown, ArrowUp } from '@tamagui/lucide-icons' import { IconError } from 'app/components/icons' import { useTokenMarketData } from 'app/utils/coin-gecko' @@ -90,7 +90,7 @@ export const TokenDetails = ({ coin }: { coin: CoinWithBalance }) => { ) } -export const TokenDetailsMarketData = ({ coin }: { coin: CoinWithBalance }) => { +export const TokenDetailsMarketData = ({ coin }: { coin: allCoins[number] }) => { const { data: tokenMarketData, isLoading: isLoadingMarketData } = useTokenMarketData( coin.coingeckoTokenId )