BUS-Ticket-client/app/booking/[tripId].tsx
user 87d9bda46a BUS-Tickets Mobile v1.1.0 - Standalone Build
Features:
- OAuth authentication (Google, Facebook, Apple)
- Two-factor authentication (TOTP, SMS, Email)
- Multilingual support (cs, en, uk)
- Multi-provider bus operators
- Complete booking flow
- Push notifications
- Offline mode
- Dark/light theme

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 08:54:11 +00:00

1036 lines
31 KiB
TypeScript

/**
* BUS-Tickets - Booking Screen
* Copyright (c) 2024-2026 IT Enterprise
*/
import { useState, useEffect } from 'react';
import {
View,
Text,
ScrollView,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
} from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useTheme } from '@/contexts/ThemeContext';
import { useAuth } from '@/contexts/AuthContext';
import { useConfig } from '@/contexts/ConfigContext';
import { useLocale } from '@/contexts/LocaleContext';
import { useProviders } from '@/contexts/ProvidersContext';
import { usePayment, PaymentStatus } from '@/hooks/usePayment';
import type { Trip, Passenger, PaymentConfig } from '@/types';
// Helper functions (avoid import issues)
const formatPrice = (price: { amount: number; currency: string }) => {
return `${price.amount} ${price.currency}`;
};
const formatTime = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' });
};
const formatShortDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString('cs-CZ', { day: 'numeric', month: 'short' });
};
const formatDuration = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = Math.round(minutes % 60);
return `${hours}h ${mins}m`;
};
type BookingStep = 'seats' | 'passenger' | 'payment';
// Helper functions for payment providers
const getProviderIcon = (provider: string): string => {
const icons: Record<string, string> = {
monobank: 'card',
liqpay: 'card-outline',
stripe: 'card',
paypal: 'logo-paypal',
gopay: 'wallet',
fondy: 'card',
cash: 'cash-outline',
bank_transfer: 'business-outline',
};
return icons[provider] || 'card';
};
const getProviderColor = (provider: string): string => {
const colors: Record<string, string> = {
monobank: '#000000',
liqpay: '#7AB72B',
stripe: '#635BFF',
paypal: '#003087',
gopay: '#2E7D32',
fondy: '#FF6B00',
cash: '#28a745',
bank_transfer: '#0D47A1',
};
return colors[provider] || '#007AFF';
};
export default function BookingScreen() {
const router = useRouter();
const params = useLocalSearchParams<{ tripId: string; passengers: string; providerId?: string }>();
const { colors } = useTheme();
const { isAuthenticated, user } = useAuth();
const { config } = useConfig();
const { t, formatCurrency, formatDate, locale } = useLocale();
const { getProvider, activeProviders } = useProviders();
const [trip, setTrip] = useState<Trip | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [step, setStep] = useState<BookingStep>('seats');
const [selectedSeats, setSelectedSeats] = useState<number[]>([]);
const [passenger, setPassenger] = useState<Passenger>({
name: user?.name || '',
email: user?.email || '',
phone: user?.phone || '',
});
const [selectedProvider, setSelectedProvider] = useState<PaymentConfig | null>(null);
const [reservationIds, setReservationIds] = useState<number[]>([]);
// Payment hook with callbacks
const {
status: paymentStatus,
error: paymentError,
transaction,
availableProviders,
isPolling,
initiatePayment,
openPaymentPage,
startPolling,
reset: resetPayment,
} = usePayment({
onSuccess: (result) => {
Alert.alert(t.payment.success, t.booking.successMessage, [
{
text: t.tickets.title,
onPress: () => router.replace('/tickets'),
},
]);
},
onError: (error) => {
Alert.alert(t.payment.failed, error || t.errors.paymentFailed);
},
onCancelled: () => {
Alert.alert(t.payment.cancelled, t.payment.cancelled);
},
});
const isProcessing = paymentStatus === 'initiating' || paymentStatus === 'processing';
const passengerCount = parseInt(params.passengers || '1', 10);
useEffect(() => {
loadTrip();
}, [params.tripId]);
const loadTrip = async () => {
setIsLoading(true);
try {
const apiUrl = config.backend.url;
const response = await fetch(`${apiUrl}/api/v1/trip/${params.tripId}`);
const data = await response.json();
if (data.success && data.data) {
const t = data.data;
const loadedTrip: Trip = {
id: t.id,
route: {
id: t.route?.id || 0,
name: t.route?.name || '',
origin: {
id: t.route?.origin?.id || 0,
name: t.route?.origin?.name || '',
city: t.route?.origin?.city || t.route?.origin?.name || '',
country: 'CZ',
},
destination: {
id: t.route?.destination?.id || 0,
name: t.route?.destination?.name || '',
city: t.route?.destination?.city || t.route?.destination?.name || '',
country: 'UA',
},
},
departureTime: t.departureTime || t.tripDate,
arrivalTime: t.arrivalTime || t.tripDate,
bus: {
id: t.bus?.id || 0,
name: t.bus?.name || 'Bus',
plateNumber: t.bus?.plateNumber || '',
capacity: t.bus?.capacity || t.totalSeats || 50,
amenities: ['wifi', 'ac'],
},
availableSeats: t.availableSeats || 0,
totalSeats: t.totalSeats || t.bus?.capacity || 50,
price: {
amount: t.price?.amount || 0,
currency: t.price?.currency || 'CZK',
},
status: t.status || 'scheduled',
};
setTrip(loadedTrip);
} else {
throw new Error('Trip not found');
}
} catch (error) {
console.error('Error loading trip:', error);
Alert.alert('Error', 'Could not load trip details');
router.back();
} finally {
setIsLoading(false);
}
};
const handleSeatSelect = (seatNumber: number) => {
if (selectedSeats.includes(seatNumber)) {
setSelectedSeats(selectedSeats.filter((s) => s !== seatNumber));
} else if (selectedSeats.length < passengerCount) {
setSelectedSeats([...selectedSeats, seatNumber]);
}
};
const handleContinue = () => {
if (step === 'seats') {
if (selectedSeats.length !== passengerCount) {
Alert.alert(t.booking.selectSeat, `${t.booking.selectSeat}: ${passengerCount}`);
return;
}
setStep('passenger');
} else if (step === 'passenger') {
if (!passenger.name || !passenger.email || !passenger.phone) {
Alert.alert(t.booking.passengerDetails, t.errors.invalidInput);
return;
}
setStep('payment');
}
};
const handlePayment = async () => {
if (!selectedProvider) {
Alert.alert(t.payment.selectMethod, t.payment.selectMethod);
return;
}
try {
// First create reservation via API
const reservations = await createReservations();
if (!reservations || reservations.length === 0) {
throw new Error(t.errors.bookingFailed);
}
setReservationIds(reservations);
// Initiate payment with selected provider
const result = await initiatePayment(
reservations,
selectedProvider.id as number,
);
// Handle different payment flows based on provider
if (result.paymentUrl) {
// For PayPal, Monobank, Stripe - open payment page
await openPaymentPage(result.paymentUrl);
// Start polling for status updates
startPolling(result.transactionId);
} else if (result.bankDetails) {
// Bank transfer - show bank details
Alert.alert(
t.payment.card,
`IBAN: ${result.bankDetails.iban}\n` +
`SWIFT: ${result.bankDetails.swift}\n` +
`VS: ${result.bankDetails.variableSymbol}\n` +
`${t.common.total}: ${result.bankDetails.amount} ${result.bankDetails.currency}`,
[{ text: 'OK' }]
);
} else if (selectedProvider.provider === 'cash') {
// Cash payment - just confirm booking
Alert.alert(t.payment.confirmCash, t.payment.cashNote, [
{
text: t.tickets.title,
onPress: () => router.replace('/tickets'),
},
]);
}
} catch (error) {
const message = error instanceof Error ? error.message : t.errors.paymentFailed;
Alert.alert(t.common.error, message);
}
};
const createReservations = async (): Promise<number[]> => {
try {
const response = await fetch(`${config.backend.url}/api/v1/reservations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tripId: parseInt(params.tripId!, 10),
passengers: passengerCount,
seats: selectedSeats,
passenger: {
name: passenger.name,
email: passenger.email,
phone: passenger.phone,
},
paymentMethod: selectedProvider?.provider || 'cash',
}),
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error?.message || 'Rezervace selhala');
}
// Return array of reservation IDs
return data.data?.reservationId ? [data.data.reservationId] : [];
} catch (error) {
console.error('Create reservations error:', error);
throw error;
}
};
const styles = createStyles(colors);
if (isLoading || !trip) {
return (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Loading trip...</Text>
</View>
);
}
const duration =
(new Date(trip.arrivalTime).getTime() - new Date(trip.departureTime).getTime()) /
1000 /
60;
const totalPrice = trip.price.amount * passengerCount;
// Generate seat grid
const seatRows = Math.ceil(trip.totalSeats / 4);
const occupiedSeats = [3, 7, 12, 15, 22, 28, 33, 41]; // Mock occupied seats
return (
<View style={styles.container}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.content}>
{/* Trip Summary */}
<View style={styles.tripSummary}>
<View style={styles.routeRow}>
<Text style={styles.cityText}>{trip.route.origin.city}</Text>
<Ionicons name="arrow-forward" size={20} color={colors.primary} />
<Text style={styles.cityText}>{trip.route.destination.city}</Text>
</View>
<View style={styles.detailsRow}>
<Text style={styles.detailText}>
{formatShortDate(trip.departureTime)} {formatTime(trip.departureTime)} {' '}
{formatDuration(duration)}
</Text>
</View>
</View>
{/* Step indicator */}
<View style={styles.stepIndicator}>
{(['seats', 'passenger', 'payment'] as BookingStep[]).map((s, index) => (
<View key={s} style={styles.stepItem}>
<View
style={[
styles.stepCircle,
(step === s || index < ['seats', 'passenger', 'payment'].indexOf(step)) &&
styles.stepCircleActive,
]}
>
<Text
style={[
styles.stepNumber,
(step === s || index < ['seats', 'passenger', 'payment'].indexOf(step)) &&
styles.stepNumberActive,
]}
>
{index + 1}
</Text>
</View>
<Text style={styles.stepLabel}>
{s === 'seats' ? 'Seats' : s === 'passenger' ? 'Details' : 'Payment'}
</Text>
</View>
))}
</View>
{/* Step Content */}
{step === 'seats' && (
<View style={styles.stepContent}>
<Text style={styles.sectionTitle}>Select Your Seats</Text>
<Text style={styles.sectionSubtitle}>
Select {passengerCount} seat(s) {selectedSeats.length} selected
</Text>
{/* Seat legend */}
<View style={styles.seatLegend}>
<View style={styles.legendItem}>
<View style={[styles.seatSmall, styles.seatAvailable]} />
<Text style={styles.legendText}>Available</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.seatSmall, styles.seatSelected]} />
<Text style={styles.legendText}>Selected</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.seatSmall, styles.seatOccupied]} />
<Text style={styles.legendText}>Occupied</Text>
</View>
</View>
{/* Seat grid */}
<View style={styles.busLayout}>
<View style={styles.driverArea}>
<Ionicons name="car" size={24} color={colors.textSecondary} />
</View>
<View style={styles.seatsContainer}>
{Array.from({ length: seatRows }, (_, rowIndex) => (
<View key={rowIndex} style={styles.seatRow}>
{Array.from({ length: 4 }, (_, colIndex) => {
const seatNumber = rowIndex * 4 + colIndex + 1;
if (seatNumber > trip.totalSeats) return null;
const isOccupied = occupiedSeats.includes(seatNumber);
const isSelected = selectedSeats.includes(seatNumber);
return (
<TouchableOpacity
key={seatNumber}
style={[
styles.seat,
isOccupied && styles.seatOccupied,
isSelected && styles.seatSelected,
colIndex === 1 && styles.seatAisle,
]}
onPress={() => !isOccupied && handleSeatSelect(seatNumber)}
disabled={isOccupied}
>
<Text
style={[
styles.seatText,
isOccupied && styles.seatTextOccupied,
isSelected && styles.seatTextSelected,
]}
>
{seatNumber}
</Text>
</TouchableOpacity>
);
})}
</View>
))}
</View>
</View>
</View>
)}
{step === 'passenger' && (
<View style={styles.stepContent}>
<Text style={styles.sectionTitle}>Passenger Details</Text>
<View style={styles.formGroup}>
<Text style={styles.inputLabel}>Full Name</Text>
<TextInput
style={styles.input}
placeholder="Enter full name"
placeholderTextColor={colors.textSecondary}
value={passenger.name}
onChangeText={(text) => setPassenger({ ...passenger, name: text })}
/>
</View>
<View style={styles.formGroup}>
<Text style={styles.inputLabel}>Email</Text>
<TextInput
style={styles.input}
placeholder="Enter email"
placeholderTextColor={colors.textSecondary}
value={passenger.email}
onChangeText={(text) => setPassenger({ ...passenger, email: text })}
keyboardType="email-address"
autoCapitalize="none"
/>
</View>
<View style={styles.formGroup}>
<Text style={styles.inputLabel}>Phone</Text>
<TextInput
style={styles.input}
placeholder="Enter phone number"
placeholderTextColor={colors.textSecondary}
value={passenger.phone}
onChangeText={(text) => setPassenger({ ...passenger, phone: text })}
keyboardType="phone-pad"
/>
</View>
<View style={styles.selectedSeatsInfo}>
<Text style={styles.selectedSeatsLabel}>Selected seats:</Text>
<Text style={styles.selectedSeatsValue}>
{selectedSeats.sort((a, b) => a - b).join(', ')}
</Text>
</View>
</View>
)}
{step === 'payment' && (
<View style={styles.stepContent}>
<Text style={styles.sectionTitle}>Vyberte platební metodu</Text>
{/* Payment status indicator */}
{isProcessing && (
<View style={styles.paymentStatusBar}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={styles.paymentStatusText}>
{paymentStatus === 'initiating'
? 'Zahajuji platbu...'
: isPolling
? 'Čekám na potvrzení platby...'
: 'Zpracovávám...'}
</Text>
</View>
)}
{paymentError && (
<View style={styles.errorBanner}>
<Ionicons name="warning" size={20} color="#dc3545" />
<Text style={styles.errorText}>{paymentError}</Text>
</View>
)}
{availableProviders.map((provider) => {
const isSelected = selectedProvider?.id === provider.id;
const providerIcon = getProviderIcon(provider.provider);
return (
<TouchableOpacity
key={provider.id}
style={[
styles.paymentOption,
isSelected && styles.paymentOptionSelected,
isProcessing && styles.paymentOptionDisabled,
]}
onPress={() => setSelectedProvider(provider)}
disabled={isProcessing}
>
<View style={styles.paymentInfo}>
<View style={[styles.providerIconBg, { backgroundColor: getProviderColor(provider.provider) + '20' }]}>
<Ionicons
name={providerIcon as any}
size={24}
color={getProviderColor(provider.provider)}
/>
</View>
<View style={styles.paymentDetails}>
<Text
style={[
styles.paymentName,
isSelected && styles.paymentNameSelected,
]}
>
{provider.name}
</Text>
{provider.testMode && (
<View style={styles.testModeBadge}>
<Text style={styles.testModeText}>TEST</Text>
</View>
)}
{(provider.supportsApplePay || provider.supportsGooglePay) && (
<View style={styles.walletBadges}>
{provider.supportsApplePay && (
<Text style={styles.walletBadge}>Apple Pay</Text>
)}
{provider.supportsGooglePay && (
<Text style={styles.walletBadge}>Google Pay</Text>
)}
</View>
)}
</View>
</View>
{isSelected && (
<Ionicons name="checkmark-circle" size={24} color={colors.primary} />
)}
</TouchableOpacity>
);
})}
{/* Order summary */}
<View style={styles.orderSummary}>
<Text style={styles.summaryTitle}>Order Summary</Text>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Route</Text>
<Text style={styles.summaryValue}>
{trip.route.origin.city} {trip.route.destination.city}
</Text>
</View>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Date</Text>
<Text style={styles.summaryValue}>
{formatShortDate(trip.departureTime)}
</Text>
</View>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Seats</Text>
<Text style={styles.summaryValue}>
{selectedSeats.sort((a, b) => a - b).join(', ')}
</Text>
</View>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Passenger</Text>
<Text style={styles.summaryValue}>{passenger.name}</Text>
</View>
<View style={[styles.summaryRow, styles.summaryTotal]}>
<Text style={styles.totalLabel}>Total</Text>
<Text style={styles.totalValue}>
{formatPrice({ amount: totalPrice, currency: trip.price.currency })}
</Text>
</View>
</View>
</View>
)}
</ScrollView>
{/* Bottom bar */}
<View style={styles.bottomBar}>
<View style={styles.priceContainer}>
<Text style={styles.priceLabel}>Total</Text>
<Text style={styles.priceValue}>
{formatPrice({ amount: totalPrice, currency: trip.price.currency })}
</Text>
</View>
{step === 'payment' ? (
<TouchableOpacity
style={[
styles.continueButton,
(isProcessing || !selectedProvider) && styles.buttonDisabled,
]}
onPress={handlePayment}
disabled={isProcessing || !selectedProvider}
>
{isProcessing ? (
<View style={styles.buttonContent}>
<ActivityIndicator size="small" color="#fff" />
<Text style={styles.continueButtonText}>
{paymentStatus === 'initiating' ? 'Zahajuji...' : 'Zpracovávám...'}
</Text>
</View>
) : (
<View style={styles.buttonContent}>
<Ionicons name="lock-closed" size={18} color="#fff" />
<Text style={styles.continueButtonText}>Zaplatit</Text>
</View>
)}
</TouchableOpacity>
) : (
<TouchableOpacity style={styles.continueButton} onPress={handleContinue}>
<Text style={styles.continueButtonText}>Continue</Text>
</TouchableOpacity>
)}
</View>
</View>
);
}
const createStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
fontSize: 16,
color: colors.textSecondary,
},
scrollView: {
flex: 1,
},
content: {
padding: 16,
paddingBottom: 100,
},
tripSummary: {
backgroundColor: colors.card,
borderRadius: 12,
padding: 16,
marginBottom: 16,
},
routeRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
cityText: {
fontSize: 18,
fontWeight: '600',
color: colors.text,
},
detailsRow: {
marginTop: 8,
},
detailText: {
fontSize: 14,
color: colors.textSecondary,
},
stepIndicator: {
flexDirection: 'row',
justifyContent: 'center',
gap: 32,
marginBottom: 24,
},
stepItem: {
alignItems: 'center',
},
stepCircle: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.card,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.border,
},
stepCircleActive: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
stepNumber: {
fontSize: 14,
fontWeight: '600',
color: colors.textSecondary,
},
stepNumberActive: {
color: '#fff',
},
stepLabel: {
fontSize: 12,
color: colors.textSecondary,
marginTop: 4,
},
stepContent: {
backgroundColor: colors.card,
borderRadius: 12,
padding: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text,
marginBottom: 8,
},
sectionSubtitle: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 16,
},
seatLegend: {
flexDirection: 'row',
justifyContent: 'center',
gap: 24,
marginBottom: 16,
},
legendItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
seatSmall: {
width: 16,
height: 16,
borderRadius: 4,
},
seatAvailable: {
backgroundColor: colors.background,
borderWidth: 1,
borderColor: colors.border,
},
seatSelected: {
backgroundColor: colors.primary,
},
seatOccupied: {
backgroundColor: colors.textSecondary,
},
legendText: {
fontSize: 12,
color: colors.textSecondary,
},
busLayout: {
alignItems: 'center',
},
driverArea: {
width: 40,
height: 40,
borderRadius: 8,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 16,
},
seatsContainer: {
gap: 8,
},
seatRow: {
flexDirection: 'row',
gap: 8,
},
seat: {
width: 40,
height: 40,
borderRadius: 8,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
seatAisle: {
marginRight: 16,
},
seatText: {
fontSize: 12,
fontWeight: '500',
color: colors.text,
},
seatTextOccupied: {
color: '#fff',
},
seatTextSelected: {
color: '#fff',
},
formGroup: {
marginBottom: 16,
},
inputLabel: {
fontSize: 14,
fontWeight: '500',
color: colors.text,
marginBottom: 8,
},
input: {
backgroundColor: colors.background,
borderRadius: 8,
padding: 12,
fontSize: 16,
color: colors.text,
borderWidth: 1,
borderColor: colors.border,
},
selectedSeatsInfo: {
flexDirection: 'row',
marginTop: 16,
padding: 12,
backgroundColor: colors.background,
borderRadius: 8,
},
selectedSeatsLabel: {
fontSize: 14,
color: colors.textSecondary,
},
selectedSeatsValue: {
fontSize: 14,
fontWeight: '600',
color: colors.text,
marginLeft: 8,
},
paymentStatusBar: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
padding: 12,
backgroundColor: colors.primary + '15',
borderRadius: 8,
marginBottom: 16,
},
paymentStatusText: {
fontSize: 14,
color: colors.primary,
fontWeight: '500',
},
errorBanner: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
padding: 12,
backgroundColor: '#dc354520',
borderRadius: 8,
marginBottom: 16,
},
errorText: {
fontSize: 14,
color: '#dc3545',
flex: 1,
},
paymentOption: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
backgroundColor: colors.background,
borderRadius: 12,
marginBottom: 8,
borderWidth: 2,
borderColor: 'transparent',
},
paymentOptionSelected: {
borderColor: colors.primary,
backgroundColor: colors.primary + '08',
},
paymentOptionDisabled: {
opacity: 0.5,
},
paymentInfo: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
flex: 1,
},
providerIconBg: {
width: 48,
height: 48,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
paymentDetails: {
flex: 1,
},
paymentName: {
fontSize: 16,
color: colors.text,
},
paymentNameSelected: {
fontWeight: '600',
},
testModeBadge: {
backgroundColor: '#ff9800',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
alignSelf: 'flex-start',
marginTop: 4,
},
testModeText: {
color: '#fff',
fontSize: 10,
fontWeight: 'bold',
},
walletBadges: {
flexDirection: 'row',
gap: 8,
marginTop: 4,
},
walletBadge: {
fontSize: 11,
color: colors.textSecondary,
backgroundColor: colors.border,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
orderSummary: {
marginTop: 16,
padding: 16,
backgroundColor: colors.background,
borderRadius: 8,
},
summaryTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
marginBottom: 12,
},
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
summaryLabel: {
fontSize: 14,
color: colors.textSecondary,
},
summaryValue: {
fontSize: 14,
color: colors.text,
},
summaryTotal: {
marginTop: 8,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: colors.border,
},
totalLabel: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
},
totalValue: {
fontSize: 18,
fontWeight: '700',
color: colors.primary,
},
bottomBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
backgroundColor: colors.card,
borderTopWidth: 1,
borderTopColor: colors.border,
},
priceContainer: {},
priceLabel: {
fontSize: 12,
color: colors.textSecondary,
},
priceValue: {
fontSize: 20,
fontWeight: '700',
color: colors.primary,
},
continueButton: {
backgroundColor: colors.primary,
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 8,
minWidth: 140,
},
buttonDisabled: {
opacity: 0.6,
},
buttonContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
continueButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
});