Standardize half modal back (#894)

* standardized chain row sizing

* add back to online listings

* add back to gift quantity page

* add back button to view tokens page

* update pool half modal to back button

* Added back button to swap flow

* adding back button to savings pages
This commit is contained in:
Blake Kaufman
2026-06-06 09:34:50 -04:00
committed by GitHub
parent 885ad138bc
commit e79a28d0e7
12 changed files with 1605 additions and 1437 deletions
@@ -1,19 +1,11 @@
import { useEffect, useMemo } from 'react';
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { ThemeText } from '../../../../functions/CustomElements';
import ThemeIcon from '../../../../functions/CustomElements/themeIcon';
import { useMemo } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import GetThemeColors from '../../../../hooks/themeColors';
import { HIDDEN_OPACITY } from '../../../../constants/theme';
import { ACCUMULATION_CHAINS } from '../../../../constants/accumulationAddresses';
import { CENTER, ICONS } from '../../../../constants';
import { Image } from 'expo-image';
import { useGlobalThemeContext } from '../../../../../context-store/theme';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
import ChainRow from './chainRow';
export default function CreateAccumulationAddressDepositModal({
setContentHeight,
@@ -66,152 +58,10 @@ export default function CreateAccumulationAddressDepositModal({
return <View style={styles.container}>{chainElements}</View>;
}
function ChainRow({
chain,
expanded,
onToggleExpand,
onSelectAsset,
isAssetTaken,
onDisabledAssetPress,
theme,
darkModeType,
backgroundColor,
backgroundOffset,
}) {
const expandHeight = useSharedValue(0);
const chevronRotation = useSharedValue(0);
useEffect(() => {
expandHeight.value = withTiming(expanded ? 1 : 0, { duration: 200 });
chevronRotation.value = withTiming(expanded ? 1 : 0, { duration: 200 });
}, [expanded]);
const expandedStyle = useAnimatedStyle(() => ({
height: expandHeight.value * (chain.assets.length * 65 + 26),
opacity: expandHeight.value,
}));
const chevronStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${chevronRotation.value * 180}deg` }],
}));
return (
<View>
<TouchableOpacity
activeOpacity={0.7}
style={styles.chainRow}
onPress={() => onToggleExpand(chain.id)}
>
<View
style={[
styles.chainIconContainer,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
},
]}
>
<Image
style={styles.assetIcon}
source={ICONS[`chain_${chain.label.toLowerCase()}`]}
contentFit="contain"
/>
</View>
<ThemeText styles={styles.optionLabel} content={chain.label} />
<Animated.View style={[{ opacity: HIDDEN_OPACITY }, chevronStyle]}>
<ThemeIcon iconName="ChevronDown" size={18} />
</Animated.View>
</TouchableOpacity>
<Animated.View style={[styles.assetOptionsContainer, expandedStyle]}>
{chain.assets.map(asset => {
const disabled = isAssetTaken(asset);
return (
<TouchableOpacity
key={asset}
activeOpacity={0.7}
style={[
styles.assetOptionRow,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
opacity: disabled ? HIDDEN_OPACITY : 1,
},
]}
onPress={() =>
disabled ? onDisabledAssetPress() : onSelectAsset(chain, asset)
}
>
<View style={styles.assetOptionIconContainer}>
<Image
style={styles.assetOptionIcon}
source={ICONS[`${asset.toLowerCase()}Logo`]}
contentFit="contain"
/>
</View>
<ThemeText styles={styles.optionLabel} content={asset} />
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
width: '100%',
...CENTER,
},
chainRow: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
// paddingVertical: 8,
},
chainIconContainer: {
width: 45,
height: 45,
borderRadius: 22.5,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
marginRight: 15,
},
optionLabel: {
flex: 1,
includeFontPadding: false,
},
assetIcon: {
width: 45,
height: 45,
},
assetOptionsContainer: {
overflow: 'hidden',
gap: 12,
paddingTop: 8,
paddingBottom: 8,
},
assetOptionRow: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 14,
borderRadius: 12,
gap: 12,
},
assetOptionIconContainer: {
width: 35,
height: 35,
borderRadius: 17.5,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
},
assetOptionIcon: {
width: 35,
height: 35,
},
});
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
@@ -27,8 +27,8 @@ import Animated, {
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
import { scheduleOnRN } from 'react-native-worklets';
import { useGlobalInsets } from '../../../../../context-store/insetsProvider';
import ChainRow from './chainRow';
// Steps: 'chain' | 'destination' | 'confirm'
const STEPS = ['chain', 'destination', 'confirm'];
@@ -37,6 +37,7 @@ export default function CreateAccumulationAddressModal({
setContentHeight,
handleBackPressFunction,
forcedDestination,
setBackNav,
}) {
const navigate = useNavigation();
const { t } = useTranslation();
@@ -46,8 +47,6 @@ export default function CreateAccumulationAddressModal({
const { bottomPadding } = useGlobalInsets();
const [step, setStep] = useState('chain');
const [renderedStep, setRenderedStep] = useState('chain');
const renderedStepRef = useRef('chain');
const [expandedChain, setExpandedChain] = useState(null);
const [selectedAsset, setSelectedAsset] = useState(null);
const [selectedChain, setSelectedChain] = useState(null);
@@ -55,6 +54,7 @@ export default function CreateAccumulationAddressModal({
forcedDestination || null,
);
const [isCreating, setIsCreating] = useState(false);
const [mountedSteps, setMountedSteps] = useState(() => new Set(['chain']));
const isPairTaken = useCallback(
(chainId, asset, dest) =>
@@ -67,39 +67,66 @@ export default function CreateAccumulationAddressModal({
[addresses],
);
const pageOpacity = useSharedValue(1);
const pageTranslateX = useSharedValue(0);
const startInAnimation = useCallback(
(newStep, goingForward) => {
renderedStepRef.current = newStep;
setRenderedStep(newStep);
pageTranslateX.value = goingForward ? 30 : -30;
pageOpacity.value = withTiming(1, { duration: 125 });
pageTranslateX.value = withTiming(0, { duration: 125 });
},
[pageTranslateX, pageOpacity],
);
const chainOpacity = useSharedValue(1);
const chainTranslateX = useSharedValue(0);
const destinationOpacity = useSharedValue(0);
const destinationTranslateX = useSharedValue(30);
const confirmOpacity = useSharedValue(0);
const confirmTranslateX = useSharedValue(30);
useEffect(() => {
if (step === renderedStepRef.current) return;
const goingForward =
STEPS.indexOf(step) > STEPS.indexOf(renderedStepRef.current);
pageOpacity.value = withTiming(0, { duration: 125 });
pageTranslateX.value = withTiming(
goingForward ? -30 : 30,
{ duration: 125 },
finished => {
if (finished) scheduleOnRN(startInAnimation, step, goingForward);
},
);
const activeIndex = STEPS.indexOf(step);
const translateForStep = screenStep => {
const screenIndex = STEPS.indexOf(screenStep);
if (screenIndex === activeIndex) return 0;
return screenIndex < activeIndex ? -30 : 30;
};
chainOpacity.value = withTiming(step === 'chain' ? 1 : 0, {
duration: 250,
});
chainTranslateX.value = withTiming(translateForStep('chain'), {
duration: 250,
});
destinationOpacity.value = withTiming(step === 'destination' ? 1 : 0, {
duration: 250,
});
destinationTranslateX.value = withTiming(translateForStep('destination'), {
duration: 250,
});
confirmOpacity.value = withTiming(step === 'confirm' ? 1 : 0, {
duration: 250,
});
confirmTranslateX.value = withTiming(translateForStep('confirm'), {
duration: 250,
});
}, [step]);
const pageAnimatedStyle = useAnimatedStyle(() => ({
opacity: pageOpacity.value,
transform: [{ translateX: pageTranslateX.value }],
const chainAnimatedStyle = useAnimatedStyle(() => ({
opacity: chainOpacity.value,
transform: [{ translateX: chainTranslateX.value }],
}));
const destinationAnimatedStyle = useAnimatedStyle(() => ({
opacity: destinationOpacity.value,
transform: [{ translateX: destinationTranslateX.value }],
}));
const confirmAnimatedStyle = useAnimatedStyle(() => ({
opacity: confirmOpacity.value,
transform: [{ translateX: confirmTranslateX.value }],
}));
const goToStep = useCallback(nextStep => {
setMountedSteps(prev => {
if (prev.has(nextStep)) return prev;
const next = new Set(prev);
next.add(nextStep);
return next;
});
setStep(nextStep);
}, []);
useEffect(() => {
setContentHeight(450);
}, [step]);
@@ -109,22 +136,38 @@ export default function CreateAccumulationAddressModal({
if (step === 'destination') {
setSelectedDestination(null);
setStep('chain');
goToStep('chain');
return true;
}
if (step === 'confirm') {
setSelectedDestination(forcedDestination || null);
setStep(forcedDestination ? 'chain' : 'destination');
goToStep(forcedDestination ? 'chain' : 'destination');
return true;
}
// 'chain' step — let the modal close naturally
return false;
}, [isCreating, step, forcedDestination]);
}, [isCreating, step, forcedDestination, goToStep]);
useHandleBackPressNew(handleBackPress);
// Register the chrome's back arrow whenever past the first step.
useEffect(() => {
if (step === 'chain') {
setBackNav?.(null);
} else {
setBackNav?.({
onPress: handleBackPress,
title:
step === 'destination'
? t('screens.accumulationAddresses.create.pickDestination')
: t('screens.accumulationAddresses.create.confirm'),
});
}
return () => setBackNav?.(null);
}, [step, handleBackPress, setBackNav]);
const handleCreate = useCallback(async () => {
setIsCreating(true);
const result = await createAddress({
@@ -150,15 +193,21 @@ export default function CreateAccumulationAddressModal({
t,
]);
// ── Chain step ──────────────────────────────────────────────────────────────
if (renderedStep === 'chain') {
return (
<Animated.View style={[styles.container, pageAnimatedStyle]}>
return (
<View style={styles.container}>
{/* Chain step */}
<Animated.View
style={[styles.animatedContainer, chainAnimatedStyle]}
pointerEvents={step === 'chain' ? 'auto' : 'none'}
>
<ThemeText
styles={styles.stepTitle}
content={t('screens.accumulationAddresses.create.pickChain')}
/>
<ScrollView showsVerticalScrollIndicator={false}>
<ScrollView
contentContainerStyle={{ paddingBottom: bottomPadding }}
showsVerticalScrollIndicator={false}
>
{ACCUMULATION_CHAINS.map(chain => (
<ChainRow
key={chain.id}
@@ -170,7 +219,8 @@ export default function CreateAccumulationAddressModal({
onSelectAsset={(c, asset) => {
setSelectedChain(c);
setSelectedAsset(asset);
setStep(forcedDestination ? 'confirm' : 'destination');
goToStep(forcedDestination ? 'confirm' : 'destination');
setExpandedChain(null);
}}
isAssetTaken={
forcedDestination
@@ -192,268 +242,176 @@ export default function CreateAccumulationAddressModal({
))}
</ScrollView>
</Animated.View>
);
}
// ── Destination step ────────────────────────────────────────────────────────
if (renderedStep === 'destination') {
return (
<Animated.View style={[styles.container, pageAnimatedStyle]}>
<ThemeText
styles={styles.stepTitle}
content={t('screens.accumulationAddresses.create.pickDestination')}
/>
{ACCUMULATION_DESTINATIONS.map(dest => {
const taken = isPairTaken(selectedChain?.id, selectedAsset, dest);
return (
<TouchableOpacity
key={dest}
activeOpacity={taken ? HIDDEN_OPACITY : 0.2}
style={[
styles.optionRow,
{ opacity: taken ? HIDDEN_OPACITY : 1 },
]}
onPress={() => {
if (taken) {
navigate.navigate('ErrorScreen', {
errorMessage: t(
'screens.accumulationAddresses.create.alreadyExists',
),
});
return;
}
setSelectedDestination(dest);
setStep('confirm');
}}
>
<View
style={[
styles.iconContainer,
{
backgroundColor:
theme && darkModeType
? darkModeType
? backgroundColor
: backgroundOffset
: dest === 'BTC'
? COLORS.bitcoinOrange
: COLORS.dollarGreen,
},
]}
>
<ThemeImage
styles={{ width: 25, height: 25 }}
lightModeIcon={
dest === 'BTC' ? ICONS.bitcoinIcon : ICONS.dollarIcon
}
darkModeIcon={
dest === 'BTC' ? ICONS.bitcoinIcon : ICONS.dollarIcon
}
lightsOutIcon={
dest === 'BTC' ? ICONS.bitcoinIcon : ICONS.dollarIcon
}
/>
</View>
<ThemeText
styles={styles.optionLabel}
content={
dest === 'BTC'
? t('constants.bitcoin_upper')
: t('constants.dollars_upper')
}
/>
<ThemeIcon iconName="ChevronRight" size={18} />
</TouchableOpacity>
);
})}
</Animated.View>
);
}
// ── Confirm step ────────────────────────────────────────────────────────────
return (
<Animated.View style={[styles.container, pageAnimatedStyle]}>
<ThemeText
styles={styles.stepTitle}
content={t('screens.accumulationAddresses.create.confirm')}
/>
<View style={styles.confirmContent}>
<View style={styles.assetRow}>
<View style={styles.confirmIconWrapper}>
<View
style={[
styles.confirmChainCircle,
{ backgroundColor: backgroundOffset },
]}
>
<Image
style={styles.confirmChainIcon}
source={ICONS[`chain_${selectedChain?.label.toLowerCase()}`]}
contentFit="contain"
/>
</View>
<View
style={[
styles.confirmCurrencyBadge,
{ borderColor: backgroundColor },
]}
>
<Image
style={styles.confirmCurrencyIcon}
source={ICONS[`${selectedAsset?.toLowerCase()}Logo`]}
contentFit="contain"
/>
</View>
</View>
<ThemeIcon
styles={{ opacity: HIDDEN_OPACITY }}
iconName={'ArrowRight'}
/>
<View style={styles.confirmIconWrapper}>
<View
style={[
styles.confirmChainCircle,
{
backgroundColor:
theme && darkModeType
? backgroundColor
: selectedDestination === 'BTC'
? COLORS.bitcoinOrange
: COLORS.dollarGreen,
},
]}
>
<Image
style={[styles.confirmChainIcon, { width: 50, height: 50 }]}
source={
ICONS[
selectedDestination === 'BTC' ? 'bitcoinIcon' : 'dollarIcon'
]
}
contentFit="contain"
/>
</View>
</View>
</View>
<ThemeText
styles={styles.confirmChainName}
content={selectedChain?.label}
/>
<ThemeText
styles={styles.confirmSubtitle}
content={t('screens.accumulationAddresses.create.convertDesc', {
chain: selectedChain?.label,
asset: selectedAsset,
receiveCurrency:
selectedDestination === 'BTC'
? t('constants.bitcoin_upper')
: t('constants.dollars_upper'),
})}
/>
</View>
<CustomButton
buttonStyles={[styles.createBtn, { marginBottom: bottomPadding }]}
textContent={t('screens.accumulationAddresses.summary.create')}
actionFunction={handleCreate}
useLoading={isCreating}
/>
</Animated.View>
);
}
function ChainRow({
chain,
expanded,
onToggleExpand,
onSelectAsset,
isAssetTaken,
onDisabledAssetPress,
theme,
darkModeType,
backgroundColor,
backgroundOffset,
}) {
const expandHeight = useSharedValue(0);
const chevronRotation = useSharedValue(0);
useEffect(() => {
expandHeight.value = withTiming(expanded ? 1 : 0, { duration: 200 });
chevronRotation.value = withTiming(expanded ? 1 : 0, { duration: 200 });
}, [expanded]);
const expandedStyle = useAnimatedStyle(() => ({
height: expandHeight.value * (chain.assets.length * 65 + 16),
opacity: expandHeight.value,
}));
const chevronStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${chevronRotation.value * 180}deg` }],
}));
return (
<View>
<TouchableOpacity
activeOpacity={0.7}
style={styles.chainRow}
onPress={() => onToggleExpand(chain.id)}
>
<View
{mountedSteps.has('destination') && (
<Animated.View
style={[
styles.chainIconContainer,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
},
StyleSheet.absoluteFill,
styles.animatedContainer,
destinationAnimatedStyle,
]}
pointerEvents={step === 'destination' ? 'auto' : 'none'}
>
<Image
style={styles.assetIcon}
source={ICONS[`chain_${chain.label.toLowerCase()}`]}
contentFit="contain"
/>
</View>
<ThemeText styles={styles.optionLabel} content={chain.label} />
<View style={{ opacity: HIDDEN_OPACITY }}>
<Animated.View style={chevronStyle}>
<ThemeIcon iconName="ChevronDown" size={18} />
</Animated.View>
</View>
</TouchableOpacity>
<Animated.View style={[styles.assetOptionsContainer, expandedStyle]}>
{chain.assets.map(asset => {
const disabled = isAssetTaken(asset);
return (
<TouchableOpacity
key={asset}
activeOpacity={0.7}
style={[
styles.assetOptionRow,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
opacity: disabled ? HIDDEN_OPACITY : 1,
},
]}
onPress={() =>
disabled ? onDisabledAssetPress() : onSelectAsset(chain, asset)
}
>
<View style={styles.assetOptionIconContainer}>
<Image
style={styles.assetOptionIcon}
source={ICONS[`${asset.toLowerCase()}Logo`]}
contentFit="contain"
{ACCUMULATION_DESTINATIONS.map(dest => {
const taken = isPairTaken(selectedChain?.id, selectedAsset, dest);
return (
<TouchableOpacity
key={dest}
activeOpacity={taken ? HIDDEN_OPACITY : 0.2}
style={[
styles.optionRow,
{ opacity: taken ? HIDDEN_OPACITY : 1 },
]}
onPress={() => {
if (taken) {
navigate.navigate('ErrorScreen', {
errorMessage: t(
'screens.accumulationAddresses.create.alreadyExists',
),
});
return;
}
setSelectedDestination(dest);
goToStep('confirm');
}}
>
<View
style={[
styles.iconContainer,
{
backgroundColor:
theme && darkModeType
? darkModeType
? backgroundColor
: backgroundOffset
: dest === 'BTC'
? COLORS.bitcoinOrange
: COLORS.dollarGreen,
},
]}
>
<ThemeImage
styles={{ width: 25, height: 25 }}
lightModeIcon={
dest === 'BTC' ? ICONS.bitcoinIcon : ICONS.dollarIcon
}
darkModeIcon={
dest === 'BTC' ? ICONS.bitcoinIcon : ICONS.dollarIcon
}
lightsOutIcon={
dest === 'BTC' ? ICONS.bitcoinIcon : ICONS.dollarIcon
}
/>
</View>
<ThemeText
styles={styles.optionLabel}
content={
dest === 'BTC'
? t('constants.bitcoin_upper')
: t('constants.dollars_upper')
}
/>
<ThemeIcon iconName="ChevronRight" size={18} />
</TouchableOpacity>
);
})}
</Animated.View>
)}
{mountedSteps.has('confirm') && (
<Animated.View
style={[
StyleSheet.absoluteFill,
styles.animatedContainer,
confirmAnimatedStyle,
]}
pointerEvents={step === 'confirm' ? 'auto' : 'none'}
>
<View style={styles.confirmContent}>
<View style={styles.assetRow}>
<View style={styles.confirmIconWrapper}>
<View
style={[
styles.confirmChainCircle,
{ backgroundColor: backgroundOffset },
]}
>
<Image
style={styles.confirmChainIcon}
source={
ICONS[`chain_${selectedChain?.label.toLowerCase()}`]
}
contentFit="contain"
/>
</View>
<View
style={[
styles.confirmCurrencyBadge,
{ borderColor: backgroundColor },
]}
>
<Image
style={styles.confirmCurrencyIcon}
source={ICONS[`${selectedAsset?.toLowerCase()}Logo`]}
contentFit="contain"
/>
</View>
</View>
<ThemeText styles={styles.optionLabel} content={asset} />
</TouchableOpacity>
);
})}
</Animated.View>
<ThemeIcon styles={{ opacity: 0.7 }} iconName={'ArrowRight'} />
<View style={styles.confirmIconWrapper}>
<View
style={[
styles.confirmChainCircle,
{
backgroundColor:
theme && darkModeType
? backgroundColor
: selectedDestination === 'BTC'
? COLORS.bitcoinOrange
: COLORS.dollarGreen,
},
]}
>
<Image
style={[styles.confirmChainIcon, { width: 50, height: 50 }]}
source={
ICONS[
selectedDestination === 'BTC'
? 'bitcoinIcon'
: 'dollarIcon'
]
}
contentFit="contain"
/>
</View>
</View>
</View>
<ThemeText
styles={styles.confirmChainName}
content={selectedChain?.label}
/>
<ThemeText
styles={styles.confirmSubtitle}
content={t('screens.accumulationAddresses.create.convertDesc', {
chain: selectedChain?.label,
asset: selectedAsset,
receiveCurrency:
selectedDestination === 'BTC'
? t('constants.bitcoin_upper')
: t('constants.dollars_upper'),
})}
/>
</View>
<CustomButton
buttonStyles={[styles.createBtn, { marginBottom: bottomPadding }]}
textContent={t('screens.accumulationAddresses.summary.create')}
actionFunction={handleCreate}
useLoading={isCreating}
/>
</Animated.View>
)}
</View>
);
}
@@ -464,32 +422,20 @@ const styles = StyleSheet.create({
width: INSET_WINDOW_WIDTH,
...CENTER,
},
animatedContainer: {
flex: 1,
width: '100%',
},
stepTitle: {
fontSize: SIZES.large,
fontWeight: 500,
marginBottom: 8,
},
chainRow: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
},
chainIconContainer: {
width: 45,
height: 45,
borderRadius: 22.5,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
marginRight: 15,
},
optionRow: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 12,
paddingVertical: 10,
marginBottom: 8,
paddingBottom: 16,
gap: 10,
},
assetRow: {
@@ -0,0 +1,157 @@
import { Image } from 'expo-image';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { CENTER, ICONS } from '../../../../constants';
import { HIDDEN_OPACITY } from '../../../../constants/theme';
import ThemeIcon from '../../../../functions/CustomElements/themeIcon';
import { ThemeText } from '../../../../functions/CustomElements';
import { useEffect } from 'react';
export default function ChainRow({
chain,
expanded,
onToggleExpand,
onSelectAsset,
isAssetTaken,
onDisabledAssetPress,
theme,
darkModeType,
backgroundColor,
backgroundOffset,
}) {
const expandHeight = useSharedValue(0);
const chevronRotation = useSharedValue(0);
useEffect(() => {
expandHeight.value = withTiming(expanded ? 1 : 0, { duration: 200 });
chevronRotation.value = withTiming(expanded ? 1 : 0, { duration: 200 });
}, [expanded]);
const expandedStyle = useAnimatedStyle(() => ({
height: expandHeight.value * (chain.assets.length * 65 + 26),
opacity: expandHeight.value,
}));
const chevronStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${chevronRotation.value * 180}deg` }],
}));
return (
<View>
<TouchableOpacity
activeOpacity={0.7}
style={styles.chainRow}
onPress={() => onToggleExpand(chain.id)}
>
<View
style={[
styles.chainIconContainer,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
},
]}
>
<Image
style={styles.assetIcon}
source={ICONS[`chain_${chain.label.toLowerCase()}`]}
contentFit="contain"
/>
</View>
<ThemeText styles={styles.optionLabel} content={chain.label} />
<Animated.View style={[{ opacity: HIDDEN_OPACITY }, chevronStyle]}>
<ThemeIcon iconName="ChevronDown" size={18} />
</Animated.View>
</TouchableOpacity>
<Animated.View style={[styles.assetOptionsContainer, expandedStyle]}>
{chain.assets.map(asset => {
const disabled = isAssetTaken(asset);
return (
<TouchableOpacity
key={asset}
activeOpacity={0.7}
style={[
styles.assetOptionRow,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
opacity: disabled ? HIDDEN_OPACITY : 1,
},
]}
onPress={() =>
disabled ? onDisabledAssetPress() : onSelectAsset(chain, asset)
}
>
<View style={styles.assetOptionIconContainer}>
<Image
style={styles.assetOptionIcon}
source={ICONS[`${asset.toLowerCase()}Logo`]}
contentFit="contain"
/>
</View>
<ThemeText styles={styles.optionLabel} content={asset} />
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
chainRow: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
// paddingVertical: 8,
},
chainIconContainer: {
width: 45,
height: 45,
borderRadius: 22.5,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
marginRight: 15,
},
optionLabel: {
flex: 1,
includeFontPadding: false,
},
assetIcon: {
width: 45,
height: 45,
},
assetOptionsContainer: {
overflow: 'hidden',
gap: 12,
paddingTop: 8,
paddingBottom: 8,
},
assetOptionRow: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 14,
borderRadius: 12,
gap: 12,
},
assetOptionIconContainer: {
width: 35,
height: 35,
borderRadius: 17.5,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
},
assetOptionIcon: {
width: 35,
height: 35,
},
});
@@ -48,6 +48,7 @@ export default function OnlineListingsFilterHalfModal({
handleBackPressFunction,
setContentHeight,
categoryOptions = [],
setBackNav,
}) {
const { t } = useTranslation();
const { bottomPadding } = useGlobalInsets();
@@ -238,6 +239,16 @@ export default function OnlineListingsFilterHalfModal({
useHandleBackPressNew(handleCountryBackPress);
// Register the chrome's back arrow while the country picker is open.
useEffect(() => {
if (showCountryPicker) {
setBackNav?.({ onPress: handleCountryBackPress, title: '' });
} else {
setBackNav?.(null);
}
return () => setBackNav?.(null);
}, [showCountryPicker, handleCountryBackPress, setBackNav]);
const renderCountryItem = useCallback(
({ item }) => {
const isSelected = draft.countryCode === item.code;
@@ -325,11 +336,6 @@ export default function OnlineListingsFilterHalfModal({
{ paddingBottom: bottomPadding },
]}
>
<ThemeText
styles={styles.title}
content={t('apps.onlineListings.filterTitle')}
/>
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
@@ -514,16 +520,6 @@ export default function OnlineListingsFilterHalfModal({
countryOverlayStyle,
]}
>
<View style={styles.countryOverlayHeader}>
<TouchableOpacity
activeOpacity={0.7}
onPress={handleCountryBackPress}
style={styles.countryOverlayBack}
>
<ThemeIcon iconName="ArrowLeft" />
</TouchableOpacity>
</View>
<CustomSearchInput
inputText={countrySearch}
setInputText={setCountrySearch}
@@ -659,18 +655,6 @@ const styles = StyleSheet.create({
countryOverlay: {
...StyleSheet.absoluteFillObject,
},
countryOverlayHeader: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 16,
},
countryOverlayBack: {
// width: 32,
alignItems: 'center',
justifyContent: 'center',
},
searchInput: {
width: '100%',
paddingBottom: CONTENT_KEYBOARD_OFFSET,
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import Animated from 'react-native-reanimated';
import { ThemeText } from '../../../../functions/CustomElements';
import {
CENTER,
@@ -24,9 +25,12 @@ import { useGlobalContextProvider } from '../../../../../context-store/context';
import { useFlashnet } from '../../../../../context-store/flashnetContext';
import { dollarsToSats } from '../../../../functions/spark/flashnet';
import useHandleBackPressNew from '../../../../hooks/useHandleBackPressNew';
import useHalfModalStepTransition from '../../../../hooks/useHalfModalStepTransition';
import displayCorrectDenomination from '../../../../functions/displayCorrectDenomination';
import { useNodeContext } from '../../../../../context-store/nodeContext';
const STEP_ORDER = ['select', 'custom'];
export default function AddGiftQuantityHalfModal({
amount,
amountValue,
@@ -34,6 +38,7 @@ export default function AddGiftQuantityHalfModal({
giftDenomination,
setContentHeight,
handleBackPressFunction,
setBackNav,
}) {
const navigate = useNavigation();
const { t } = useTranslation();
@@ -47,6 +52,11 @@ export default function AddGiftQuantityHalfModal({
const [step, setStep] = useState('select');
const [customValue, setCustomValue] = useState('');
const { renderedPage, pageAnimatedStyle } = useHalfModalStepTransition(
step,
STEP_ORDER,
);
const currentDerivedGiftIndex = masterInfoObject.currentDerivedGiftIndex || 1;
useEffect(() => {
@@ -64,6 +74,16 @@ export default function AddGiftQuantityHalfModal({
useHandleBackPressNew(handleBackPress);
// Register the chrome's back arrow while on the custom-quantity step.
useEffect(() => {
if (step === 'custom') {
setBackNav?.({ onPress: handleBackPress, title: '' });
} else {
setBackNav?.(null);
}
return () => setBackNav?.(null);
}, [step, handleBackPress, setBackNav]);
const validateAndProceed = quantity => {
if (!quantity || quantity <= 0) return;
@@ -229,12 +249,12 @@ export default function AddGiftQuantityHalfModal({
);
};
if (step === 'custom') {
if (renderedPage === 'custom') {
const customQty = parseInt(customValue, 10) || 0;
const isValid = customQty >= 1 && customQty <= MAX_GIFT_QUANTITY;
return (
<View style={styles.container}>
<Animated.View style={[styles.container, pageAnimatedStyle]}>
<View style={styles.inputContainer}>
<ThemeText
styles={styles.quantityDisplay}
@@ -274,12 +294,12 @@ export default function AddGiftQuantityHalfModal({
textContent={isValid ? t('constants.continue') : t('constants.back')}
actionFunction={handleCustomContinue}
/>
</View>
</Animated.View>
);
}
return (
<View style={styles.container}>
<Animated.View style={[styles.container, pageAnimatedStyle]}>
<ThemeText
styles={styles.header}
content={t('screens.inAccount.giftPages.createGift.quantityHeader')}
@@ -291,7 +311,7 @@ export default function AddGiftQuantityHalfModal({
</View>
))}
</View>
</View>
</Animated.View>
);
}
@@ -31,7 +31,6 @@ import {
INFINITY_SYMBOL,
TOKEN_TICKER_MAX_LENGTH,
} from '../../../../constants';
import CustomButton from '../../../../functions/CustomElements/button';
import { useGlobalThemeContext } from '../../../../../context-store/theme';
import useHandleBackPressNew from '../../../../hooks/useHandleBackPressNew';
import openWebBrowser from '../../../../functions/openWebBrowser';
@@ -125,13 +124,7 @@ function TokenListItem({
// ─── Token Detail View ───────────────────────────────────────────────────────
function TokenDetailView({
token,
tokenIdentifier,
onBack,
theme,
darkModeType,
}) {
function TokenDetailView({ token, tokenIdentifier, theme, darkModeType }) {
const navigate = useNavigation();
const { tokensImageCache } = useSparkWallet();
const { masterInfoObject } = useGlobalContextProvider();
@@ -368,13 +361,6 @@ function TokenDetailView({
content={t('screens.inAccount.lrc20TokenDataHalfModal.tokenInfo')}
/>
</TouchableOpacity>
{/* Back button */}
<CustomButton
actionFunction={onBack}
buttonStyles={{ ...CENTER, marginTop: 'auto' }}
textContent={t('constants.back')}
/>
</Animated.ScrollView>
);
}
@@ -384,6 +370,7 @@ function TokenDetailView({
export default function ViewAllTokensHalfModal({
setContentHeight,
handleBackPressFunction,
setBackNav,
}) {
const { theme, darkModeType } = useGlobalThemeContext();
const { sparkInformation } = useSparkWallet();
@@ -413,6 +400,16 @@ export default function ViewAllTokensHalfModal({
useHandleBackPressNew(customBackHandler);
// Register the chrome's back arrow while viewing a token's detail page.
useEffect(() => {
if (selectedIdentifier && selectedToken) {
setBackNav?.({ onPress: handleBack, title: '' });
} else {
setBackNav?.(null);
}
return () => setBackNav?.(null);
}, [selectedIdentifier, selectedToken, setBackNav]);
useEffect(() => {
setContentHeight(selectedIdentifier && selectedToken ? 700 : 500);
}, [selectedIdentifier, selectedToken]);
@@ -423,7 +420,6 @@ export default function ViewAllTokensHalfModal({
<TokenDetailView
token={selectedToken}
tokenIdentifier={selectedIdentifier}
onBack={handleBack}
theme={theme}
darkModeType={darkModeType}
/>
@@ -478,8 +474,8 @@ const styles = StyleSheet.create({
sectionTitle: {
fontSize: SIZES.large,
fontWeight: 500,
marginBottom: 14,
textAlign: 'center',
},
tokenList: {
flexGrow: 1,
@@ -1,7 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { ThemeText } from '../../../../functions/CustomElements';
import { CENTER, SIZES, SATSPERBITCOIN } from '../../../../constants';
import { CENTER, SIZES } from '../../../../constants';
import { useTranslation } from 'react-i18next';
import { HIDDEN_OPACITY } from '../../../../constants/theme';
import { useNavigation } from '@react-navigation/native';
@@ -44,11 +49,14 @@ import { simulateSwap } from '../../../../functions/spark/flashnet';
const confirmTxAnimation = require('../../../../assets/confirmTxAnimation.json');
const STEP_ORDER = ['select', 'custom', 'confirm', 'loading', 'success'];
export default function ContributeToPoolHalfModal({
pool,
poolId,
setContentHeight,
handleBackPressFunction,
setBackNav,
}) {
const navigate = useNavigation();
const { bitcoinBalance, dollarBalanceSat, dollarBalanceToken } =
@@ -73,6 +81,96 @@ export default function ContributeToPoolHalfModal({
);
// extract current page for easier handling
const currentPage = step[step.length - 1];
const previousPageRef = useRef(currentPage);
const selectOpacity = useSharedValue(1);
const selectTranslateX = useSharedValue(0);
const customOpacity = useSharedValue(0);
const customTranslateX = useSharedValue(30);
const confirmOpacity = useSharedValue(0);
const confirmTranslateX = useSharedValue(30);
const loadingOpacity = useSharedValue(0);
const loadingTranslateX = useSharedValue(30);
const successOpacity = useSharedValue(0);
const successTranslateX = useSharedValue(30);
const getStepAnimation = useCallback(
page => {
switch (page) {
case 'custom':
return { opacity: customOpacity, translateX: customTranslateX };
case 'confirm':
return { opacity: confirmOpacity, translateX: confirmTranslateX };
case 'loading':
return { opacity: loadingOpacity, translateX: loadingTranslateX };
case 'success':
return { opacity: successOpacity, translateX: successTranslateX };
case 'select':
default:
return { opacity: selectOpacity, translateX: selectTranslateX };
}
},
[
confirmOpacity,
confirmTranslateX,
customOpacity,
customTranslateX,
loadingOpacity,
loadingTranslateX,
selectOpacity,
selectTranslateX,
successOpacity,
successTranslateX,
],
);
useEffect(() => {
const previousPage = previousPageRef.current;
if (previousPage === currentPage) return;
const previousIndex = STEP_ORDER.indexOf(previousPage);
const currentIndex = STEP_ORDER.indexOf(currentPage);
const isForward = currentIndex > previousIndex;
const previousAnimation = getStepAnimation(previousPage);
const currentAnimation = getStepAnimation(currentPage);
previousAnimation.opacity.value = withTiming(0, { duration: 250 });
previousAnimation.translateX.value = withTiming(isForward ? -30 : 30, {
duration: 250,
});
currentAnimation.opacity.value = 0;
currentAnimation.translateX.value = isForward ? 30 : -30;
currentAnimation.opacity.value = withTiming(1, { duration: 250 });
currentAnimation.translateX.value = withTiming(0, { duration: 250 });
previousPageRef.current = currentPage;
}, [currentPage, getStepAnimation]);
const selectAnimatedStyle = useAnimatedStyle(() => ({
opacity: selectOpacity.value,
transform: [{ translateX: selectTranslateX.value }],
}));
const customAnimatedStyle = useAnimatedStyle(() => ({
opacity: customOpacity.value,
transform: [{ translateX: customTranslateX.value }],
}));
const confirmAnimatedStyle = useAnimatedStyle(() => ({
opacity: confirmOpacity.value,
transform: [{ translateX: confirmTranslateX.value }],
}));
const loadingAnimatedStyle = useAnimatedStyle(() => ({
opacity: loadingOpacity.value,
transform: [{ translateX: loadingTranslateX.value }],
}));
const successAnimatedStyle = useAnimatedStyle(() => ({
opacity: successOpacity.value,
transform: [{ translateX: successTranslateX.value }],
}));
const normalizedInputDenomination = inputDenomination
? inputDenomination
@@ -123,11 +221,24 @@ export default function ContributeToPoolHalfModal({
useHandleBackPressNew(handleBackPress);
// Register the chrome's back arrow whenever a previous step exists.
useEffect(() => {
if (
step.length > 1 &&
currentPage !== 'loading' &&
currentPage !== 'success'
) {
setBackNav?.({ onPress: handleBackPress, title: '' });
} else {
setBackNav?.(null);
}
return () => setBackNav?.(null);
}, [step, currentPage, handleBackPress, setBackNav]);
const isFiatMode = masterInfoObject.userBalanceDenomination === 'fiat';
// Convert current keyboard input to sats (used only in custom step)
const localSatAmount = convertDisplayToSats(amountValue);
const localFiatAmount = convertSatsToDisplay(localSatAmount);
const effectiveSats =
currentPage === 'custom' ? localSatAmount : selectedAmountSats;
@@ -143,18 +254,8 @@ export default function ContributeToPoolHalfModal({
};
useEffect(() => {
if (currentPage === 'select') {
setContentHeight(500);
} else if (
currentPage === 'confirm' ||
currentPage === 'loading' ||
currentPage === 'success'
) {
setContentHeight(500);
} else {
setContentHeight(550);
}
}, [currentPage]);
setContentHeight(550);
}, []);
const confirmAnimation = useMemo(() => {
return updateConfirmAnimation(
@@ -342,79 +443,61 @@ export default function ContributeToPoolHalfModal({
dollarBalanceToken,
]);
if (currentPage === 'loading') {
return (
<View style={styles.stepContainer}>
<FullLoadingScreen text={t('wallet.pools.sendingContribution')} />
</View>
);
}
const displayAmount = isFiatMode
? convertSatsToDisplay(effectiveSats)
: effectiveSats;
const stepBackgroundStyle = {
backgroundColor: theme && darkModeType ? backgroundOffset : backgroundColor,
};
const layerChrome = page => ({
zIndex: currentPage === page ? 2 : 1,
});
const layerPointerEvents = page => (currentPage === page ? 'auto' : 'none');
if (currentPage === 'success') {
return (
<View style={[styles.stepContainer, styles.successContainer]}>
<LottieView
source={confirmAnimation}
loop={false}
style={styles.lottieView}
autoPlay={true}
/>
return (
<View style={styles.container}>
<Animated.View
style={[
StyleSheet.absoluteFill,
styles.stepContainer,
stepBackgroundStyle,
selectAnimatedStyle,
layerChrome('select'),
]}
pointerEvents={layerPointerEvents('select')}
>
<ThemeText
styles={styles.successText}
content={t('wallet.pools.contributionSent')}
styles={styles.selectTitle}
content={t('wallet.pools.chooseContributionAmount')}
/>
<PresetAmountGrid
onSelectPreset={setSelectedAmountSats}
selectedAmountSats={selectedAmountSats}
fiatStats={fiatStats}
onCustomPress={handleCustomPress}
/>
<CustomButton
actionFunction={handleBackPressFunction}
textContent={t('constants.back')}
buttonStyles={[
styles.continueButton,
{ opacity: !selectedAmountSats ? HIDDEN_OPACITY : 1 },
]}
textContent={t('constants.continue')}
actionFunction={handleContinue}
/>
</View>
);
}
</Animated.View>
if (currentPage === 'confirm') {
const displayAmount = isFiatMode ? localFiatAmount : effectiveSats;
return (
<View style={styles.stepContainer}>
<FormattedBalanceInput
maxWidth={0.9}
amountValue={displayAmount}
inputDenomination={isFiatMode ? 'fiat' : 'sats'}
/>
<ThemeText
styles={styles.confirmPoolTitle}
content={`${t('wallet.pools.contributeTo')}${pool.poolTitle}`}
/>
<ThemeText
styles={styles.infoItem}
content={t('wallet.pools.contributionWarning')}
/>
<SwipeButtonNew
onSwipeSuccess={handleConfirmPayment}
width={0.95}
containerStyles={{ marginBottom: 12 }}
thumbIconStyles={{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
borderColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
}}
railStyles={{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
borderColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
}}
/>
</View>
);
}
if (currentPage === 'custom') {
return (
<View style={styles.stepContainer}>
<Animated.View
style={[
StyleSheet.absoluteFill,
styles.stepContainer,
stepBackgroundStyle,
customAnimatedStyle,
layerChrome('custom'),
]}
pointerEvents={layerPointerEvents('custom')}
>
<TouchableOpacity
style={{ marginTop: 10 }}
activeOpacity={1}
@@ -450,38 +533,101 @@ export default function ContributeToPoolHalfModal({
Number(amountValue) ? t('constants.continue') : t('constants.back')
}
/>
</View>
);
}
</Animated.View>
// Step: 'select'
return (
<View style={styles.stepContainer}>
<ThemeText
styles={styles.selectTitle}
content={t('wallet.pools.chooseContributionAmount')}
/>
<PresetAmountGrid
onSelectPreset={setSelectedAmountSats}
selectedAmountSats={selectedAmountSats}
fiatStats={fiatStats}
onCustomPress={handleCustomPress}
/>
<CustomButton
buttonStyles={[
styles.continueButton,
{ opacity: !selectedAmountSats ? HIDDEN_OPACITY : 1 },
<Animated.View
style={[
StyleSheet.absoluteFill,
styles.stepContainer,
stepBackgroundStyle,
confirmAnimatedStyle,
layerChrome('confirm'),
]}
textContent={t('constants.continue')}
actionFunction={handleContinue}
/>
pointerEvents={layerPointerEvents('confirm')}
>
<FormattedBalanceInput
maxWidth={0.9}
amountValue={displayAmount}
inputDenomination={isFiatMode ? 'fiat' : 'sats'}
/>
<ThemeText
styles={styles.confirmPoolTitle}
content={`${t('wallet.pools.contributeTo')}${pool.poolTitle}`}
/>
<ThemeText
styles={styles.infoItem}
content={t('wallet.pools.contributionWarning')}
/>
<SwipeButtonNew
onSwipeSuccess={handleConfirmPayment}
width={0.9}
containerStyles={{ marginBottom: 12 }}
thumbIconStyles={{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
borderColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
}}
railStyles={{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
borderColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
}}
/>
</Animated.View>
<Animated.View
style={[
StyleSheet.absoluteFill,
styles.stepContainer,
stepBackgroundStyle,
loadingAnimatedStyle,
layerChrome('loading'),
]}
pointerEvents={layerPointerEvents('loading')}
>
<FullLoadingScreen text={t('wallet.pools.sendingContribution')} />
</Animated.View>
<Animated.View
style={[
StyleSheet.absoluteFill,
styles.stepContainer,
styles.successContainer,
stepBackgroundStyle,
successAnimatedStyle,
layerChrome('success'),
]}
pointerEvents={layerPointerEvents('success')}
>
{currentPage === 'success' && (
<LottieView
source={confirmAnimation}
loop={false}
style={styles.lottieView}
autoPlay={true}
/>
)}
<ThemeText
styles={styles.successText}
content={t('wallet.pools.contributionSent')}
/>
<CustomButton
actionFunction={handleBackPressFunction}
textContent={t('constants.back')}
/>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
stepContainer: {
flex: 1,
paddingHorizontal: 16,
@@ -70,6 +70,7 @@ export default function AddMoneyToSavingsHalfModal({
setContentHeight,
handleBackPressFunction,
selectedGoalUUID,
setBackNav,
}) {
const navigate = useNavigation();
const { t } = useTranslation();
@@ -211,6 +212,20 @@ export default function AddMoneyToSavingsHalfModal({
useHandleBackPressNew(handleBackPress);
// Register the chrome's back arrow whenever a previous step exists.
useEffect(() => {
if (
step.length > 1 &&
currentPage !== 'loading' &&
currentPage !== 'success'
) {
setBackNav?.({ onPress: handleBackPress, title: '' });
} else {
setBackNav?.(null);
}
return () => setBackNav?.(null);
}, [step, currentPage, handleBackPress, setBackNav]);
const handleConfirm = async () => {
setStep(prev => [...prev, 'loading']);
setLoadingStep('processing');
@@ -103,6 +103,7 @@ export default function WithdrawFromSavingsHalfModal({
setContentHeight,
handleBackPressFunction,
selectedGoalUUID,
setBackNav,
}) {
const navigate = useNavigation();
const { t } = useTranslation();
@@ -379,6 +380,20 @@ export default function WithdrawFromSavingsHalfModal({
useHandleBackPressNew(handleBackPress);
// Register the chrome's back arrow whenever a previous step exists.
useEffect(() => {
if (
step.length > 1 &&
currentPage !== 'loading' &&
currentPage !== 'success'
) {
setBackNav?.({ onPress: handleBackPress, title: '' });
} else {
setBackNav?.(null);
}
return () => setBackNav?.(null);
}, [step, currentPage, handleBackPress, setBackNav]);
const parsedAmount = Number(amountValue || 0);
const handleConfirm = async () => {
File diff suppressed because it is too large Load Diff
@@ -555,6 +555,7 @@ export default function CustomHalfModal(props) {
poolId={props?.route?.params?.poolId}
setContentHeight={setContentHeight}
handleBackPressFunction={handleBackPressFunction}
setBackNav={setBackNav}
/>
);
case 'addGiftQuantity':
@@ -566,6 +567,7 @@ export default function CustomHalfModal(props) {
giftDenomination={props?.route?.params?.giftDenomination}
setContentHeight={setContentHeight}
handleBackPressFunction={handleBackPressFunction}
setBackNav={setBackNav}
/>
);
case 'addMoneyToSavings':
@@ -575,6 +577,7 @@ export default function CustomHalfModal(props) {
selectedGoalUUID={props?.route?.params?.selectedGoalUUID}
setContentHeight={setContentHeight}
handleBackPressFunction={handleBackPressFunction}
setBackNav={setBackNav}
/>
</SavingsProvider>
);
@@ -586,6 +589,7 @@ export default function CustomHalfModal(props) {
selectedGoalUUID={props?.route?.params?.selectedGoalUUID}
setContentHeight={setContentHeight}
handleBackPressFunction={handleBackPressFunction}
setBackNav={setBackNav}
/>
</SavingsProvider>
);
@@ -602,6 +606,7 @@ export default function CustomHalfModal(props) {
<ViewAllTokensHalfModal
handleBackPressFunction={handleBackPressFunction}
setContentHeight={setContentHeight}
setBackNav={setBackNav}
/>
);
@@ -618,6 +623,7 @@ export default function CustomHalfModal(props) {
<SwapFlowHalfModal
setContentHeight={setContentHeight}
handleBackPressFunction={handleBackPressFunction}
setBackNav={setBackNav}
/>
);
case 'txFilter':
@@ -638,6 +644,7 @@ export default function CustomHalfModal(props) {
handleBackPressFunction={handleBackPressFunction}
setContentHeight={setContentHeight}
setIsKeyboardActive={setIsKeyboardActive}
setBackNav={setBackNav}
/>
);
case 'payLinkCurrencySelect':
@@ -670,6 +677,7 @@ export default function CustomHalfModal(props) {
<CreateAccumulationAddressModal
handleBackPressFunction={handleBackPressFunction}
setContentHeight={setContentHeight}
setBackNav={setBackNav}
/>
);
case 'RemoveBudgetHalfModal':
+56
View File
@@ -0,0 +1,56 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
import { scheduleOnRN } from 'react-native-worklets';
/**
* Standard half-modal step transition (fade + directional translateX), modeled on
* CreateAccumulationAddressModal. Lets step-based flows that render via early-return
* branches animate between steps without duplicating the animation logic.
*
* @param {string} page The active page key.
* @param {string[]} order Ordered list of page keys defines slide direction.
* @returns {{ renderedPage: string, pageAnimatedStyle: object }} Render JSX off
* renderedPage and wrap it in an Animated.View with pageAnimatedStyle. Keep all
* logic/height/back behavior on the live `page`.
*/
export default function useHalfModalStepTransition(page, order) {
const opacity = useSharedValue(1);
const translateX = useSharedValue(0);
const [renderedPage, setRenderedPage] = useState(page);
const renderedRef = useRef(page);
const animateIn = useCallback(
(next, forward) => {
renderedRef.current = next;
setRenderedPage(next);
translateX.value = forward ? 30 : -30;
opacity.value = withTiming(1, { duration: 125 });
translateX.value = withTiming(0, { duration: 125 });
},
[opacity, translateX],
);
useEffect(() => {
if (page === renderedRef.current) return;
const forward = order.indexOf(page) > order.indexOf(renderedRef.current);
opacity.value = withTiming(0, { duration: 125 });
translateX.value = withTiming(
forward ? -30 : 30,
{ duration: 125 },
fin => {
if (fin) scheduleOnRN(animateIn, page, forward);
},
);
}, [page]);
const pageAnimatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ translateX: translateX.value }],
}));
return { renderedPage, pageAnimatedStyle };
}