Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions react/lib/components/Widget/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
CryptoCurrency,
DEFAULT_DONATION_RATE,
DEFAULT_MINIMUM_DONATION_AMOUNT,
darkMode,
} from '../../util';
import AltpaymentWidget from './AltpaymentWidget'
import {
Expand Down Expand Up @@ -322,6 +323,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
const [isAboveMinimumAltpaymentAmount, setIsAboveMinimumAltpaymentAmount] = useState<boolean | null>(null)

const theme = useTheme(props.theme, isValidXecAddress(to))
const isDarkMode = useMemo(() => darkMode(theme.palette.tertiary), [theme.palette.tertiary])

const [thisAmount, setThisAmount] = useState(props.amount)
const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject)
Expand Down Expand Up @@ -353,13 +355,13 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
return {
root: {
minWidth: '240px',
background: '#f5f5f7',
background: isDarkMode ? '#2a2a2a' : '#f5f5f7',
position: 'relative',
overflow: 'hidden',
},
qrCode: {
background: '#fff',
border: '1px solid #eee',
background: isDarkMode ? '#1a1a1a' : '#fff',
border: isDarkMode ? '1px solid #333' : '1px solid #eee',
borderRadius: '4px',
outline: 'none',
lineHeight: 0,
Expand All @@ -381,18 +383,19 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
},
copyTextContainer: {
display: base.loading ? 'none' : 'block',
background: '#ffffffcc',
background: isDarkMode ? '#1a1a1acc' : '#ffffffcc',
padding: '0 0.15rem 0.15rem 0',
},
copyText: {
lineHeight: '1.2em',
fontSize: '0.7em',
color: base.theme.palette.tertiary,
textShadow:
'#fff -2px 0 1px, #fff 0 -2px 1px, #fff 0 2px 1px, #fff 2px 0 1px',
textShadow: isDarkMode
? '#000 -2px 0 1px, #000 0 -2px 1px, #000 0 2px 1px, #000 2px 0 1px'
: '#fff -2px 0 1px, #fff 0 -2px 1px, #fff 0 2px 1px, #fff 2px 0 1px',
'&:disabled span': {
filter: 'blur(2px)',
color: 'rgba(0, 0, 0, 0.5)',
color: isDarkMode ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)',
},
},
text: {
Expand All @@ -404,7 +407,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
},
footer: {
fontSize: '0.6rem',
color: '#a8a8a8',
color: isDarkMode ? '#888888' : '#a8a8a8',
fontWeight: 'normal',
userSelect: 'none',
display: 'flex',
Expand All @@ -424,7 +427,8 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
cursor: 'pointer',
padding: '6px 12px',
marginTop: '20px',
background: '#e9e9e9',
background: isDarkMode ? '#444444' : '#e9e9e9',
color: isDarkMode ? '#ffffff' : 'inherit',
borderRadius: '5px',
transition: 'all ease-in-out 200ms',
opacity: 0,
Expand Down Expand Up @@ -477,7 +481,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
animationDelay: '0.4s',
},
}
}, [success, qrLoading, theme, recentlyCopied, copied])
}, [success, qrLoading, theme, recentlyCopied, copied, isDarkMode])

