adding confirm page for contact payments (#966)

* adding confirm page for contact payments

* improve confirm style
This commit is contained in:
Blake Kaufman
2026-06-25 18:12:59 -04:00
committed by GitHub
parent c3a64bc546
commit 48c4eaad5d
9 changed files with 172 additions and 13 deletions
@@ -1,12 +1,18 @@
import { useCallback, useEffect } from 'react';
import { StyleSheet } from 'react-native';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
import LottieView from 'lottie-react-native';
import { CENTER, SIZES } from '../../../../constants';
import {
HIDDEN_OPACITY,
INSET_WINDOW_WIDTH,
} from '../../../../constants/theme';
import { useGlobalInsets } from '../../../../../context-store/insetsProvider';
import GetThemeColors from '../../../../hooks/themeColors';
import useHandleBackPressNew from '../../../../hooks/useHandleBackPressNew';
@@ -14,6 +20,12 @@ import ContactAmountEntry from './internalComponents/contactAmountEntry';
import useContactPayment from './hooks/useContactPayment';
import { useAppStatus } from '../../../../../context-store/appStatus';
import CurrencySwitchButton from '../../../../functions/CustomElements/currencySwitchButton';
import { ThemeText } from '../../../../functions/CustomElements';
import CustomButton from '../../../../functions/CustomElements/button';
import FormattedBalanceInput from '../../../../functions/CustomElements/formattedBalanceInput';
import { updateConfirmAnimation } from '../../../../functions/lottieViewColorTransformer';
const confirmTxAnimation = require('../../../../assets/confirmTxAnimation.json');
export default function ContactPaymentOverlay({
visible,
@@ -43,22 +55,71 @@ export default function ContactPaymentOverlay({
t,
});
// Snapshot of the just-submitted request, captured at submit time so the
// confirmation view keeps showing the right amount even if the payment hook's
// state changes afterwards. null = still on the amount-entry step.
const [successData, setSuccessData] = useState(null);
const overlayOpacity = useSharedValue(0);
const entryOpacity = useSharedValue(1);
const entryTranslateX = useSharedValue(0);
const successOpacity = useSharedValue(0);
const successTranslateX = useSharedValue(30);
useEffect(() => {
overlayOpacity.value = withTiming(visible ? 1 : 0, { duration: 250 });
}, [visible]);
// Cross-fade between the amount-entry step and the confirmation step.
useEffect(() => {
const showingSuccess = successData !== null;
entryOpacity.value = withTiming(showingSuccess ? 0 : 1, { duration: 250 });
entryTranslateX.value = withTiming(showingSuccess ? -30 : 0, {
duration: 250,
});
successOpacity.value = withTiming(showingSuccess ? 1 : 0, {
duration: 250,
});
successTranslateX.value = withTiming(showingSuccess ? 0 : 30, {
duration: 250,
});
}, [successData]);
const overlayStyle = useAnimatedStyle(() => ({
opacity: overlayOpacity.value,
pointerEvents: overlayOpacity.value > 0.5 ? 'auto' : 'none',
}));
const entryStyle = useAnimatedStyle(() => ({
opacity: entryOpacity.value,
transform: [{ translateX: entryTranslateX.value }],
}));
const successStyle = useAnimatedStyle(() => ({
opacity: successOpacity.value,
transform: [{ translateX: successTranslateX.value }],
}));
const confirmAnimation = useMemo(
() =>
updateConfirmAnimation(
confirmTxAnimation,
theme ? (darkModeType ? 'lightsOut' : 'dark') : 'light',
),
[theme, darkModeType],
);
const handleBackPress = useCallback(() => {
if (!visible) return false;
// Once the request has been sent there's nothing to step back to — close
// the whole modal, matching the Done button.
if (successData !== null) {
handleBackPressFunction();
return true;
}
onClose();
return true;
}, [onClose, visible]);
}, [onClose, visible, successData, handleBackPressFunction]);
const openCurrencyPicker = useCallback(
() =>
@@ -77,13 +138,15 @@ export default function ContactPaymentOverlay({
setBackNav?.({
onPress: handleBackPress,
title: '',
rightElement: (
<CurrencySwitchButton
displayCurrency={payment.displayCurrency}
onPress={openCurrencyPicker}
disabled={payment.isResolvingDisplayCurrency}
/>
),
// The currency switcher is meaningless on the confirmation step.
rightElement:
successData !== null ? null : (
<CurrencySwitchButton
displayCurrency={payment.displayCurrency}
onPress={openCurrencyPicker}
disabled={payment.isResolvingDisplayCurrency}
/>
),
});
return () => {
setBackNav?.(null);
@@ -93,6 +156,7 @@ export default function ContactPaymentOverlay({
handleBackPress,
setBackNav,
visible,
successData,
payment.displayCurrency,
payment.isResolvingDisplayCurrency,
openCurrencyPicker,
@@ -146,15 +210,35 @@ export default function ContactPaymentOverlay({
});
return;
}
handleBackPressFunction();
}, [handleBackPressFunction, navigate, payment, paymentType]);
// Replace the entry screen with an in-place confirmation instead of closing
// straight to the homepage, so the user gets clear feedback the request
// actually went through.
setSuccessData({
amountValue: payment.amountValue,
denomination: payment.primaryDisplay.denomination,
forceCurrency: payment.primaryDisplay.forceCurrency,
forceFiatStats: payment.primaryDisplay.forceFiatStats,
contactName: selectedContact?.name || selectedContact?.uniqueName || '',
});
}, [
handleBackPressFunction,
navigate,
payment,
paymentType,
selectedContact,
]);
if (!visible) return null;
return (
<Animated.View style={[styles.container, overlayStyle]}>
<Animated.View
style={[styles.stepContainer, { paddingBottom: bottomPadding }]}
style={[
styles.stepContainer,
entryStyle,
{ paddingBottom: bottomPadding },
]}
pointerEvents={successData !== null ? 'none' : 'auto'}
>
<ContactAmountEntry
selectedContact={selectedContact}
@@ -181,6 +265,39 @@ export default function ContactPaymentOverlay({
isResolvingDisplayCurrency={payment.isResolvingDisplayCurrency}
/>
</Animated.View>
<Animated.View
style={[
styles.stepContainer,
styles.successContainer,
successStyle,
{ paddingBottom: bottomPadding },
]}
pointerEvents={successData !== null ? 'auto' : 'none'}
>
<View style={styles.successContent}>
{successData !== null && (
<LottieView
source={confirmAnimation}
loop={false}
autoPlay
style={styles.lottie}
/>
)}
<ThemeText
CustomNumberOfLines={2}
styles={styles.successSubtitle}
content={t('wallet.halfModal.requestSentSubtitle', {
name: successData?.contactName,
})}
/>
</View>
<CustomButton
buttonStyles={styles.doneButton}
actionFunction={handleBackPressFunction}
textContent={t('constants.done')}
/>
</Animated.View>
</Animated.View>
);
}
@@ -201,4 +318,30 @@ const styles = StyleSheet.create({
right: 0,
bottom: 0,
},
successContainer: {
alignItems: 'center',
},
successContent: {
flex: 1,
width: INSET_WINDOW_WIDTH,
alignItems: 'center',
// justifyContent: 'center',
},
lottie: {
width: 200,
height: 200,
marginBottom: 8,
},
successSubtitle: {
fontSize: SIZES.medium,
// opacity: HIDDEN_OPACITY,
textAlign: 'center',
// marginTop: 20,
includeFontPadding: false,
},
doneButton: {
// width: INSET_WINDOW_WIDTH,
...CENTER,
},
});
+2
View File
@@ -1006,6 +1006,8 @@
"payMe": "Schnell bezahlen",
"createInvoice": "Rechnung erstellen",
"invoiceDescription": "Erstellen Sie eine Rechnung und erhalten Sie sofort eine Zahlung.",
"requestSentTitle": "Anfrage gesendet",
"requestSentSubtitle": "{{name}} wurde über Ihre Anfrage benachrichtigt",
"poolsDescription": "Sammeln Sie Beiträge von einer Gruppe für ein gemeinsames Ziel.",
"noContactHead": "{{username}} nicht gefunden",
"noContactDesc": "Fügen Sie {{username}} zu Ihren Kontakten hinzu, damit der Kontakt hier künftig angezeigt wird, oder fahren Sie fort, um jetzt eine Zahlung zu senden.",
+2
View File
@@ -1006,6 +1006,8 @@
"payMe": "Quick Pay",
"createInvoice": "Create Invoice",
"invoiceDescription": "Create an invoice and get paid instantly.",
"requestSentTitle": "Request Sent",
"requestSentSubtitle": "{{name}} has been notified of your request",
"poolsDescription": "Collect contributions from a group toward a shared goal.",
"noContactHead": "{{username}} not found",
"noContactDesc": "Add {{username}} to your contacts to find them here in the future, or continue to send a payment now.",
+2
View File
@@ -1006,6 +1006,8 @@
"payMe": "Pago rápido",
"createInvoice": "Crear factura",
"invoiceDescription": "Crea una factura y recibe el pago al instante.",
"requestSentTitle": "Solicitud enviada",
"requestSentSubtitle": "{{name}} ha sido notificado de tu solicitud",
"poolsDescription": "Reúne contribuciones de un grupo para un objetivo compartido.",
"noContactHead": "{{username}} no encontrado",
"noContactDesc": "Agrega a {{username}} a tus contactos para encontrarlo aquí en el futuro, o continúa para enviar un pago ahora.",
+2
View File
@@ -1006,6 +1006,8 @@
"payMe": "Paiement rapide",
"createInvoice": "Créer une facture",
"invoiceDescription": "Créez une facture et recevez un paiement instantanément.",
"requestSentTitle": "Demande envoyée",
"requestSentSubtitle": "{{name}} a été informé de votre demande",
"poolsDescription": "Collectez des contributions dun groupe pour un objectif commun.",
"noContactHead": "{{username}} introuvable",
"noContactDesc": "Ajoutez {{username}} à vos contacts pour le retrouver ici à lavenir, ou continuez pour envoyer un paiement maintenant.",
+2
View File
@@ -1006,6 +1006,8 @@
"payMe": "Pagamento rapido",
"createInvoice": "Crea fattura",
"invoiceDescription": "Crea una fattura e ricevi il pagamento immediatamente.",
"requestSentTitle": "Richiesta inviata",
"requestSentSubtitle": "{{name}} è stato informato della tua richiesta",
"poolsDescription": "Raccogli contributi da un gruppo per un obiettivo condiviso.",
"noContactHead": "{{username}} non trovato",
"noContactDesc": "Aggiungi {{username}} ai tuoi contatti per trovarlo qui in futuro, oppure continua per inviare un pagamento adesso.",
+2
View File
@@ -1006,6 +1006,8 @@
"payMe": "Endereço Blitz",
"createInvoice": "Criar Fatura",
"invoiceDescription": "Escolha a moeda e quantia desejada",
"requestSentTitle": "Solicitação enviada",
"requestSentSubtitle": "{{name}} foi notificado sobre sua solicitação",
"poolsDescription": "Colete pagamentos para um objetivo",
"noContactHead": "{{username}} não encontrado",
"noContactDesc": "Adicione {{username}} aos seus contatos para encontrá-lo aqui no futuro, ou continue para enviar um pagamento agora.",
+2
View File
@@ -1006,6 +1006,8 @@
"payMe": "Быстрая оплата",
"createInvoice": "Создать счёт",
"invoiceDescription": "Создайте счёт и получите оплату мгновенно.",
"requestSentTitle": "Запрос отправлен",
"requestSentSubtitle": "{{name}} был уведомлён о вашем запросе",
"poolsDescription": "Собирайте взносы от группы для общей цели.",
"noContactHead": "{{username}} не найден",
"noContactDesc": "Добавьте {{username}} в свои контакты, чтобы находить здесь в будущем, или продолжите, чтобы отправить платёж сейчас.",
+2
View File
@@ -1006,6 +1006,8 @@
"payMe": "Snabb betalning",
"createInvoice": "Skapa faktura",
"invoiceDescription": "Skapa en faktura och få betalt direkt.",
"requestSentTitle": "Begäran skickad",
"requestSentSubtitle": "{{name}} har meddelats om din begäran",
"poolsDescription": "Samla in bidrag från en grupp till ett gemensamt mål.",
"noContactHead": "{{username}} hittades inte",
"noContactDesc": "Lägg till {{username}} i dina kontakter för att hitta dem här i framtiden, eller fortsätt för att skicka en betalning nu.",