diff --git a/src/features/rnbw-membership/components/MembershipTierButton/MembershipTierButtonSurface.tsx b/src/features/rnbw-membership/components/MembershipTierButton/MembershipTierButtonSurface.tsx
index 734391b8204..7cc77ab53cf 100644
--- a/src/features/rnbw-membership/components/MembershipTierButton/MembershipTierButtonSurface.tsx
+++ b/src/features/rnbw-membership/components/MembershipTierButton/MembershipTierButtonSurface.tsx
@@ -7,6 +7,7 @@ import { GradientBorderView } from '@/components/gradient-border/GradientBorderV
import { useColorMode } from '@/design-system';
import { getValueForColorMode } from '@/design-system/color/palettes';
import { InnerShadow } from '@/features/polymarket/components/InnerShadow';
+import { RainbowShimmerFill } from '@/features/rnbw-membership/components/RainbowShimmerFill';
import { ShadowLayers } from '@/features/rnbw-membership/components/ShadowLayers';
import { MEMBERSHIP_CARD_BACKGROUND_COLOR } from '@/features/rnbw-membership/membershipCardTheme';
import { getTierPrimaryButtonTheme, getTierSecondaryButtonTheme } from '@/features/rnbw-membership/tierVisuals';
@@ -39,7 +40,7 @@ export const MembershipTierButtonSurface = memo(function MembershipTierButtonSur
const borderRadius = height / 2;
const shadowLayerBackgroundColor = getValueForColorMode(MEMBERSHIP_CARD_BACKGROUND_COLOR, colorMode);
- const { borderGradient, surfaceGradient, surfaceHighlight, shadows } = useMemo(() => {
+ const { borderGradient, surfaceGradient, surfaceHighlight, shadows, overlay } = useMemo(() => {
const surfaceTheme =
variant === 'primary' ? getTierPrimaryButtonTheme(tier.level).surface : getTierSecondaryButtonTheme(tier.level).surface;
@@ -48,6 +49,7 @@ export const MembershipTierButtonSurface = memo(function MembershipTierButtonSur
surfaceGradient: getValueForColorMode(surfaceTheme.fill, colorMode),
surfaceHighlight: surfaceTheme.highlight ? getValueForColorMode(surfaceTheme.highlight, colorMode) : null,
shadows: getValueForColorMode(surfaceTheme.shadows, colorMode),
+ overlay: surfaceTheme.overlay ? getValueForColorMode(surfaceTheme.overlay, colorMode) : null,
};
}, [tier.level, variant, colorMode]);
@@ -66,13 +68,24 @@ export const MembershipTierButtonSurface = memo(function MembershipTierButtonSur
end={borderGradient.end ?? GRADIENT_END}
style={resolvedContainerStyle}
>
-
+ {overlay ? (
+
+ ) : (
+
+ )}
{surfaceHighlight && (
(null);
+
+ const onLayout = useCallback((e: { nativeEvent: { layout: { width: number; height: number } } }) => {
+ const { width: w, height: h } = e.nativeEvent.layout;
+ setMeasured(prev => (prev && prev.width === w && prev.height === h ? prev : { width: w, height: h }));
+ }, []);
+
+ const width = measured?.width ?? 0;
+ const height = measured?.height ?? 0;
+ const canDisplay = width > 0 && height > 0;
+
+ // Skia wants mutable arrays
+ const skiaFillColors = useMemo(() => [...fillColors], [fillColors]);
+ const skiaFillLocations = useMemo(() => (fillLocations ? [...fillLocations] : undefined), [fillLocations]);
+ const skiaOverlayColors = useMemo(() => [...overlay.colors], [overlay.colors]);
+ const skiaOverlayLocations = useMemo(() => [...overlay.locations], [overlay.locations]);
+
+ return (
+
+ {canDisplay && (
+
+ )}
+
+ );
+});
diff --git a/src/features/rnbw-membership/components/ShadowLayers.tsx b/src/features/rnbw-membership/components/ShadowLayers.tsx
index f301b1ced82..60ede92d8c5 100644
--- a/src/features/rnbw-membership/components/ShadowLayers.tsx
+++ b/src/features/rnbw-membership/components/ShadowLayers.tsx
@@ -13,6 +13,7 @@ type ShadowLayersProps = {
borderRadius: number;
backgroundColor: string;
children: ReactNode;
+ outerGlowLayer?: ReactNode;
style?: StyleProp;
};
@@ -30,9 +31,17 @@ function getShadowLayerStyle(shadowStyle: ShadowStyle): ViewStyle {
return shadowStyle;
}
-export const ShadowLayers = memo(function ShadowLayers({ shadows, borderRadius, backgroundColor, children, style }: ShadowLayersProps) {
+export const ShadowLayers = memo(function ShadowLayers({
+ shadows,
+ borderRadius,
+ backgroundColor,
+ children,
+ outerGlowLayer,
+ style,
+}: ShadowLayersProps) {
return (
+ {outerGlowLayer}
{shadows.map((shadowStyle, index) => (
(null);
+
+ const dx = shadow.dx ?? 0;
+ const dy = shadow.dy ?? 0;
+ const start = shadow.start ?? DEFAULT_START;
+ const end = shadow.end ?? DEFAULT_END;
+ const padding = useMemo(
+ () => PixelRatio.roundToNearestPixel(shadow.blur * 2 + Math.max(Math.abs(dx), Math.abs(dy))),
+ [shadow.blur, dx, dy]
+ );
+
+ const skiaColors = useMemo(() => [...shadow.colors], [shadow.colors]);
+ const skiaLocations = useMemo(() => (shadow.locations ? [...shadow.locations] : undefined), [shadow.locations]);
+
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
+ const nextLayout = {
+ width: PixelRatio.roundToNearestPixel(event.nativeEvent.layout.width),
+ height: PixelRatio.roundToNearestPixel(event.nativeEvent.layout.height),
+ };
+
+ setLayout(current => (current && current.width === nextLayout.width && current.height === nextLayout.height ? current : nextLayout));
+ }, []);
+
+ const width = layout?.width ?? 0;
+ const height = layout?.height ?? 0;
+ const canRender = width > 0 && height > 0;
+
+ return (
+
+ {canRender && (
+
+ )}
+
+ );
+});
+
+const styles = StyleSheet.create({
+ canvas: {
+ position: 'absolute',
+ },
+});
diff --git a/src/features/rnbw-membership/components/TierBadge.tsx b/src/features/rnbw-membership/components/TierBadge.tsx
index 411e1b46118..b83b5a8a5a9 100644
--- a/src/features/rnbw-membership/components/TierBadge.tsx
+++ b/src/features/rnbw-membership/components/TierBadge.tsx
@@ -9,7 +9,9 @@ import { Text, useColorMode } from '@/design-system';
import { getValueForColorMode, globalColors } from '@/design-system/color/palettes';
import type { TextSize, TextWeight } from '@/design-system/components/Text/Text';
import { InnerShadow } from '@/features/polymarket/components/InnerShadow';
+import { RainbowShimmerFill } from '@/features/rnbw-membership/components/RainbowShimmerFill';
import { ShadowLayers } from '@/features/rnbw-membership/components/ShadowLayers';
+import { SkiaGradientShadow } from '@/features/rnbw-membership/components/SkiaGradientShadow';
import { getTierBadgeTheme } from '@/features/rnbw-membership/tierVisuals';
import type { Tier as TierType } from '@/features/rnbw-membership/types';
import { opacity } from '@/framework/ui/utils/opacity';
@@ -36,7 +38,14 @@ export const TierBadge = memo(function TierBadge({
}) {
const { colorMode } = useColorMode();
const borderRadius = height / 2;
- const { fill: badgeGradient, text, shadows: badgeShadows, border: badgeBorderGradient } = getTierBadgeTheme(tier.level);
+ const {
+ fill: badgeGradient,
+ text,
+ shadows: badgeShadows,
+ border: badgeBorderGradient,
+ overlay,
+ backgroundShadow,
+ } = getTierBadgeTheme(tier.level);
const {
colors: badgeGradientColors,
locations: badgeGradientLocations,
@@ -57,6 +66,8 @@ export const TierBadge = memo(function TierBadge({
} = getValueForColorMode(badgeBorderGradient, colorMode);
const shadowStyles = getValueForColorMode(badgeShadows, colorMode);
const textShadowStyle = getValueForColorMode(text.shadow, colorMode);
+ const resolvedOverlay = overlay ? getValueForColorMode(overlay, colorMode) : null;
+ const resolvedBackgroundShadow = backgroundShadow ? getValueForColorMode(backgroundShadow, colorMode) : null;
const [firstBadgeGradientColor] = badgeGradientColors;
const shadowLayerBackgroundColor = typeof firstBadgeGradientColor === 'string' ? firstBadgeGradientColor : '#FFFFFF';
@@ -65,6 +76,9 @@ export const TierBadge = memo(function TierBadge({
shadows={shadowStyles}
borderRadius={borderRadius}
backgroundColor={shadowLayerBackgroundColor}
+ outerGlowLayer={
+ resolvedBackgroundShadow ? : null
+ }
style={styles.shadowStack}
>
-
+ {resolvedOverlay ? (
+
+ ) : (
+
+ )}
{
+ const { gradient, borderGradient, shadow, highlightGradient, backgroundShadow } = useMemo(() => {
const progressTheme = getTierProgressBarTheme(tier.level);
return {
gradient: getValueForColorMode(progressTheme.fill, colorMode),
borderGradient: getValueForColorMode(progressTheme.border, colorMode),
shadow: getValueForColorMode(progressTheme.shadow, colorMode),
highlightGradient: getValueForColorMode(progressTheme.highlight, colorMode),
+ backgroundShadow: progressTheme.backgroundShadow ? getValueForColorMode(progressTheme.backgroundShadow, colorMode) : null,
};
}, [tier.level, colorMode]);
@@ -82,6 +84,13 @@ export const TierProgressBar = memo(function TierProgressBar({
};
}, [fillWidth]);
+ // Skia wants mutable arrays
+ const skiaHighlightColors = useMemo(() => [...highlightGradient.colors], [highlightGradient.colors]);
+ const skiaHighlightLocations = useMemo(
+ () => (highlightGradient.locations ? [...highlightGradient.locations] : undefined),
+ [highlightGradient.locations]
+ );
+
return (
-
-
- : null}
+
-
+
-
+
+
+
+
+
+
+
= { light: T; dark: T };
+export type GradientColors = readonly [string, string, ...string[]];
+export type GradientLocations = readonly [number, number, ...number[]];
type Gradient = {
- colors: LinearGradientProps['colors'];
- locations?: LinearGradientProps['locations'];
+ colors: GradientColors;
+ locations?: GradientLocations;
start?: Point;
end?: Point;
};
@@ -30,6 +30,27 @@ type TextShadow = {
textShadowRadius: number;
};
+export type HardLightOverlay = {
+ colors: GradientColors;
+ locations: GradientLocations;
+ maskShadow: {
+ blur: number;
+ dx: number;
+ dy: number;
+ };
+};
+
+export type GradientShadow = {
+ colors: GradientColors;
+ locations?: GradientLocations;
+ start?: Point;
+ end?: Point;
+ opacity: number;
+ blur: number;
+ dx?: number;
+ dy?: number;
+};
+
type GradientTextTheme = {
gradient: Themed;
shadow: Themed;
@@ -40,6 +61,8 @@ type BadgeTheme = {
border: Themed;
shadows: Themed;
text: GradientTextTheme;
+ overlay?: Themed;
+ backgroundShadow?: Themed;
};
type ButtonSurfaceTheme = {
@@ -47,6 +70,7 @@ type ButtonSurfaceTheme = {
border: Themed;
shadows: Themed;
highlight?: Themed;
+ overlay?: Themed;
};
type PrimaryButtonTheme = {
@@ -64,6 +88,7 @@ type ProgressBarTheme = {
border: Themed;
shadow: Themed;
highlight: Themed;
+ backgroundShadow?: Themed;
};
type TierTheme = {
@@ -80,6 +105,7 @@ type PrimaryButtonConfig = {
border?: Themed;
shadows?: Themed;
highlight?: Themed;
+ overlay?: Themed;
text?: {
gradient?: Themed;
shadow?: Themed;
@@ -90,6 +116,7 @@ type SecondaryButtonConfig = {
fill: Themed;
border?: Themed;
shadows?: Themed;
+ overlay?: Themed;
textColor: Themed;
};
@@ -119,12 +146,16 @@ const textShadow = (textShadowColor: string, textShadowOffset: TextShadow['textS
textShadowRadius,
});
+function isShadowArray(value: Shadow | readonly Shadow[]): value is readonly Shadow[] {
+ return Array.isArray(value);
+}
+
function toShadowArray(value: Shadow | readonly Shadow[]): readonly Shadow[] {
- if (Array.isArray(value)) {
- return value as readonly Shadow[];
+ if (isShadowArray(value)) {
+ return value;
}
- return [value as Shadow];
+ return [value];
}
const themedShadows = (light: Shadow | readonly Shadow[], dark?: Shadow | readonly Shadow[]): Themed =>
@@ -133,27 +164,34 @@ const themedShadows = (light: Shadow | readonly Shadow[], dark?: Shadow | readon
const background = (lightColor: string, darkColor = lightColor): Themed =>
themed(gradient([opacity(lightColor, 0.4), opacity(lightColor, 0)]), gradient([opacity(darkColor, 0.16), opacity(darkColor, 0)]));
-const badgePrimaryButton = (badge: BadgeTheme, config: PrimaryButtonConfig = {}): PrimaryButtonTheme => ({
- surface: {
- fill: config.fill ?? badge.fill,
- border: config.border ?? badge.border,
- shadows: config.shadows ?? badge.shadows,
- ...(config.highlight ? { highlight: config.highlight } : {}),
- },
- text: {
- gradient: config.text?.gradient ?? badge.text.gradient,
- shadow: config.text?.shadow ?? badge.text.shadow,
- },
-});
+const badgePrimaryButton = (badge: BadgeTheme, config: PrimaryButtonConfig = {}): PrimaryButtonTheme => {
+ const overlay = config.overlay ?? badge.overlay;
+ return {
+ surface: {
+ fill: config.fill ?? badge.fill,
+ border: config.border ?? badge.border,
+ shadows: config.shadows ?? badge.shadows,
+ ...(config.highlight ? { highlight: config.highlight } : {}),
+ ...(overlay ? { overlay } : {}),
+ },
+ text: {
+ gradient: config.text?.gradient ?? badge.text.gradient,
+ shadow: config.text?.shadow ?? badge.text.shadow,
+ },
+ };
+};
-const secondaryButton = (badge: BadgeTheme, config: SecondaryButtonConfig): SecondaryButtonTheme => ({
- surface: {
- fill: config.fill,
- border: config.border ?? badge.border,
- shadows: config.shadows ?? badge.shadows,
- },
- textColor: config.textColor,
-});
+const secondaryButton = (badge: BadgeTheme, config: SecondaryButtonConfig): SecondaryButtonTheme => {
+ return {
+ surface: {
+ fill: config.fill,
+ border: config.border ?? badge.border,
+ shadows: config.shadows ?? badge.shadows,
+ ...(config.overlay ? { overlay: config.overlay } : {}),
+ },
+ textColor: config.textColor,
+ };
+};
// ============ Shared Tokens ================================================== //
@@ -161,6 +199,30 @@ const LIGHT_BADGE_BORDER_COLORS: Gradient['colors'] = [opacity(globalColors.grey
const DEFAULT_BADGE_SHADOW = shadow('#000000', { width: 0, height: 4 }, 0.06, 6);
const DEFAULT_PROGRESS_HIGHLIGHT = themed(solid('#FFFFFF'));
const MONO_LABEL_GRADIENT = themed(solid('#000000'), solid('#FFFFFF'));
+const TRANSPARENT_BORDER_COLORS: Gradient['colors'] = ['rgba(0,0,0,0)', 'rgba(0,0,0,0)'];
+
+const BLACK_TIER_RAINBOW_OVERLAY_COLORS: HardLightOverlay['colors'] = ['#A78BFF', '#8AD7FF', '#70D59E', '#FADA5D', '#FF9C92', '#FBAFD0'];
+const BLACK_TIER_RAINBOW_OVERLAY_LOCATIONS: HardLightOverlay['locations'] = [0, 0.147, 0.362, 0.566, 0.785, 1];
+
+const BLACK_TIER_BADGE_RAINBOW_OVERLAY: Themed = themed({
+ colors: BLACK_TIER_RAINBOW_OVERLAY_COLORS,
+ locations: BLACK_TIER_RAINBOW_OVERLAY_LOCATIONS,
+ maskShadow: {
+ blur: 2,
+ dx: 0,
+ dy: 2,
+ },
+});
+
+const BLACK_TIER_PRIMARY_BUTTON_RAINBOW_OVERLAY: Themed = themed({
+ colors: BLACK_TIER_RAINBOW_OVERLAY_COLORS,
+ locations: BLACK_TIER_RAINBOW_OVERLAY_LOCATIONS,
+ maskShadow: {
+ blur: 2.5,
+ dx: 0,
+ dy: 4,
+ },
+});
// ============ Badges ========================================================= //
@@ -215,12 +277,20 @@ const DIAMOND_BADGE: BadgeTheme = {
const BLACK_BADGE: BadgeTheme = {
fill: themed(gradient(['#444444', '#000000'])),
- border: themed(gradient(LIGHT_BADGE_BORDER_COLORS)),
+ border: themed(gradient(TRANSPARENT_BORDER_COLORS)),
shadows: themedShadows(DEFAULT_BADGE_SHADOW),
text: {
gradient: themed(solid('#FFFFFF')),
shadow: themed(textShadow('rgba(0, 0, 0, 0)', { width: 0, height: 0 }, 0)),
},
+ overlay: BLACK_TIER_BADGE_RAINBOW_OVERLAY,
+ backgroundShadow: themed({
+ colors: BLACK_TIER_RAINBOW_OVERLAY_COLORS,
+ locations: BLACK_TIER_RAINBOW_OVERLAY_LOCATIONS,
+ opacity: 0.5,
+ blur: 16,
+ dy: 0,
+ }),
};
// ============ Buttons ======================================================== //
@@ -333,7 +403,8 @@ const TIER_THEMES: Record = {
labelGradient: MONO_LABEL_GRADIENT,
badge: BLACK_BADGE,
primaryButton: badgePrimaryButton(BLACK_BADGE, {
- fill: themed(solid('#1A1716')),
+ fill: themed(gradient(['#2A2A2A', '#141414']), gradient(['#272727', '#141414'])),
+ overlay: BLACK_TIER_PRIMARY_BUTTON_RAINBOW_OVERLAY,
}),
secondaryButton: secondaryButton(BLACK_BADGE, {
fill: themed(
@@ -348,6 +419,13 @@ const TIER_THEMES: Record = {
fill: themed(gradient(['#444444', '#000000'])),
border: themed(solid('rgba(0,0,0,0)')),
shadow: themed(shadow('#000000', { width: 0, height: 4 }, 0.06, 6)),
+ backgroundShadow: themed({
+ colors: BLACK_TIER_RAINBOW_OVERLAY_COLORS,
+ locations: BLACK_TIER_RAINBOW_OVERLAY_LOCATIONS,
+ opacity: 0.24,
+ blur: 12,
+ dy: 0,
+ }),
highlight: themed(
gradient(['#7D53FF', '#3FBDFF', '#4BFF9D', '#FFD73A', '#FF5E4D', '#FF3C91'], {
locations: [0, 0.15, 0.36, 0.57, 0.79, 1],
@@ -361,8 +439,12 @@ const TIER_THEMES: Record = {
const FALLBACK_TIER_THEME = TIER_THEMES.STAKING_TIER_LEVEL_BASIC;
+function isTierId(level: string): level is TierId {
+ return level in TIER_THEMES;
+}
+
function resolveTierTheme(level: TierId | string): TierTheme {
- return TIER_THEMES[level as TierId] ?? FALLBACK_TIER_THEME;
+ return isTierId(level) ? TIER_THEMES[level] : FALLBACK_TIER_THEME;
}
export function getTierBackgroundTheme(level: TierId | string): { gradient: Themed } {