const bchSvg = useMemo((): string => {
const color = theme.palette.logo ?? theme.palette.primary
Expand Down Expand Up @@ -1031,6 +1035,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
size={300}
level="H"
value={url}
bgColor={isDarkMode ? '#1a1a1a' : '#ffffff'}
fgColor={theme.palette.tertiary as unknown as string}
imageSettings={{
src: success ? checkSvg : isValidCashAddress(to) ? bchSvg : xecSvg,
Expand Down Expand Up @@ -1059,7 +1064,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
<Box
flex="shrink"
alignSelf="stretch"
style={{ background: '#fff' }}
style={{ background: isDarkMode ? '#3a3a3a' : '#fff' }}
py={1}
textAlign="center"
>
Expand Down Expand Up @@ -1342,7 +1347,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
width: '13px',
height: '13px',
fill: donationEnabled ? '#f44336' : 'none',
stroke: donationEnabled ? '#f44336' : '#5c5c5c',
stroke: donationEnabled ? '#f44336' : (isDarkMode ? '#a0a0a0' : '#5c5c5c'),
strokeWidth: donationEnabled ? 0 : 1.5,
transition: 'all 0.2s ease-in-out',
'&:hover': {
Expand Down Expand Up @@ -1380,7 +1385,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
padding: '0px 2px 0px 4px',
fontSize: '0.6rem',
textAlign: 'left',
color: '#5c5c5c',
color: isDarkMode ? '#b0b0b0' : '#5c5c5c',
lineHeight: '1.5em',
},
'& fieldset': {
Expand All @@ -1393,7 +1398,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
component="span"
sx={{
fontSize: '0.6rem',
color: '#5c5c5c',
color: isDarkMode ? '#b0b0b0' : '#5c5c5c',
flexShrink: 0,
marginLeft: '2px',
}}
Expand Down
2 changes: 1 addition & 1 deletion react/lib/tests/components/Widget.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Widget from '../../components/Widget/Widget'
jest.mock('copy-to-clipboard', () => jest.fn())

jest.mock('../../util', () => ({
__esModule: true,
...jest.requireActual('../../util'),
// network / balance
getAddressBalance: jest.fn().mockResolvedValue(0),
// address / currency helpers
Expand Down
77 changes: 77 additions & 0 deletions react/lib/util/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Converts a hex color to RGB values
*/
export const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
// Remove # if present
const cleanHex = hex.replace(/^#/, '');

// Handle 3-character hex codes
const fullHex = cleanHex.length === 3
? cleanHex.split('').map(c => c + c).join('')
: cleanHex;

const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};

/**
* Normalizes any CSS color value to a hex code using the browser's canvas API
* Handles hex codes, named colors (e.g., "navy"), rgb(), hsl(), etc.
*/
export const normalizeColorToHex = (color: string): string | null => {
if (!color) return null;

const trimmed = color.trim();
if (!trimmed) return null;

// Use canvas to parse any CSS color value
const ctx = document.createElement('canvas').getContext('2d');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: SSR compatibility: document is not available in server-side environments

Suggested change
const ctx = document.createElement('canvas').getContext('2d');
const ctx = typeof document !== 'undefined' ? document.createElement('canvas').getContext('2d') : null;
Prompt To Fix With AI
This is a comment left during a code review.
Path: react/lib/util/color.ts
Line: 34:34

Comment:
**logic:** SSR compatibility: `document` is not available in server-side environments

```suggestion
  const ctx = typeof document !== 'undefined' ? document.createElement('canvas').getContext('2d') : null;
```

How can I resolve this? If you propose a fix, please make it concise.

if (!ctx) return null;

ctx.fillStyle = trimmed;
// The browser normalizes the color to a hex string (or rgb() for transparent colors)
return ctx.fillStyle;
};

/**
* Calculates the relative luminance of a color
* Based on WCAG 2.0 formula: https://www.w3.org/TR/WCAG20/#relativeluminancedef
* Returns a value between 0 (black) and 1 (white)
*/
export const getLuminance = (color: string): number => {
const hex = normalizeColorToHex(color);
if (!hex) return 0;

const rgb = hexToRgb(hex);
if (!rgb) return 0;

const { r, g, b } = rgb;

// Convert to sRGB
const sR = r / 255;
const sG = g / 255;
const sB = b / 255;

// Apply gamma correction
const R = sR <= 0.03928 ? sR / 12.92 : Math.pow((sR + 0.055) / 1.055, 2.4);
const G = sG <= 0.03928 ? sG / 12.92 : Math.pow((sG + 0.055) / 1.055, 2.4);
const B = sB <= 0.03928 ? sB / 12.92 : Math.pow((sB + 0.055) / 1.055, 2.4);

// Calculate luminance
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
};

/**
* Determines if dark mode should be used based on the color
* Dark mode is enabled when the color is light (luminance > 0.5)
* This threshold can be adjusted based on needs
*/
export const darkMode = (color: string, threshold = 0.5): boolean => {
return getLuminance(color) > threshold;
};
1 change: 1 addition & 0 deletions react/lib/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './address';
export * from './api-client';
export * from './cashtab';
export * from './color';
export * from './constants';
export * from './format';
export * from './opReturn';
Expand Down