Skip to content
Draft
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
161 changes: 161 additions & 0 deletions components/AmountHero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React, { useRef } from 'react';
import { Pressable, StyleSheet, TextInput, View } from 'react-native';

import { ShroudText } from '../ShroudComponents';
import { ClashFont } from '../constants/fonts';
import loc from '../loc';
import { isAmountEmpty } from '../helpers/send/format';
import CheckmarkIcon from './icons/CheckmarkIcon';
import { useTheme } from './themes';

interface AmountHeroProps {
/** BTC amount as a string ('' when empty), or a display string in read-only mode. */
amount?: string;
/** Preformatted fiat estimate, e.g. "≈ ₹0". */
fiat: string;
editable?: boolean;
onChangeAmount?: (text: string) => void;
/** Shown beneath the fiat line in editable mode (e.g. "Tap amount to edit"). */
showHint?: boolean;
/** When provided (editable mode), renders the "Max" pill. */
onUseMax?: () => void;
useMaxDisabled?: boolean;
/** When true, the Max pill shows a tick and the "Sending Max" hint replaces the edit hint. */
isMax?: boolean;
}

const AmountHero: React.FC<AmountHeroProps> = ({
amount,
fiat,
editable = false,
onChangeAmount,
showHint = false,
onUseMax,
useMaxDisabled = false,
isMax = false,
}) => {
const { colors } = useTheme();
const inputRef = useRef<TextInput>(null);
const empty = isAmountEmpty(amount);

const stylesHook = StyleSheet.create({
amountFilled: { color: colors.black },
amountEmpty: { color: colors.amountPlaceholder },
meta: { color: colors.amountMeta },
hint: { color: colors.brandPrimary },
sendingMax: { color: colors.brandPrimary },
useMax: { backgroundColor: colors.surfaceSubtle, borderColor: colors.useMaxBorder },
useMaxText: { color: colors.useMaxText },
});

const amountColor = empty ? stylesHook.amountEmpty : stylesHook.amountFilled;

return (
<Pressable
style={styles.container}
accessibilityRole={editable ? 'button' : undefined}
onPress={editable ? () => inputRef.current?.focus() : undefined}
>
<View style={styles.amountRow}>
{editable ? (
<TextInput
ref={inputRef}
style={[styles.amount, amountColor]}
value={amount}
onChangeText={onChangeAmount}
placeholder="0"
placeholderTextColor={colors.amountPlaceholder}
keyboardType="decimal-pad"
testID="AmountHeroInput"
/>
) : (
<ShroudText style={[styles.amount, amountColor]} testID="AmountHeroValue">
{empty ? '0' : amount}
</ShroudText>
)}
<ShroudText style={[styles.unit, stylesHook.meta]}>BTC</ShroudText>
</View>

<ShroudText style={[styles.fiat, stylesHook.meta]}>{fiat}</ShroudText>

{isMax ? (
<ShroudText style={[styles.hint, stylesHook.sendingMax]}>{loc.send.sending_max}</ShroudText>
) : (
showHint && <ShroudText style={[styles.hint, stylesHook.hint]}>{loc.send.tap_amount_to_edit}</ShroudText>
)}

{onUseMax && (
<Pressable
accessibilityRole="button"
disabled={useMaxDisabled}
onPress={onUseMax}
style={[styles.useMax, stylesHook.useMax, useMaxDisabled && styles.useMaxDisabled]}
testID="UseMaxButton"
>
{isMax && <CheckmarkIcon color={colors.brandPrimary} size={16} />}
<ShroudText style={[styles.useMaxText, stylesHook.useMaxText]}>{loc.send.max}</ShroudText>
</Pressable>
)}
</Pressable>
);
};

