Skip to content

Commit bf9b51e

Browse files
committed
feature: tier progress bar and membership tier card redesign
1 parent 9d590f6 commit bf9b51e

File tree

6 files changed

+306
-26
lines changed

6 files changed

+306
-26
lines changed

src/components/gradient-border/GradientBorderView.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ type GradientBorderViewProps = {
1717
start?: { x: number; y: number };
1818
end?: { x: number; y: number };
1919
borderRadius?: number;
20+
borderTopLeftRadius?: number;
21+
borderTopRightRadius?: number;
22+
borderBottomLeftRadius?: number;
23+
borderBottomRightRadius?: number;
2024
backgroundColor?: string;
2125
style?: StyleProp<ViewStyle>;
2226
};
@@ -29,13 +33,24 @@ export const GradientBorderView = memo(function GradientBorderView({
2933
end = DEFAULT_END,
3034
borderWidth = THICK_BORDER_WIDTH,
3135
borderRadius = DEFAULT_BORDER_RADIUS,
36+
borderTopLeftRadius,
37+
borderTopRightRadius,
38+
borderBottomLeftRadius,
39+
borderBottomRightRadius,
3240
style,
3341
backgroundColor = DEFAULT_BACKGROUND_COLOR,
3442
}: GradientBorderViewProps) {
43+
const radiusStyle = {
44+
borderTopLeftRadius: borderTopLeftRadius ?? borderRadius,
45+
borderTopRightRadius: borderTopRightRadius ?? borderRadius,
46+
borderBottomLeftRadius: borderBottomLeftRadius ?? borderRadius,
47+
borderBottomRightRadius: borderBottomRightRadius ?? borderRadius,
48+
};
49+
3550
return (
36-
<View style={[styles.baseStyle, style, { backgroundColor, borderRadius }]}>
51+
<View style={[styles.baseStyle, style, { backgroundColor }, radiusStyle]}>
3752
<MaskedView
38-
maskElement={<View style={[styles.maskElement, { borderWidth, borderRadius }]} />}
53+
maskElement={<View style={[styles.maskElement, { borderWidth }, radiusStyle]} />}
3954
style={styles.maskView}
4055
pointerEvents="none"
4156
>
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { memo, useMemo } from 'react';
2+
import { StyleSheet, View } from 'react-native';
3+
import { InnerShadow } from '@/features/polymarket/components/InnerShadow';
4+
import { useBackgroundColor, useColorMode } from '@/design-system';
5+
import { GradientBorderView } from '@/components/gradient-border/GradientBorderView';
6+
import { opacity } from '@/framework/ui/utils/opacity';
7+
import { useMembershipTierInfo } from '@/features/rnbw-membership/stores/derived/useMembershipTierInfo';
8+
import { TIER_VISUALS } from '@/features/rnbw-membership/constants';
9+
import { getValueForColorMode } from '@/design-system/color/palettes';
10+
import { LinearGradient } from 'expo-linear-gradient';
11+
import { Blur, Canvas, RoundedRect } from '@shopify/react-native-skia';
12+
import type { Tier } from '@/features/rnbw-membership/types';
13+
14+
const PROGRESS_BAR_HORIZONTAL_PADDING = 12;
15+
const PROGRESS_HIGHLIGHT_INSET = 5;
16+
const PROGRESS_HIGHLIGHT_HEIGHT = 5;
17+
const PROGRESS_HIGHLIGHT_RADIUS = 2.5;
18+
const PROGRESS_HIGHLIGHT_BLUR = 2;
19+
const PROGRESS_HIGHLIGHT_BLUR_PADDING = PROGRESS_HIGHLIGHT_BLUR * 2;
20+
21+
type TierProgressBarProps = {
22+
width: number;
23+
height: number;
24+
tier?: Tier;
25+
tierIndex?: number;
26+
tierProgress?: number;
27+
};
28+
29+
export const TierProgressBar = memo(function TierProgressBar({ width, height, tier, tierIndex, tierProgress }: TierProgressBarProps) {
30+
const { isDarkMode, colorMode } = useColorMode();
31+
const surfaceSecondary = useBackgroundColor('surfaceSecondary');
32+
const backgroundColor = isDarkMode ? '#242529' : surfaceSecondary;
33+
34+
const { currentTier, currentTierIndex, currentTierProgress, allTiers } = useMembershipTierInfo();
35+
const resolvedTier = tier ?? currentTier;
36+
const resolvedTierIndex = tierIndex ?? currentTierIndex;
37+
const resolvedTierProgress = tierProgress ?? currentTierProgress;
38+
const isMaxTier = resolvedTierIndex === allTiers.length - 1;
39+
40+
const { gradient, borderGradient, shadow } = useMemo(() => {
41+
const visuals = TIER_VISUALS[resolvedTier.level];
42+
return {
43+
gradient: getValueForColorMode(visuals.progressBarGradient, colorMode),
44+
borderGradient: getValueForColorMode(visuals.progressBarBorderGradient, colorMode),
45+
shadow: getValueForColorMode(visuals.progressBarShadow, colorMode),
46+
};
47+
}, [resolvedTier.level, colorMode]);
48+
49+
const tierMarkerPositions = useMemo(() => {
50+
const tierCount = allTiers.length;
51+
const usableWidth = width - PROGRESS_BAR_HORIZONTAL_PADDING * 2;
52+
return allTiers.map((_, i) => PROGRESS_BAR_HORIZONTAL_PADDING * 2 + (i * usableWidth) / (tierCount - 1));
53+
}, [allTiers, width]);
54+
55+
const fillWidth = useMemo(() => {
56+
const base = tierMarkerPositions[resolvedTierIndex];
57+
const next = tierMarkerPositions[resolvedTierIndex + 1] ?? base;
58+
return base + resolvedTierProgress * (next - base);
59+
}, [tierMarkerPositions, resolvedTierIndex, resolvedTierProgress]);
60+
61+
const { leftRadius, rightRadius } = useMemo(() => {
62+
const full = height / 2;
63+
return { leftRadius: full, rightRadius: isMaxTier ? full : 8 };
64+
}, [height, isMaxTier]);
65+
66+
const progressHighlight = useMemo(() => {
67+
const rectWidth = fillWidth - PROGRESS_HIGHLIGHT_INSET * 2;
68+
69+
// Blur needs extra canvas area, otherwise Skia clips the softened edges.
70+
return {
71+
canvasLeft: PROGRESS_HIGHLIGHT_INSET - PROGRESS_HIGHLIGHT_BLUR_PADDING,
72+
canvasTop: PROGRESS_HIGHLIGHT_INSET - PROGRESS_HIGHLIGHT_BLUR_PADDING,
73+
canvasWidth: rectWidth + PROGRESS_HIGHLIGHT_BLUR_PADDING * 2,
74+
canvasHeight: PROGRESS_HIGHLIGHT_HEIGHT + PROGRESS_HIGHLIGHT_BLUR_PADDING * 2,
75+
rectX: PROGRESS_HIGHLIGHT_BLUR_PADDING,
76+
rectY: PROGRESS_HIGHLIGHT_BLUR_PADDING,
77+
rectWidth,
78+
};
79+
}, [fillWidth]);
80+
81+
return (
82+
<GradientBorderView
83+
borderGradientColors={isDarkMode ? [opacity('#9AA2A7', 0.016), opacity('#9AA2A7', 0.08)] : ['rgba(0,0,0,0)', 'rgba(0,0,0,0)']}
84+
borderWidth={1}
85+
start={{ x: 0, y: 0 }}
86+
end={{ x: 0, y: 1 }}
87+
backgroundColor={backgroundColor}
88+
style={{ height, width, overflow: 'visible' }}
89+
>
90+
<View style={styles.dotsContainer}>
91+
{allTiers.map(t => (
92+
<TierDot key={t.level} />
93+
))}
94+
</View>
95+
<GradientBorderView
96+
borderGradientColors={borderGradient.colors}
97+
start={{ x: 0, y: 0 }}
98+
end={{ x: 0, y: 1 }}
99+
borderWidth={1}
100+
borderTopRightRadius={rightRadius}
101+
borderBottomRightRadius={rightRadius}
102+
borderTopLeftRadius={leftRadius}
103+
borderBottomLeftRadius={leftRadius}
104+
style={{
105+
height,
106+
width: fillWidth,
107+
position: 'absolute',
108+
top: 0,
109+
left: 0,
110+
overflow: 'visible',
111+
...shadow,
112+
}}
113+
>
114+
<LinearGradient
115+
colors={gradient.colors}
116+
start={gradient.start}
117+
end={gradient.end}
118+
style={[
119+
StyleSheet.absoluteFill,
120+
{
121+
borderTopRightRadius: rightRadius,
122+
borderBottomRightRadius: rightRadius,
123+
borderTopLeftRadius: leftRadius,
124+
borderBottomLeftRadius: leftRadius,
125+
borderCurve: 'continuous',
126+
},
127+
]}
128+
/>
129+
<Canvas
130+
style={{
131+
position: 'absolute',
132+
top: progressHighlight.canvasTop,
133+
left: progressHighlight.canvasLeft,
134+
height: progressHighlight.canvasHeight,
135+
width: progressHighlight.canvasWidth,
136+
}}
137+
>
138+
<RoundedRect
139+
x={progressHighlight.rectX}
140+
y={progressHighlight.rectY}
141+
width={progressHighlight.rectWidth}
142+
height={PROGRESS_HIGHLIGHT_HEIGHT}
143+
r={PROGRESS_HIGHLIGHT_RADIUS}
144+
color="rgba(255,255,255,0.4)"
145+
>
146+
<Blur blur={PROGRESS_HIGHLIGHT_BLUR} />
147+
</RoundedRect>
148+
</Canvas>
149+
</GradientBorderView>
150+
<InnerShadow
151+
width={width}
152+
height={height}
153+
borderRadius={height / 2}
154+
color={isDarkMode ? opacity('#000000', 0.43) : opacity('#7A7A7A', 0.13)}
155+
blur={2}
156+
dx={0}
157+
dy={1}
158+
/>
159+
</GradientBorderView>
160+
);
161+
});
162+
163+
const DOT_SIZE = 8;
164+
const DOT_RADIUS = DOT_SIZE / 2;
165+
166+
function TierDot() {
167+
const { isDarkMode } = useColorMode();
168+
return (
169+
<View style={[styles.dot, { backgroundColor: isDarkMode ? opacity('#FFFFFF', 0.05) : '#E3E3E3' }]}>
170+
{!isDarkMode && (
171+
<InnerShadow width={DOT_SIZE} height={DOT_SIZE} borderRadius={DOT_RADIUS} color={opacity('#7A7A7A', 0.13)} blur={2} dx={0} dy={1} />
172+
)}
173+
</View>
174+
);
175+
}
176+
177+
const styles = StyleSheet.create({
178+
dotsContainer: {
179+
flexDirection: 'row',
180+
height: '100%',
181+
width: '100%',
182+
paddingHorizontal: PROGRESS_BAR_HORIZONTAL_PADDING,
183+
justifyContent: 'space-between',
184+
alignItems: 'center',
185+
},
186+
dot: {
187+
width: DOT_SIZE,
188+
height: DOT_SIZE,
189+
borderRadius: DOT_RADIUS,
190+
overflow: 'hidden',
191+
},
192+
});

src/features/rnbw-membership/constants.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ type TierVisuals = {
3333
badgeBorderGradient: Themed<GradientConfig>;
3434
badgeShadow: Themed<ShadowConfig>;
3535
badgeTextShadow: Themed<TextShadowConfig>;
36+
progressBarGradient: Themed<GradientConfig>;
37+
progressBarBorderGradient: Themed<GradientConfig>;
38+
progressBarShadow: Themed<ShadowConfig>;
3639
};
3740

3841
export const TIER_VISUALS: Record<TierId, TierVisuals> = {
@@ -56,6 +59,15 @@ export const TIER_VISUALS: Record<TierId, TierVisuals> = {
5659
light: { textShadowColor: 'rgba(0, 0, 0, 0.14)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 1 },
5760
dark: { textShadowColor: 'rgba(0, 0, 0, 0.14)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 1 },
5861
},
62+
progressBarGradient: { light: { colors: ['#2B80F4', '#1F6FDC'] }, dark: { colors: ['#2B80F4', '#1F6FDC'] } },
63+
progressBarBorderGradient: {
64+
light: { colors: ['rgba(0,0,0,0.06)', 'rgba(0,0,0,0.06)'] },
65+
dark: { colors: ['rgba(255,255,255,0.12)', 'rgba(255,255,255,0.12)'] },
66+
},
67+
progressBarShadow: {
68+
light: { shadowColor: '#2A99FA', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.5, shadowRadius: 2.5 },
69+
dark: { shadowColor: '#2A99FA', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.5, shadowRadius: 2.5 },
70+
},
5971
},
6072
STAKING_TIER_LEVEL_SILVER: {
6173
backgroundGradient: {
@@ -77,6 +89,15 @@ export const TIER_VISUALS: Record<TierId, TierVisuals> = {
7789
light: { textShadowColor: 'rgba(255, 255, 255, 0.46)', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 0 },
7890
dark: { textShadowColor: 'rgba(255, 255, 255, 0.46)', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 0 },
7991
},
92+
progressBarGradient: { light: { colors: ['#EFEFEF', '#A2A2A2'] }, dark: { colors: ['#EFEFEF', '#A2A2A2'] } },
93+
progressBarBorderGradient: {
94+
light: { colors: [opacity('#959595', 0.15), opacity('#3B3B3B', 0.09)] },
95+
dark: { colors: ['rgba(255,255,255,0.3)', 'rgba(255,255,255,0.3)'] },
96+
},
97+
progressBarShadow: {
98+
light: { shadowColor: '#000000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 6 },
99+
dark: { shadowColor: '#B2B2B2', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.3, shadowRadius: 15 },
100+
},
80101
},
81102
STAKING_TIER_LEVEL_GOLD: {
82103
backgroundGradient: {
@@ -98,6 +119,15 @@ export const TIER_VISUALS: Record<TierId, TierVisuals> = {
98119
light: { textShadowColor: 'rgba(255, 255, 255, 0.26)', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 0 },
99120
dark: { textShadowColor: 'rgba(255, 255, 255, 0.26)', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 0 },
100121
},
122+
progressBarGradient: { light: { colors: ['#FAD96A', '#E7B114'] }, dark: { colors: ['#FAD96A', '#E7B114'] } },
123+
progressBarBorderGradient: {
124+
light: { colors: ['rgba(0,0,0,0.06)', 'rgba(0,0,0,0.06)'] },
125+
dark: { colors: ['rgba(255,255,255,0.3)', 'rgba(255,255,255,0.3)'] },
126+
},
127+
progressBarShadow: {
128+
light: { shadowColor: '#E5B92C', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.5, shadowRadius: 2.5 },
129+
dark: { shadowColor: '#FFDD20', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.3, shadowRadius: 15 },
130+
},
101131
},
102132
STAKING_TIER_LEVEL_DIAMOND: {
103133
backgroundGradient: {
@@ -119,6 +149,15 @@ export const TIER_VISUALS: Record<TierId, TierVisuals> = {
119149
light: { textShadowColor: opacity('#CBF6FF', 0.46), textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 0 },
120150
dark: { textShadowColor: opacity('#CBF6FF', 0.46), textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 0 },
121151
},
152+
progressBarGradient: { light: { colors: ['#ECF2F8', '#C8D1D7'] }, dark: { colors: ['#D5DCE3', '#9AA7B0'] } },
153+
progressBarBorderGradient: {
154+
light: { colors: [opacity('#6C8CA8', 0.15), opacity('#404647', 0.09)] },
155+
dark: { colors: [opacity('#959595', 0.15), opacity('#3B3B3B', 0.06)] },
156+
},
157+
progressBarShadow: {
158+
light: { shadowColor: '#000000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 6 },
159+
dark: { shadowColor: '#42D2EB', shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.3, shadowRadius: 15 },
160+
},
122161
},
123162
STAKING_TIER_LEVEL_BLACK: {
124163
backgroundGradient: {
@@ -143,6 +182,15 @@ export const TIER_VISUALS: Record<TierId, TierVisuals> = {
143182
light: { textShadowColor: 'rgba(0, 0, 0, 0)', textShadowOffset: { width: 0, height: 0 }, textShadowRadius: 0 },
144183
dark: { textShadowColor: 'rgba(0, 0, 0, 0)', textShadowOffset: { width: 0, height: 0 }, textShadowRadius: 0 },
145184
},
185+
progressBarGradient: { light: { colors: ['#444444', '#000000'] }, dark: { colors: ['#444444', '#000000'] } },
186+
progressBarBorderGradient: {
187+
light: { colors: ['rgba(0,0,0,0)', 'rgba(0,0,0,0)'] },
188+
dark: { colors: ['rgba(0,0,0,0)', 'rgba(0,0,0,0)'] },
189+
},
190+
progressBarShadow: {
191+
light: { shadowColor: '#000000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 6 },
192+
dark: { shadowColor: '#000000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 6 },
193+
},
146194
},
147195
};
148196

src/features/rnbw-membership/screens/rnbw-membership-screen/components/MembershipTierCard.tsx

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,48 @@
11
import { memo } from 'react';
2-
import { Box, Text } from '@/design-system';
2+
import { Box, Separator, Text } from '@/design-system';
33
import { useMembershipTierInfo } from '@/features/rnbw-membership/stores/derived/useMembershipTierInfo';
44
import ButtonPressAnimation from '@/components/animations/ButtonPressAnimation';
55
import Navigation from '@/navigation/Navigation';
66
import Routes from '@/navigation/routesNames';
7+
import { MembershipCard } from '@/features/rnbw-membership/screens/rnbw-membership-screen/components/MembershipCard';
8+
import { TierThemedLabel } from '@/features/rnbw-membership/components/TierThemedLabel';
9+
import { TierProgressBar } from '@/features/rnbw-membership/components/TierProgressBar';
10+
import { RNBW_SYMBOL } from '@/features/rnbw-rewards/constants';
711

812
export const MembershipTierCard = memo(function MembershipTierCard() {
9-
const { currentTier, stakeRequiredForNextTier, cashbackPercentage, currentTierProgress } = useMembershipTierInfo();
13+
const { currentTier, stakeRequiredForNextTier, cashbackPercentage } = useMembershipTierInfo();
1014

1115
return (
1216
<ButtonPressAnimation onPress={navigateToMembershipTiersSheet} scaleTo={0.96}>
13-
<Box background="surfacePrimary" borderRadius={24} padding="20px" gap={20} shadow={'18px'}>
14-
<Text size="22pt" weight="heavy" color="label">
15-
{`${currentTier.name} Tier`}
16-
</Text>
17-
<Text size="17pt" weight="heavy" color="label">
18-
{`current tier progress: ${currentTierProgress * 100}/100`}
19-
</Text>
20-
<Text size="17pt" weight="heavy" color="label">
21-
{`Cashback: ${cashbackPercentage}%`}
22-
</Text>
23-
<Text size="17pt" weight="heavy" color="label">
24-
{`Stake to unlock: ${stakeRequiredForNextTier}`}
25-
</Text>
26-
</Box>
17+
<MembershipCard paddingHorizontal="20px" paddingVertical="24px">
18+
<Box gap={16}>
19+
<TierThemedLabel tier={currentTier}>
20+
<Text size="22pt" weight="heavy" color="label">
21+
{`${currentTier.name} Tier`}
22+
</Text>
23+
</TierThemedLabel>
24+
<TierProgressBar width={300} height={24} />
25+
<Box gap={16} paddingHorizontal={'4px'}>
26+
<Box flexDirection="row" justifyContent="space-between">
27+
<Text size="17pt" weight="semibold" color="labelTertiary">
28+
{'Rewards'}
29+
</Text>
30+
<Text size="17pt" weight="bold" color="label">
31+
{`${cashbackPercentage}%`}
32+
</Text>
33+
</Box>
34+
<Separator color="separatorTertiary" thickness={1} />
35+
<Box flexDirection="row" justifyContent="space-between">
36+
<Text size="17pt" weight="semibold" color="labelTertiary">
37+
{'Stake to next tier'}
38+
</Text>
39+
<Text size="17pt" weight="bold" color="label">
40+
{`${stakeRequiredForNextTier} ${RNBW_SYMBOL}`}
41+
</Text>
42+
</Box>
43+
</Box>
44+
</Box>
45+
</MembershipCard>
2746
</ButtonPressAnimation>
2847
);
2948
});

0 commit comments

Comments
 (0)