/ src / components / TransactionConfirmModal.tsx
TransactionConfirmModal.tsx
  1  /**
  2   * Transaction Confirmation Modal
  3   * Shows transaction details and requires biometric/PIN confirmation
  4   */
  5  import React, { useState } from 'react';
  6  import {
  7    View,
  8    Text,
  9    StyleSheet,
 10    Modal,
 11    TouchableOpacity,
 12    ActivityIndicator,
 13    Platform,
 14  } from 'react-native';
 15  import * as LocalAuthentication from 'expo-local-authentication';
 16  import * as Haptics from 'expo-haptics';
 17  import { colors } from '../theme/colors';
 18  
 19  interface TransactionConfirmModalProps {
 20    visible: boolean;
 21    recipient: string;
 22    amount: string;
 23    fee: string;
 24    memo?: string;
 25    tokenSymbol: string;
 26    onConfirm: () => Promise<void>;
 27    onCancel: () => void;
 28  }
 29  
 30  export function TransactionConfirmModal({
 31    visible,
 32    recipient,
 33    amount,
 34    fee,
 35    memo,
 36    tokenSymbol,
 37    onConfirm,
 38    onCancel,
 39  }: TransactionConfirmModalProps) {
 40    const [isAuthenticating, setIsAuthenticating] = useState(false);
 41    const [isSubmitting, setIsSubmitting] = useState(false);
 42    const [error, setError] = useState<string | null>(null);
 43  
 44    const totalAmount = (parseFloat(amount) + parseFloat(fee)).toFixed(6);
 45  
 46    const handleConfirm = async () => {
 47      setError(null);
 48      setIsAuthenticating(true);
 49  
 50      try {
 51        // Check if biometric authentication is available
 52        const hasHardware = await LocalAuthentication.hasHardwareAsync();
 53        const isEnrolled = await LocalAuthentication.isEnrolledAsync();
 54  
 55        if (hasHardware && isEnrolled) {
 56          const result = await LocalAuthentication.authenticateAsync({
 57            promptMessage: 'Confirm Transaction',
 58            fallbackLabel: 'Use PIN',
 59            cancelLabel: 'Cancel',
 60            disableDeviceFallback: false,
 61          });
 62  
 63          if (!result.success) {
 64            if (result.error === 'user_cancel') {
 65              setIsAuthenticating(false);
 66              return;
 67            }
 68            setError('Authentication failed. Please try again.');
 69            setIsAuthenticating(false);
 70            return;
 71          }
 72        }
 73  
 74        // Authentication successful, submit transaction
 75        setIsAuthenticating(false);
 76        setIsSubmitting(true);
 77  
 78        await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
 79        await onConfirm();
 80  
 81        await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
 82      } catch (err) {
 83        setError(err instanceof Error ? err.message : 'Transaction failed');
 84        await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
 85      } finally {
 86        setIsAuthenticating(false);
 87        setIsSubmitting(false);
 88      }
 89    };
 90  
 91    const formatAddress = (address: string): string => {
 92      if (address.length <= 20) return address;
 93      return `${address.slice(0, 12)}...${address.slice(-8)}`;
 94    };
 95  
 96    return (
 97      <Modal
 98        visible={visible}
 99        animationType="slide"
100        transparent
101        onRequestClose={onCancel}
102      >
103        <View style={styles.overlay}>
104          <View style={styles.modal}>
105            {/* Header */}
106            <View style={styles.header}>
107              <Text style={styles.title}>Confirm Transaction</Text>
108              <TouchableOpacity
109                style={styles.closeButton}
110                onPress={onCancel}
111                disabled={isSubmitting}
112              >
113                <Text style={styles.closeButtonText}>X</Text>
114              </TouchableOpacity>
115            </View>
116  
117            {/* Transaction Details */}
118            <View style={styles.details}>
119              {/* Recipient */}
120              <View style={styles.row}>
121                <Text style={styles.label}>To</Text>
122                <Text style={styles.value} numberOfLines={1}>
123                  {formatAddress(recipient)}
124                </Text>
125              </View>
126  
127              {/* Amount */}
128              <View style={styles.row}>
129                <Text style={styles.label}>Amount</Text>
130                <Text style={styles.valueHighlight}>
131                  {amount} {tokenSymbol}
132                </Text>
133              </View>
134  
135              {/* Fee */}
136              <View style={styles.row}>
137                <Text style={styles.label}>Network Fee</Text>
138                <Text style={styles.value}>
139                  {fee} {tokenSymbol}
140                </Text>
141              </View>
142  
143              {/* Memo */}
144              {memo && (
145                <View style={styles.row}>
146                  <Text style={styles.label}>Memo</Text>
147                  <Text style={styles.value} numberOfLines={2}>
148                    {memo}
149                  </Text>
150                </View>
151              )}
152  
153              {/* Divider */}
154              <View style={styles.divider} />
155  
156              {/* Total */}
157              <View style={styles.row}>
158                <Text style={styles.totalLabel}>Total</Text>
159                <Text style={styles.totalValue}>
160                  {totalAmount} {tokenSymbol}
161                </Text>
162              </View>
163            </View>
164  
165            {/* Error Message */}
166            {error && (
167              <View style={styles.errorContainer}>
168                <Text style={styles.errorText}>{error}</Text>
169              </View>
170            )}
171  
172            {/* Actions */}
173            <View style={styles.actions}>
174              <TouchableOpacity
175                style={styles.cancelButton}
176                onPress={onCancel}
177                disabled={isSubmitting || isAuthenticating}
178              >
179                <Text style={styles.cancelButtonText}>Cancel</Text>
180              </TouchableOpacity>
181  
182              <TouchableOpacity
183                style={[
184                  styles.confirmButton,
185                  (isSubmitting || isAuthenticating) && styles.confirmButtonDisabled,
186                ]}
187                onPress={handleConfirm}
188                disabled={isSubmitting || isAuthenticating}
189              >
190                {isSubmitting || isAuthenticating ? (
191                  <ActivityIndicator color={colors.white} size="small" />
192                ) : (
193                  <Text style={styles.confirmButtonText}>
194                    {isAuthenticating ? 'Authenticating...' : 'Confirm & Send'}
195                  </Text>
196                )}
197              </TouchableOpacity>
198            </View>
199  
200            {/* Security Notice */}
201            <View style={styles.securityNotice}>
202              <Text style={styles.securityIcon}>
203                {Platform.OS === 'ios' ? 'Face ID / Touch ID' : 'Biometric'}
204              </Text>
205              <Text style={styles.securityText}>
206                Authentication required to confirm
207              </Text>
208            </View>
209          </View>
210        </View>
211      </Modal>
212    );
213  }
214  
215  const styles = StyleSheet.create({
216    overlay: {
217      flex: 1,
218      backgroundColor: 'rgba(0, 0, 0, 0.7)',
219      justifyContent: 'flex-end',
220    },
221    modal: {
222      backgroundColor: colors.background.secondary,
223      borderTopLeftRadius: 24,
224      borderTopRightRadius: 24,
225      paddingTop: 16,
226      paddingBottom: 34,
227      paddingHorizontal: 20,
228    },
229    header: {
230      flexDirection: 'row',
231      justifyContent: 'space-between',
232      alignItems: 'center',
233      marginBottom: 20,
234    },
235    title: {
236      color: colors.white,
237      fontSize: 20,
238      fontWeight: '700',
239    },
240    closeButton: {
241      width: 32,
242      height: 32,
243      borderRadius: 16,
244      backgroundColor: colors.background.tertiary,
245      alignItems: 'center',
246      justifyContent: 'center',
247    },
248    closeButtonText: {
249      color: colors.text.secondary,
250      fontSize: 14,
251      fontWeight: '600',
252    },
253    details: {
254      backgroundColor: colors.background.tertiary,
255      borderRadius: 16,
256      padding: 16,
257      marginBottom: 16,
258    },
259    row: {
260      flexDirection: 'row',
261      justifyContent: 'space-between',
262      alignItems: 'center',
263      paddingVertical: 12,
264    },
265    label: {
266      color: colors.text.secondary,
267      fontSize: 14,
268    },
269    value: {
270      color: colors.white,
271      fontSize: 14,
272      maxWidth: '60%',
273      textAlign: 'right',
274    },
275    valueHighlight: {
276      color: colors.white,
277      fontSize: 18,
278      fontWeight: '600',
279    },
280    divider: {
281      height: 1,
282      backgroundColor: colors.border.strong,
283      marginVertical: 8,
284    },
285    totalLabel: {
286      color: colors.white,
287      fontSize: 16,
288      fontWeight: '600',
289    },
290    totalValue: {
291      color: colors.accent.alpha,
292      fontSize: 18,
293      fontWeight: '700',
294    },
295    errorContainer: {
296      backgroundColor: 'rgba(239, 68, 68, 0.1)',
297      borderRadius: 12,
298      padding: 12,
299      marginBottom: 16,
300    },
301    errorText: {
302      color: colors.semantic.error,
303      fontSize: 14,
304      textAlign: 'center',
305    },
306    actions: {
307      flexDirection: 'row',
308      gap: 12,
309      marginBottom: 16,
310    },
311    cancelButton: {
312      flex: 1,
313      backgroundColor: colors.background.tertiary,
314      borderRadius: 12,
315      padding: 16,
316      alignItems: 'center',
317    },
318    cancelButtonText: {
319      color: colors.white,
320      fontSize: 16,
321      fontWeight: '600',
322    },
323    confirmButton: {
324      flex: 2,
325      backgroundColor: colors.accent.alpha,
326      borderRadius: 12,
327      padding: 16,
328      alignItems: 'center',
329      justifyContent: 'center',
330      minHeight: 52,
331    },
332    confirmButtonDisabled: {
333      backgroundColor: '#4B4B6D',
334    },
335    confirmButtonText: {
336      color: colors.white,
337      fontSize: 16,
338      fontWeight: '600',
339    },
340    securityNotice: {
341      flexDirection: 'row',
342      alignItems: 'center',
343      justifyContent: 'center',
344      gap: 8,
345    },
346    securityIcon: {
347      color: colors.text.secondary,
348      fontSize: 12,
349    },
350    securityText: {
351      color: colors.text.secondary,
352      fontSize: 12,
353    },
354  });