const styles = StyleSheet.create({
container: {
width: '100%',
alignItems: 'center',
gap: 3,
},
amountRow: {
flexDirection: 'row',
alignItems: 'flex-end',
gap: 8,
},
amount: {
fontFamily: ClashFont.medium,
fontSize: 48,
lineHeight: 48,
letterSpacing: -1.2,
textAlign: 'center',
minWidth: 40,
padding: 0,
},
unit: {
fontFamily: ClashFont.regular,
fontSize: 20,
lineHeight: 30,
marginBottom: 8,
},
fiat: {
fontFamily: ClashFont.regular,
fontSize: 15,
lineHeight: 22,
textAlign: 'center',
},
hint: {
fontFamily: ClashFont.regular,
fontSize: 12,
lineHeight: 22,
textAlign: 'center',
},
useMax: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
height: 32,
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 16,
},
useMaxDisabled: {
opacity: 0.5,
},
useMaxText: {
fontFamily: ClashFont.medium,
fontSize: 14,
lineHeight: 24,
},
});

export default AmountHero;
9 changes: 6 additions & 3 deletions components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { forwardRef } from 'react';
import { ActivityIndicator, StyleProp, StyleSheet, Text, Pressable, PressableProps, View, ViewStyle, Platform } from 'react-native';
import { Icon } from '@rneui/themed';

import { ClashFont } from '../constants/fonts';
import { useTheme } from './themes';

