diff --git a/src/components/info-alert/info-alert.tsx b/src/components/info-alert/info-alert.tsx new file mode 100644 index 00000000000..4dd3a9ec84e --- /dev/null +++ b/src/components/info-alert/info-alert.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Box, Text, useForegroundColor } from '@/design-system'; + +type InfoAlertProps = { + title: string; + description: string; + rightIcon: React.ReactNode; +}; + +export const InfoAlert: React.FC = ({ + rightIcon, + title, + description, +}) => { + return ( + + + {rightIcon} + + + + {title} + + + {description} + + + + ); +}; diff --git a/src/components/sheet/DynamicHeightSheet.js b/src/components/sheet/DynamicHeightSheet.js new file mode 100644 index 00000000000..68ead153e95 --- /dev/null +++ b/src/components/sheet/DynamicHeightSheet.js @@ -0,0 +1,229 @@ +import { BottomSheetScrollView } from '@gorhom/bottom-sheet'; +import { BottomSheetContext } from '@gorhom/bottom-sheet/src/contexts/external'; +import React, { + forwardRef, + Fragment, + useContext, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from 'react'; +import { Pressable, StyleSheet, TouchableWithoutFeedback } from 'react-native'; +import Animated, { + useAnimatedScrollHandler, + useSharedValue, +} from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useTheme } from '../../theme/ThemeContext'; +import { Centered } from '../layout'; +import SheetHandleFixedToTop, { + SheetHandleFixedToTopHeight, +} from './SheetHandleFixedToTop'; +import { useDimensions } from '@/hooks'; +import { useNavigation } from '@/navigation'; +import styled from '@/styled-thing'; +import { position } from '@/styles'; +import { IS_ANDROID, IS_IOS } from '@/env'; +import TouchableBackdrop from '../TouchableBackdrop'; + +const AndroidBackground = styled.View({ + ...position.coverAsObject, + backgroundColor: ({ backgroundColor }) => backgroundColor, +}); + +const Container = styled(Centered).attrs({ direction: 'column' })( + ({ + backgroundColor, + borderRadius, + contentHeight, + sheetHeight, + sheetHeightRatio, // Default to 2/3 of the screen + }) => ({ + borderTopLeftRadius: borderRadius, + borderTopRightRadius: borderRadius, + borderBottomRightRadius: 0, + borderBottomLeftRadius: 0, + backgroundColor: backgroundColor, + bottom: 0, + left: 0, + overflow: 'hidden', + position: 'absolute', + right: 0, + height: sheetHeight ? sheetHeight : contentHeight * sheetHeightRatio, // Set height to a ratio of the device height + }) +); + +const Content = styled.ScrollView.attrs(({ limitScrollViewContent }) => ({ + contentContainerStyle: limitScrollViewContent ? { height: '100%' } : {}, + directionalLockEnabled: true, + keyboardShouldPersistTaps: 'always', + scrollEventThrottle: 16, +}))( + ({ + contentHeight, + deviceHeight, + backgroundColor, + removeTopPadding, + sheetHeightRatio = 0.67, + }) => ({ + // Default to 2/3 of the screen + backgroundColor: backgroundColor, + ...(contentHeight + ? { height: deviceHeight * sheetHeightRatio + contentHeight } + : {}), // Set height to a ratio of the device height + paddingTop: removeTopPadding ? 0 : SheetHandleFixedToTopHeight, + width: '100%', + }) +); + +const ContentWrapper = styled.View({ + ...position.sizeAsObject('100%'), + backgroundColor: ({ backgroundColor }) => backgroundColor, +}); + +export default forwardRef(function SlackSheet( + { + additionalTopPadding = false, + removeTopPadding = false, + backgroundColor, + borderRadius = 30, + children, + contentHeight, + deferredHeight = false, + discoverSheet, + hideHandle = false, + limitScrollViewContent, + onContentSizeChange, + renderHeader, + scrollEnabled = true, + showsHorizontalScrollIndicator = true, + showsVerticalScrollIndicator = true, + showBlur, + testID, + removeClippedSubviews = false, + yPosition: givenYPosition, + onDismiss, + sheetHeight, + sheetHeightRatio = 1, // Default to full height of screen + ...props + }, + ref +) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const yPosition = givenYPosition || useSharedValue(0); + const { height: deviceHeight } = useDimensions(); + const { goBack } = useNavigation(); + const insets = useSafeAreaInsets(); + const bottomInset = useMemo( + () => (insets.bottom || scrollEnabled ? 42 : 30), + [insets.bottom, scrollEnabled] + ); + const { colors } = useTheme(); + const contentContainerStyle = useMemo( + () => ({ + paddingBottom: bottomInset, + }), + [bottomInset] + ); + + const sheet = useRef(); + const isInsideBottomSheet = !!useContext(BottomSheetContext); + + useImperativeHandle(ref, () => sheet.current); + + const scrollIndicatorInsets = useMemo( + () => ({ + bottom: bottomInset, + top: borderRadius + SheetHandleFixedToTopHeight, + }), + [borderRadius, bottomInset] + ); + + // In discover sheet we need to set it additionally + useEffect( + () => { + discoverSheet && + ios && + sheet.current.setNativeProps({ scrollIndicatorInsets }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const scrollHandler = useAnimatedScrollHandler(event => { + yPosition.value = event.contentOffset.y; + }); + + const bg = backgroundColor || colors.white; + + // callback upon closing the sheet + useEffect( + () => () => { + if (onDismiss) onDismiss(); + }, + [] + ); + + return ( + + {IS_ANDROID ? ( + + ) : null} + + + + {IS_ANDROID && ( + + + + )} + {!hideHandle && ( + + )} + + {renderHeader?.(yPosition)} + + {children} + + + + + ); +}); diff --git a/src/components/sheet/index.js b/src/components/sheet/index.js index e0706ee4ffa..6a00943fadb 100644 --- a/src/components/sheet/index.js +++ b/src/components/sheet/index.js @@ -8,6 +8,7 @@ export { export { default as SheetKeyboardAnimation } from './SheetKeyboardAnimation'; export { default as SheetSubtitleCycler } from './SheetSubtitleCycler'; export { default as SheetTitle } from './SheetTitle'; +export { default as DynamicHeightSheet } from './DynamicHeightSheet'; export { default as SlackSheet } from './SlackSheet'; export { BuyActionButton, diff --git a/src/graphql/queries/metadata.graphql b/src/graphql/queries/metadata.graphql index 37995f85ee9..ed4f8d55755 100644 --- a/src/graphql/queries/metadata.graphql +++ b/src/graphql/queries/metadata.graphql @@ -43,28 +43,6 @@ fragment baseQuery on Rewards { } color } - leaderboard { - accounts { - address - ens - avatarURL - earnings { - base { - ...amount - } - bonus { - ...amount - } - } - } - updatedAt - } -} - -query getRewardsLeaderboard { - rewards(project: OPTIMISM) { - ...baseQuery - } } query getRewardsDataForWallet($address: String!) { @@ -84,15 +62,14 @@ query getRewardsDataForWallet($address: String!) { pending { ...amount } + daily { + day + usd + token + } updatedAt } stats { - position { - current - change { - h24 - } - } actions { type amount { diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 4faecc580f6..0174e87dfc7 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1481,22 +1481,26 @@ "error_text": "Please check your internet connection and check back later.", "ended_title": "Program ended", "ended_text": "Stay tuned for what we have in store for you next!", + "info": { + "title": "Earn OP for swapping and bridging", + "description": "Get cash back by swapping on or bridging to Optimism. Rewards distributed monthly." + }, "op": { "airdrop_timing": { - "title": "Airdrops Every Week", - "text": "Swap on Optimism or bridge to Optimism to earn OP rewards. At the end of each week, the rewards you’ve earned will be airdropped to your wallet." + "title": "Airdrops Every Month", + "text": "Swap on Optimism or bridge to Optimism to earn OP rewards. At the end of each month, the rewards you’ve earned will be airdropped to your wallet." }, "amount_distributed": { "title": "67,850 OP Every Week", "text": "Up to 67,850 OP in rewards will be distributed every week. If weekly rewards run out, rewards will temporarily pause and resume the following week." }, "bridge": { - "title": "Earn 0.125% on Bridging", - "text": "While rewards are live, bridging verified tokens to Optimism will earn you approximately 0.125% back in OP.\n\nStats update periodically throughout the day. Check back soon if you don’t see your recent bridging reflected here." + "title": "Earn %{percent}% on Bridging", + "text": "While rewards are live, bridging verified tokens to Optimism will earn you approximately %{percent}% back in OP.\n\nStats update periodically throughout the day. Check back soon if you don’t see your recent bridging reflected here." }, "swap": { - "title": "Earn 0.425% on Swaps", - "text": "While rewards are live, swapping verified tokens on Optimism will earn you approximately 0.425% back in OP.\n\nStats update periodically throughout the day. Check back soon if you don’t see your recent swaps reflected here." + "title": "Earn %{percent}% on Swaps", + "text": "While rewards are live, swapping verified tokens on Optimism will earn you approximately %{percent}% back in OP.\n\nStats update periodically throughout the day. Check back soon if you don’t see your recent swaps reflected here." }, "position": { "title": "Leaderboard Position", diff --git a/src/screens/ExplainSheet.js b/src/screens/ExplainSheet.js index 073bded4b96..6be5413ff24 100644 --- a/src/screens/ExplainSheet.js +++ b/src/screens/ExplainSheet.js @@ -423,14 +423,22 @@ export const explainers = (params, colors) => ({ }, op_rewards_bridge: { emoji: '🌉', - title: i18n.t(i18n.l.rewards.op.bridge.title), - text: i18n.t(i18n.l.rewards.op.bridge.text), + title: i18n.t(i18n.l.rewards.op.bridge.title, { + percent: params?.percent || 0, + }), + text: i18n.t(i18n.l.rewards.op.bridge.text, { + percent: params?.percent || 0, + }), extraHeight: IS_ANDROID ? -65 : 10, }, op_rewards_swap: { emoji: '🔀', - title: i18n.t(i18n.l.rewards.op.swap.title), - text: i18n.t(i18n.l.rewards.op.swap.text), + title: i18n.t(i18n.l.rewards.op.swap.title, { + percent: params?.percent || 0, + }), + text: i18n.t(i18n.l.rewards.op.swap.text, { + percent: params?.percent || 0, + }), extraHeight: IS_ANDROID ? -65 : 10, }, op_rewards_position: { diff --git a/src/screens/rewards/RewardsSheet.tsx b/src/screens/rewards/RewardsSheet.tsx index facb3087ccc..ecae5ffc387 100644 --- a/src/screens/rewards/RewardsSheet.tsx +++ b/src/screens/rewards/RewardsSheet.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { SlackSheet } from '@/components/sheet'; +import { DynamicHeightSheet } from '@/components/sheet'; import { useDimensions } from '@/hooks'; import { BackgroundProvider, Box } from '@/design-system'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { RewardsContent } from '@/screens/rewards/components/RewardsContent'; -import { IS_ANDROID, IS_IOS } from '@/env'; +import { IS_ANDROID } from '@/env'; import { StatusBar } from 'react-native'; import { useRewards } from '@/resources/rewards/rewardsQuery'; import { useSelector } from 'react-redux'; @@ -49,14 +49,16 @@ export const RewardsSheet: React.FC = () => { {({ backgroundColor }) => ( // @ts-expect-error JS component - - + { isLoading={isLoading} /> - + )} ); diff --git a/src/screens/rewards/components/RewardsContent.tsx b/src/screens/rewards/components/RewardsContent.tsx index fd91d182f0d..43f5f69169d 100644 --- a/src/screens/rewards/components/RewardsContent.tsx +++ b/src/screens/rewards/components/RewardsContent.tsx @@ -1,14 +1,13 @@ import React from 'react'; import { RewardsTitle } from '@/screens/rewards/components/RewardsTitle'; import { RewardsEarnings } from '@/screens/rewards/components/RewardsEarnings'; -import { RewardsClaimed } from '@/screens/rewards/components/RewardsClaimed'; import { GetRewardsDataForWalletQuery } from '@/graphql/__generated__/metadata'; import { RewardsStats } from './RewardsStats'; -import { RewardsLeaderboard } from '@/screens/rewards/components/RewardsLeaderboard'; -import { RewardsDuneLogo } from '@/screens/rewards/components/RewardsDuneLogo'; import { RewardsFakeContent } from '@/screens/rewards/components/RewardsFakeContent'; import { RewardsProgramStatus } from '@/screens/rewards/components/RewardsProgramStatus'; import * as i18n from '@/languages'; +import { InfoAlert } from '@/components/info-alert/info-alert'; +import { Box, Text } from '@/design-system'; type Props = { assetPrice?: number; @@ -26,6 +25,7 @@ export const RewardsContent: React.FC = ({ if (isLoading) { return ; } + if (isLoadingError || !data || !data.rewards) { return ( = ({ /> ); } - const leaderboardData = data.rewards.leaderboard.accounts ?? []; return ( - <> + + + + 􀫸 + + } + /> + {data.rewards.earnings && ( = ({ totalEarnings={data.rewards.earnings.total} /> )} - - - - - + {data.rewards.stats && ( + + )} + ); }; diff --git a/src/screens/rewards/components/RewardsEarnings.tsx b/src/screens/rewards/components/RewardsEarnings.tsx index 95a687d7909..6b2a4a30e5b 100644 --- a/src/screens/rewards/components/RewardsEarnings.tsx +++ b/src/screens/rewards/components/RewardsEarnings.tsx @@ -119,14 +119,14 @@ export const RewardsEarnings: React.FC = ({ return ( - + - + diff --git a/src/screens/rewards/components/RewardsProgramStatus.tsx b/src/screens/rewards/components/RewardsProgramStatus.tsx index fef6b8491d1..005322d7fd9 100644 --- a/src/screens/rewards/components/RewardsProgramStatus.tsx +++ b/src/screens/rewards/components/RewardsProgramStatus.tsx @@ -1,7 +1,5 @@ import React from 'react'; import { Box, Stack, Text } from '@/design-system'; -import { useDimensions } from '@/hooks'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; type Props = { title: string; @@ -14,13 +12,11 @@ export const RewardsProgramStatus: React.FC = ({ text, title, }) => { - const { height } = useDimensions(); - const { top } = useSafeAreaInsets(); - return ( diff --git a/src/screens/rewards/components/RewardsSectionCard.tsx b/src/screens/rewards/components/RewardsSectionCard.tsx index fca01465749..a0061611d12 100644 --- a/src/screens/rewards/components/RewardsSectionCard.tsx +++ b/src/screens/rewards/components/RewardsSectionCard.tsx @@ -1,5 +1,8 @@ import React, { PropsWithChildren } from 'react'; -import { Box, Space } from '@/design-system'; +import { View } from 'react-native'; +import { Box, Space, globalColors, useBackgroundColor } from '@/design-system'; +import { IS_ANDROID, IS_IOS } from '@/env'; +import { useTheme } from '@/theme'; type Props = { paddingVertical?: Space; @@ -11,15 +14,52 @@ export function RewardsSectionCard({ paddingVertical = '20px', paddingHorizontal = '20px', }: PropsWithChildren) { + const { isDarkMode } = useTheme(); + + const bg = useBackgroundColor('surfaceSecondaryElevated'); + return ( - - {children} - + + + {children} + + + ); } diff --git a/src/screens/rewards/components/RewardsStats.tsx b/src/screens/rewards/components/RewardsStats.tsx index 1020ba22a0d..34461785cea 100644 --- a/src/screens/rewards/components/RewardsStats.tsx +++ b/src/screens/rewards/components/RewardsStats.tsx @@ -1,9 +1,7 @@ -import React from 'react'; -import { Bleed, Box, Inline, Stack, Text } from '@/design-system'; +import React, { useMemo } from 'react'; +import { Box, Stack, Text } from '@/design-system'; import * as i18n from '@/languages'; -import { ScrollView } from 'react-native'; import { RewardsStatsCard } from './RewardsStatsCard'; -import { capitalize } from 'lodash'; import { RewardStatsAction, RewardStatsActionType, @@ -17,41 +15,9 @@ import { import { useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; import { analyticsV2 } from '@/analytics'; -import { TextColor } from '@/design-system/color/palettes'; -import { CustomColor } from '@/design-system/color/useForegroundColor'; -import { STATS_TITLES } from '@/screens/rewards/constants'; - -const getPositionChangeSymbol = (positionChange: number) => { - if (positionChange > 0) { - return '􀑁'; - } - if (positionChange < 0) { - return '􁘳'; - } - return '􁘶'; -}; - -const getPositionChangeColor = ( - positionChange: number, - colors: { - up: TextColor | CustomColor; - down: TextColor | CustomColor; - noChange: TextColor | CustomColor; - } -): TextColor | CustomColor => { - if (positionChange > 0) { - return colors.up; - } - if (positionChange < 0) { - return colors.down; - } - return colors.noChange; -}; type Props = { assetPrice?: number; - position: number; - positionChange: number; actions: RewardStatsAction[]; color: string; }; @@ -59,95 +25,120 @@ type Props = { export const RewardsStats: React.FC = ({ assetPrice, actions, - position, - positionChange, color, }) => { const { navigate } = useNavigation(); const nativeCurrency = useSelector( (state: AppState) => state.settings.nativeCurrency ); - const navigateToPositionExplainer = () => { - analyticsV2.track(analyticsV2.event.rewardsPressedPositionCard, { - position, - }); - navigate(Routes.EXPLAIN_SHEET, { type: 'op_rewards_position' }); - }; + + const swapsData = actions.find( + action => action.type === RewardStatsActionType.Swap + ); + + const bridgeData = actions.find( + action => action.type === RewardStatsActionType.Bridge + ); + const getPressHandlerForType = (type: RewardStatsActionType) => { switch (type) { case RewardStatsActionType.Bridge: return () => { analyticsV2.track(analyticsV2.event.rewardsPressedBridgedCard); - navigate(Routes.EXPLAIN_SHEET, { type: 'op_rewards_bridge' }); + navigate(Routes.EXPLAIN_SHEET, { + type: 'op_rewards_bridge', + percent: bridgeData?.rewardPercent || 0, + }); }; case RewardStatsActionType.Swap: return () => { analyticsV2.track(analyticsV2.event.rewardsPressedSwappedCard); - navigate(Routes.EXPLAIN_SHEET, { type: 'op_rewards_swap' }); + navigate(Routes.EXPLAIN_SHEET, { + type: 'op_rewards_swap', + percent: swapsData?.rewardPercent || 0, + }); }; default: return () => {}; } }; + + const getSwapsValue = useMemo(() => { + if (assetPrice) { + return convertAmountAndPriceToNativeDisplay( + swapsData?.amount?.token || '0', + assetPrice, + nativeCurrency + ).display; + } + + return convertAmountToNativeDisplay(swapsData?.amount?.usd || '0', 'USD'); + }, [ + assetPrice, + nativeCurrency, + swapsData?.amount?.token, + swapsData?.amount?.usd, + ]); + + const getBridgeValue = useMemo(() => { + if (assetPrice) { + return convertAmountAndPriceToNativeDisplay( + bridgeData?.amount?.token ?? '0', + assetPrice, + nativeCurrency + ).display; + } + + return convertAmountToNativeDisplay(bridgeData?.amount?.usd || '0', 'USD'); + }, [ + assetPrice, + nativeCurrency, + bridgeData?.amount?.token, + bridgeData?.amount?.usd, + ]); + return ( - - + + {i18n.t(i18n.l.rewards.my_stats)} - - - - - {actions.map(action => { - const value = - assetPrice !== undefined - ? convertAmountAndPriceToNativeDisplay( - action.amount.token, - assetPrice, - nativeCurrency - ).display - : convertAmountToNativeDisplay(action.amount.usd, 'USD'); - return ( - - ); - })} - - - + + + + + + + + + ); diff --git a/src/screens/rewards/components/RewardsStatsCard.tsx b/src/screens/rewards/components/RewardsStatsCard.tsx index 07d675ae59d..4ea4752e247 100644 --- a/src/screens/rewards/components/RewardsStatsCard.tsx +++ b/src/screens/rewards/components/RewardsStatsCard.tsx @@ -12,7 +12,7 @@ type Props = { secondaryValue: string; secondaryValueIcon: string; secondaryValueColor: TextColor | CustomColor; - onPress: () => void; + onPress?: () => void; }; export const RewardsStatsCard: React.FC = ({ @@ -26,7 +26,12 @@ export const RewardsStatsCard: React.FC = ({ const infoIconColor = useInfoIconColor(); return ( - +