Files
Blake KaufmanandGitHub d237f04fe5 Savings (#579)
* enlarging stroke width

* creating local Sql for savings accounts

* creating utils for savings feature

* fixing collisoin risk for pools and savings account

* creating standardized savings transaction container

* building explainer page

* creating add money page

* creating withdrawl page

* creating savings homepage

* creating savings preview card

* creating context to manage savings features

* standardizing action buttons between hompage and goal pages

* creating goals detail page

* creating standardized activity container

* creating emoji picker for new goal

* creating pool description page

* creating goal amount page

* creating success screen for goal creation

* creating update goal page

* creating coal remove confirmation screen

* creating goal removed success screen

* misc changes

* adding translations

* fixing missing var

* fixed spelling issue

* using spark wallet viewer for more reliable balance and txs

* adding savingspayout table to delete

* fixing formatting for intrest paid out in BTC

* combining intrest and nomral txs into single array

* adding tx history restore + payouts table

* adding earned pill

* fixing re-rendering of activities list

* adding ability to withdrawl intrest payments

* fix open delay

* removing tokens dropdown for halfmodal and card

* aujust fontsize if too large to prevent overflow

* creating clickable wrapper for settings cards

* fixing tokens preview card

* removing uneeded padding from social options

* moving gifts to settings from tabs bar

* adding overflow cap to dropdown text length

* updating settings pages

* adding translations

* reducing token image size to decrease banner height

* fix theme bug

* fixing button widths

* fix savings navigation issues + 0 balance issue

* starndarizing pool preview to match other previews
2026-02-24 12:24:32 -05:00

247 lines
7.3 KiB
JavaScript

import React, { createContext, useContext, useState, useEffect } from 'react';
import {
View,
TouchableOpacity,
ScrollView,
StyleSheet,
Dimensions,
} from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from 'react-native-reanimated';
import { scheduleOnRN } from 'react-native-worklets';
import { COLORS } from '../app/constants/theme';
import ThemeText from '../app/functions/CustomElements/textTheme';
import GetThemeColors from '../app/hooks/themeColors';
import { useTranslation } from 'react-i18next';
import CountryFlag from 'react-native-country-flag';
import { useGlobalThemeContext } from './theme';
const DropdownContext = createContext(null);
export const useDropdown = () => {
const context = useContext(DropdownContext);
if (!context)
throw new Error('useDropdown must be used within DropdownProvider');
return context;
};
export const DropdownProvider = ({ children }) => {
const { t } = useTranslation();
const [dropdownState, setDropdownState] = useState({
isOpen: false,
options: [],
buttonLayout: null,
onSelect: null,
config: {},
});
const [dropdownHeight, setDropdownHeight] = useState(0);
const { theme } = useGlobalThemeContext();
const { backgroundOffset, backgroundColor } = GetThemeColors();
const formattedDropdownHeight = Math.min(
dropdownHeight + dropdownState?.options?.length * 1,
200,
);
const animatedHeight = useSharedValue(0);
useEffect(() => {
if (dropdownState.isOpen && dropdownHeight > 0) {
// Only animate when we have a measured height
animatedHeight.value = withTiming(1, {
duration: 180,
easing: Easing.out(Easing.cubic),
});
} else if (!dropdownState.isOpen) {
animatedHeight.value = withTiming(0, {
duration: 180,
easing: Easing.in(Easing.cubic),
});
}
}, [dropdownState.isOpen, dropdownHeight]);
const openDropdown = (buttonLayout, options, onSelect, config = {}) => {
// setDropdownHeight(0); // Reset height when opening
setDropdownState({
isOpen: true,
options,
buttonLayout,
onSelect,
config,
});
};
const closeDropdownState = () => {
setDropdownState(prev => ({ ...prev, isOpen: false }));
};
const closeDropdown = () => {
animatedHeight.value = withTiming(0, { duration: 180 }, () => {
scheduleOnRN(closeDropdownState);
});
};
const handleSelect = item => {
if (dropdownState.onSelect) dropdownState.onSelect(item);
closeDropdown();
};
const screenHeight = Dimensions.get('window').height;
const isTooLow =
dropdownState.buttonLayout &&
dropdownState.buttonLayout.y +
dropdownState.buttonLayout.height +
formattedDropdownHeight +
80 >
screenHeight;
console.log(
screenHeight,
dropdownState.buttonLayout,
formattedDropdownHeight,
dropdownState.buttonLayout &&
dropdownState.buttonLayout.y +
dropdownState.buttonLayout.height +
formattedDropdownHeight +
80,
);
const {
showFlag = true,
translateLabelText = true,
dropdownItemCustomStyles = {},
dropdownTextCustomStyles = {},
} = dropdownState.config;
// Animated style for dropdown height & fade
const dropdownAnimatedStyle = useAnimatedStyle(() => {
const scale = animatedHeight.value;
// When opening upward (isTooLow), push content down as height decreases
const translateY = isTooLow ? formattedDropdownHeight * (1 - scale) : 0;
return {
height: scale * formattedDropdownHeight,
transform: [{ translateY }],
};
});
return (
<DropdownContext.Provider
value={{ openDropdown, closeDropdown, dropdownState }}
>
{children}
{dropdownState.isOpen && (
<View style={StyleSheet.absoluteFillObject}>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={closeDropdown}
>
{dropdownState.buttonLayout && (
<Animated.View
style={[
styles.dropdownMenu,
dropdownAnimatedStyle,
{
top: isTooLow
? dropdownState.buttonLayout.y -
formattedDropdownHeight -
5
: dropdownState.buttonLayout.y +
dropdownState.buttonLayout.height +
5,
left: dropdownState.buttonLayout.x,
width: dropdownState.buttonLayout.width,
borderColor: backgroundColor,
backgroundColor: theme
? backgroundOffset
: COLORS.darkModeText,
},
]}
>
<ScrollView
nestedScrollEnabled
showsVerticalScrollIndicator
keyboardShouldPersistTaps="handled"
>
<View
onLayout={e => {
setDropdownHeight(e.nativeEvent.layout.height);
}}
>
{dropdownState.options.map((item, index) => {
if (!item.label) return null;
return (
<TouchableOpacity
key={`${item.value}_${index}`}
style={{
...styles.dropdownItem,
borderBottomWidth:
index !== dropdownState.options.length - 1
? 1
: 0,
borderBottomColor: backgroundColor,
...dropdownItemCustomStyles,
}}
onPress={() => handleSelect(item)}
>
{showFlag && item.flagCode && (
<CountryFlag
isoCode={item.flagCode}
size={15}
style={{ marginRight: 5 }}
/>
)}
<ThemeText
CustomNumberOfLines={2}
styles={[
styles.defTextStyle,
dropdownTextCustomStyles,
]}
content={
translateLabelText ? t(item.label) : item.label
}
/>
</TouchableOpacity>
);
})}
</View>
</ScrollView>
</Animated.View>
)}
</TouchableOpacity>
</View>
)}
</DropdownContext.Provider>
);
};
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
},
dropdownMenu: {
position: 'absolute',
borderRadius: 8,
overflow: 'hidden',
borderWidth: 2,
maxHeight: 200,
},
dropdownItem: {
height: 45,
justifyContent: 'center',
paddingHorizontal: 10,
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
},
defTextStyle: {
includeFontPadding: false,
flexShrink: 1,
},
});