interface ButtonProps extends PressableProps {
Expand All @@ -18,6 +19,8 @@ interface ButtonProps extends PressableProps {
style?: StyleProp<ViewStyle>;
onPress?: () => void;
showActivityIndicator?: boolean;
disabledBackgroundColor?: string;
disabledTextColor?: string;
}

export const Button = forwardRef<React.ElementRef<typeof Pressable>, ButtonProps>((props, ref) => {
Expand All @@ -26,8 +29,8 @@ export const Button = forwardRef<React.ElementRef<typeof Pressable>, ButtonProps
let backgroundColor = props.backgroundColor ?? colors.mainColor;
let fontColor = props.buttonTextColor ?? colors.buttonTextColor;
if (props.disabled) {
backgroundColor = colors.buttonDisabledBackgroundColor;
fontColor = colors.buttonDisabledTextColor;
backgroundColor = props.disabledBackgroundColor ?? colors.buttonDisabledBackgroundColor;
fontColor = props.disabledTextColor ?? colors.buttonDisabledTextColor;
}

const buttonStyle = {
Expand Down Expand Up @@ -89,8 +92,8 @@ const styles = StyleSheet.create({
},
text: {
marginHorizontal: 8,
fontFamily: ClashFont.semibold,
fontSize: 16,
fontWeight: '600',
},
pressableWrapper: {
overflow: 'hidden',
Expand Down
57 changes: 57 additions & 0 deletions components/LabeledField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';

import { ShroudText } from '../ShroudComponents';
import { ClashFont } from '../constants/fonts';
import { useTheme } from './themes';

interface LabeledFieldProps {
label: string;
children: React.ReactNode; // the input element
trailing?: React.ReactNode; // e.g. a scan button
testID?: string;
}

const LabeledField: React.FC<LabeledFieldProps> = ({ label, children, trailing, testID }) => {
const { colors } = useTheme();

const stylesHook = StyleSheet.create({
label: { color: colors.textSecondary },
field: { backgroundColor: colors.fieldBackground },
});

return (
<View style={styles.container} testID={testID}>
<ShroudText style={[styles.label, stylesHook.label]}>{label}</ShroudText>
<View style={[styles.field, stylesHook.field]}>
<View style={styles.inputWrap}>{children}</View>
{trailing}
</View>
</View>
);
};

const styles = StyleSheet.create({
container: {
width: '100%',
gap: 8,
},
label: {
fontFamily: ClashFont.regular,
fontSize: 14,
lineHeight: 20,
},
field: {
flexDirection: 'row',
alignItems: 'center',
minHeight: 48,
borderRadius: 12,
paddingHorizontal: 12,
gap: 10,
},
inputWrap: {
flex: 1,
},
});

export default LabeledField;
15 changes: 15 additions & 0 deletions components/icons/CheckmarkIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import Svg, { Path } from 'react-native-svg';

interface IconProps {
color?: string;
size?: number;
}

const CheckmarkIcon: React.FC<IconProps> = ({ color = '#754CE8', size = 24 }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path d="M5 12L10 17L19 8" stroke={color} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
</Svg>
);

export default CheckmarkIcon;
15 changes: 15 additions & 0 deletions components/icons/ChevronRightIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import Svg, { Path } from 'react-native-svg';

interface IconProps {
color?: string;
size?: number;
}

const ChevronRightIcon: React.FC<IconProps> = ({ color = '#C7C7CC', size = 20 }) => (
<Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
<Path d="M7.5 5L12.5 10L7.5 15" stroke={color} strokeWidth={1.66607} strokeLinecap="round" strokeLinejoin="round" />
</Svg>
);

export default ChevronRightIcon;
16 changes: 16 additions & 0 deletions components/icons/ClockIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import Svg, { Circle, Path } from 'react-native-svg';

interface IconProps {
color?: string;
size?: number;
}

const ClockIcon: React.FC<IconProps> = ({ color = '#754CE8', size = 24 }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Circle cx={12} cy={12} r={9} stroke={color} strokeWidth={1.5} />
<Path d="M12 7 V12 L15.5 14" stroke={color} strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</Svg>
);

export default ClockIcon;
15 changes: 15 additions & 0 deletions components/icons/LightningIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import Svg, { Path } from 'react-native-svg';

interface IconProps {
color?: string;
size?: number;
}

const LightningIcon: React.FC<IconProps> = ({ color = '#754CE8', size = 24 }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path d="M13 2 L5 13 H11 L11 22 L19 10 H13 Z" stroke={color} strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</Svg>
);

export default LightningIcon;
43 changes: 43 additions & 0 deletions components/icons/ScanQRIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import Svg, { Path } from 'react-native-svg';

interface IconProps {
color?: string;
size?: number;
}

const ScanQRIcon: React.FC<IconProps> = ({ color = '#754CE8', size = 20 }) => (
<Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
<Path
d="M2.49902 5.8313V4.16522C2.49902 3.72335 2.67456 3.29958 2.98701 2.98713C3.29946 2.67468 3.72323 2.49915 4.1651 2.49915H5.83117"
stroke={color}
strokeWidth="1.66607"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M14.1616 2.49915H15.8277C16.2696 2.49915 16.6933 2.67468 17.0058 2.98713C17.3182 3.29958 17.4938 3.72335 17.4938 4.16522V5.8313"
stroke={color}
strokeWidth="1.66607"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M17.4938 14.1616V15.8277C17.4938 16.2696 17.3182 16.6933 17.0058 17.0058C16.6933 17.3182 16.2696 17.4938 15.8277 17.4938H14.1616"
stroke={color}
strokeWidth="1.66607"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M5.83117 17.4938H4.1651C3.72323 17.4938 3.29946 17.3182 2.98701 17.0058C2.67456 16.6933 2.49902 16.2696 2.49902 15.8277V14.1616"
stroke={color}
strokeWidth="1.66607"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path d="M5.83105 9.99646H14.1614" stroke={color} strokeWidth="1.66607" strokeLinecap="round" strokeLinejoin="round" />
</Svg>
);

export default ScanQRIcon;
16 changes: 16 additions & 0 deletions components/icons/SendIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import Svg, { Path } from 'react-native-svg';

interface IconProps {
color?: string;
size?: number;
}

const SendIcon: React.FC<IconProps> = ({ color = '#FFFFFF', size = 20 }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path d="M22 2 L11 13" stroke={color} strokeWidth={1.67} strokeLinecap="round" strokeLinejoin="round" />
<Path d="M22 2 L15 22 L11 13 L2 9 Z" stroke={color} strokeWidth={1.67} strokeLinecap="round" strokeLinejoin="round" />
</Svg>
);

export default SendIcon;
Loading
Loading