Files
com.blitzwallet/app/components/admin/homeComponents/analytics/analyticsBudgetPage.js
T
Blake KaufmanandGitHub 6760060083 Simplify confirm split payment (#778)
* fix small style issues

* fix touch gap

* fixing podfile

* creating standardized amountIn function

* show tx ids not amounts

* use standardized amount in

* use standardized amount in

* fixed no payment information crash

* downgrade rn and react-native-quick-crypto

* only show margin if two elements exist

* adding backup amount

* wait till hompage to call analytics data

* fix restore bug

* updating firebase

* fix check logic

* wrap to handle thrown execptions

* use utils package directly

* adding error handling

* group notification payments insted of handling them sequentually

* handle payment requset on confirm page

* improving split payment ux

* adding translations

* adding firebase sdk warning
2026-04-19 11:43:00 -04:00

371 lines
11 KiB
JavaScript

import { useNavigation } from '@react-navigation/native';
import {
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Svg, { Circle } from 'react-native-svg';
import {
COLORS,
FONT,
NEAR_BUDGET_LIMIT,
OVER_BUDGET_LIMIT,
SIZES,
} from '../../../../constants';
import {
GlobalThemeView,
ThemeText,
} from '../../../../functions/CustomElements';
import CustomSettingsTopBar from '../../../../functions/CustomElements/settingsTopBar';
import ThemeIcon from '../../../../functions/CustomElements/themeIcon';
import { useGlobalContextProvider } from '../../../../../context-store/context';
import { useMemo } from 'react';
import { useGlobalInsets } from '../../../../../context-store/insetsProvider';
import FormattedSatText from '../../../../functions/CustomElements/satTextDisplay';
import NoContentSceen from '../../../../functions/CustomElements/noContentScreen';
import GetThemeColors from '../../../../hooks/themeColors';
import {
HIDDEN_OPACITY,
INSET_WINDOW_WIDTH,
} from '../../../../constants/theme';
import { useGlobalThemeContext } from '../../../../../context-store/theme';
import { useNodeContext } from '../../../../../context-store/nodeContext';
import displayCorrectDenomination from '../../../../functions/displayCorrectDenomination';
import { useAnalytics } from '../../../../../context-store/analyticsContext';
import { useTranslation } from 'react-i18next';
const CIRCLE_SIZE = 220;
const STROKE_WIDTH = 16;
const RADIUS = (CIRCLE_SIZE - STROKE_WIDTH) / 2;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
const CX = CIRCLE_SIZE / 2;
const CY = CIRCLE_SIZE / 2;
export default function AnalyticsBudgetPage() {
const navigate = useNavigation();
const { masterInfoObject } = useGlobalContextProvider();
const { fiatStats } = useNodeContext();
const { bottomPadding } = useGlobalInsets();
const { textColor, backgroundOffset, backgroundColor } = GetThemeColors();
const { theme, darkModeType } = useGlobalThemeContext();
const { spentTotal, spentTxCount } = useAnalytics();
const { t } = useTranslation();
const budget = masterInfoObject?.monthlyBudget;
const budgetAmount = budget?.amount || 0;
const userBalanceDenomination = masterInfoObject.userBalanceDenomination;
const leftToSpend = Math.max(budgetAmount - spentTotal, 0);
const spentPercent =
budgetAmount > 0 ? Math.min(spentTotal / budgetAmount, 1) : 0;
const budgetStatus = useMemo(() => {
if (spentPercent >= OVER_BUDGET_LIMIT)
return {
label: t('analytics.overBudget'),
color: theme && darkModeType ? textColor : COLORS.cancelRed,
};
if (spentPercent >= NEAR_BUDGET_LIMIT)
return {
label: t('analytics.nearLimit'),
color: theme && darkModeType ? textColor : COLORS.bitcoinOrange,
};
return {
label: t('analytics.onTrack'),
color: theme && darkModeType ? textColor : COLORS.primary,
};
}, [spentPercent]);
const dashOffset = CIRCUMFERENCE * (1 - spentPercent);
const navigateToRemoveBudget = () => {
navigate.navigate('CustomHalfModal', {
wantedContent: 'RemoveBudgetHalfModal',
});
};
if (!budget) {
return (
<GlobalThemeView useStandardWidth={true}>
<CustomSettingsTopBar label={t('analytics.budget.budget')} />
<NoContentSceen
iconName="Wallet"
titleText={t('analytics.budget.noBudgetSet')}
subTitleText={t('analytics.budget.noBudgetSetSub')}
showButton={true}
buttonText={t('analytics.budget.noBudgetSetButton')}
buttonFunction={() => navigate.navigate('AnalyticsCreateBudgetPage')}
/>
</GlobalThemeView>
);
}
return (
<GlobalThemeView useStandardWidth={true}>
<CustomSettingsTopBar
label={t('analytics.budget.budget')}
showLeftImage={true}
iconNew="Trash2"
leftImageFunction={navigateToRemoveBudget}
/>
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingBottom: bottomPadding + 20 },
]}
showsVerticalScrollIndicator={false}
>
{/* Status badge */}
<View style={styles.statusContainer}>
<View
style={[styles.statusDot, { backgroundColor: budgetStatus.color }]}
/>
<Text style={[styles.statusText, { color: budgetStatus.color }]}>
{budgetStatus.label}
</Text>
<Text style={[styles.statusDivider, { color: textColor }]}> · </Text>
<Text
style={[styles.statusPeriod, { color: textColor, opacity: 0.5 }]}
>
{t('analytics.thisMonth')}
</Text>
</View>
{/* Circular progress */}
<View style={styles.circleContainer}>
<Svg width={CIRCLE_SIZE} height={CIRCLE_SIZE}>
{/* Track */}
<Circle
cx={CX}
cy={CY}
r={RADIUS}
fill="none"
stroke={backgroundOffset}
strokeWidth={STROKE_WIDTH}
/>
{/* Progress arc */}
<Circle
cx={CX}
cy={CY}
r={RADIUS}
fill="none"
stroke={budgetStatus.color}
strokeWidth={STROKE_WIDTH}
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={dashOffset}
strokeLinecap="round"
rotation="-90"
origin={`${CX}, ${CY}`}
/>
</Svg>
{/* Center label */}
<View style={styles.circleCenter} pointerEvents="none">
<ThemeText
styles={styles.centerText}
content={displayCorrectDenomination({
amount: leftToSpend,
masterInfoObject,
fiatStats,
})}
CustomNumberOfLines={1}
adjustsFontSizeToFit={true}
/>
<ThemeText
styles={styles.centerTextLabel}
content={t('analytics.budget.leftToSpend')}
CustomNumberOfLines={1}
adjustsFontSizeToFit={true}
/>
</View>
</View>
{/* Stats rows */}
<View style={[styles.statsCard, { backgroundColor: backgroundOffset }]}>
{/* Budget row with edit */}
<View style={styles.statsRow}>
<View style={styles.statsRowLeft}>
<ThemeIcon iconName="PieChart" size={20} />
<View style={styles.statsRowText}>
<ThemeText
styles={styles.statsLabel}
content={t('analytics.budget.budget')}
/>
</View>
</View>
<View style={styles.statsRowRight}>
<TouchableOpacity
onPress={() => navigate.navigate('AnalyticsCreateBudgetPage')}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
>
<ThemeIcon iconName="Edit" size={16} />
</TouchableOpacity>
<FormattedSatText
balance={budgetAmount}
globalBalanceDenomination={userBalanceDenomination}
styles={styles.statsValue}
neverHideBalance={true}
/>
</View>
</View>
<View style={[styles.divider, { backgroundColor }]} />
{/* Spent this month */}
<View style={styles.statsRow}>
<View style={styles.statsRowLeft}>
<ThemeIcon iconName="Coins" size={20} />
<View style={styles.statsRowText}>
<ThemeText
styles={styles.statsLabel}
content={t('analytics.budget.spentThisMonth')}
/>
<ThemeText
styles={styles.statsSubLabel}
content={t('analytics.numberOfTxs', {
count: spentTxCount,
})}
/>
</View>
</View>
<FormattedSatText
balance={spentTotal}
globalBalanceDenomination={userBalanceDenomination}
styles={styles.statsValue}
neverHideBalance={true}
/>
</View>
<View style={[styles.divider, { backgroundColor }]} />
{/* Left to spend */}
<View style={styles.statsRow}>
<View style={styles.statsRowLeft}>
<ThemeIcon iconName="Gauge" size={20} />
<View style={styles.statsRowText}>
<ThemeText
styles={styles.statsLabel}
content={t('analytics.budget.leftToSpend')}
/>
</View>
</View>
<View style={styles.statsRowRight}>
<FormattedSatText
balance={leftToSpend}
globalBalanceDenomination={userBalanceDenomination}
styles={styles.statsValue}
neverHideBalance={true}
/>
</View>
</View>
</View>
</ScrollView>
</GlobalThemeView>
);
}
const styles = StyleSheet.create({
scrollContent: {
width: INSET_WINDOW_WIDTH,
alignSelf: 'center',
alignItems: 'center',
},
statusContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 24,
alignSelf: 'flex-start',
},
statusDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
statusText: {
fontSize: SIZES.smedium,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
},
statusDivider: {
fontSize: SIZES.smedium,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
},
statusPeriod: {
fontSize: SIZES.smedium,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
},
circleContainer: {
width: CIRCLE_SIZE,
height: CIRCLE_SIZE,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 32,
},
circleCenter: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
},
centerText: {
maxWidth: '85%',
fontSize: SIZES.large,
textAlign: 'center',
includeFontPadding: false,
},
centerTextLabel: {
fontSize: SIZES.small,
textAlign: 'center',
includeFontPadding: false,
opacity: HIDDEN_OPACITY,
},
statsCard: {
width: '100%',
borderRadius: 16,
overflow: 'hidden',
},
statsRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 16,
},
statsRowLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
statsRowText: {
marginLeft: 12,
flex: 1,
},
statsLabel: {
fontSize: SIZES.smedium,
includeFontPadding: false,
},
statsSubLabel: {
fontSize: SIZES.small,
opacity: 0.5,
marginTop: 2,
includeFontPadding: false,
},
statsRowRight: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
statsValue: {
textAlign: 'right',
includeFontPadding: false,
},
divider: {
height: 2,
marginHorizontal: 16,
},
});