diff --git a/package-lock.json b/package-lock.json index 95c5fb0..aa1a182 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.0", "dependencies": { "@expo-google-fonts/poppins": "^0.2.3", - "@pharmatech/sdk": "^0.4.11", + "@pharmatech/sdk": "^0.4.21", "@ptomasroos/react-native-multi-slider": "^2.2.2", "@react-native-community/slider": "^4.5.6", "@reduxjs/toolkit": "^2.6.1", @@ -3592,9 +3592,9 @@ } }, "node_modules/@pharmatech/sdk": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.11.tgz", - "integrity": "sha512-iJChZa3uTsQrz6kLZJfnwrBTZOe2o6azPlwk/ByiMxA5V7UlPfVp8sDz4EIppYbBl2+uM4sLMGnQmiWatKEECA==", + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.21.tgz", + "integrity": "sha512-fxyXlgKN3qLxuGVg6bmRS5DhqfLOLsCYazffax5x0iYFFBIQHQBVRLltm5h84jvOEM0oA7ipD08VVXWMyBj/vQ==", "license": "MIT", "dependencies": { "axios": "^1.8.1" diff --git a/package.json b/package.json index 62c765a..1cd1ff6 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,14 @@ }, "dependencies": { "@expo-google-fonts/poppins": "^0.2.3", - "@pharmatech/sdk": "^0.4.11", + "@pharmatech/sdk": "^0.4.21", "@ptomasroos/react-native-multi-slider": "^2.2.2", "@react-native-community/slider": "^4.5.6", "@reduxjs/toolkit": "^2.6.1", "date-fns": "^4.1.0", "expo": "~52.0.37", "expo-constants": "~17.0.7", + "expo-dev-client": "~5.0.20", "expo-font": "~13.0.4", "expo-image-picker": "~16.0.6", "expo-linking": "~7.0.5", @@ -51,8 +52,7 @@ "react-native-svg": "^15.11.2", "react-redux": "^9.2.0", "socket.io-client": "^4.8.1", - "tailwindcss": "^3.4.17", - "expo-dev-client": "~5.0.20" + "tailwindcss": "^3.4.17" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 82ff0d8..7198700 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -172,6 +172,27 @@ export default function RootLayout() { /> ({ + headerTitle: '', + headerTransparent: true, + headerShown: true, + headerLeft: () => ( + navigation.goBack()} + style={{ + padding: 12, + marginLeft: -12, + flexDirection: 'row', + alignItems: 'center', + }} + > + + + ), + })} + /> + void; onBankChange: (value: string) => void; @@ -90,11 +85,14 @@ const PaymentInfoForm: React.FC = ({ useEffect(() => { const isValid = paymentMethod !== null && - (paymentMethod === 'pago_movil' || paymentMethod === 'transferencia') + (paymentMethod === 'MOBILE_PAYMENT' || paymentMethod === 'BANK_TRANSFER') ? bank.trim() !== '' && - /^\d+$/.test(reference) && + /^\d{4,}$/.test(reference) && reference.trim() !== '' && - /^\d{1,8}$/.test(documentNumber) && + !/^0+$/.test(reference) && + /^\d{7,8}$/.test(documentNumber) && + !/^0+$/.test(documentNumber) && + !/^0/.test(documentNumber) && /^\d{11}$/.test(phone) : true; @@ -129,8 +127,8 @@ const PaymentInfoForm: React.FC = ({ return ( - {(paymentMethod === 'pago_movil' || - paymentMethod === 'transferencia') && ( + {(paymentMethod === 'MOBILE_PAYMENT' || + paymentMethod === 'BANK_TRANSFER') && ( <> Realiza el pago en la siguiente cuenta de Pharmatech @@ -155,7 +153,7 @@ const PaymentInfoForm: React.FC = ({ - {paymentMethod === 'pago_movil' ? ( + {paymentMethod === 'MOBILE_PAYMENT' ? ( = ({ placeholder="Ingrese la referencia" getValue={handleReferenceChange} fieldType="number" - errorText="Debe ser un número válido" - validation={(val) => /^\d+$/.test(val) && val.trim() !== ''} - showIcon + errorText="Debe ser un número valido" + validation={(val) => + /^\d{4,}$/.test(val) && val.trim() !== '' && !/^0+$/.test(val) + } + showIcon={reference.length > 0} useDefaultValidation={false} {...editableInputProps} /> @@ -220,9 +220,14 @@ const PaymentInfoForm: React.FC = ({ value={documentNumber} getValue={handleDocumentNumberChange} fieldType="number" - errorText="El campo no debe estar vacío" - validation={(val) => /^\d+$/.test(val) && val.trim() !== ''} - showIcon + errorText="Debe ser un número de documento valido" + validation={(val) => + /^\d{7,8}$/.test(val) && + val.trim() !== '' && + !/^0+$/.test(val) && + !/^0/.test(val) + } + showIcon={documentNumber.length > 0} useDefaultValidation={false} {...editableInputProps} /> @@ -233,13 +238,14 @@ const PaymentInfoForm: React.FC = ({ getValue={handlePhoneChange} fieldType="number" errorText="Debe tener exactamente 11 dígitos" - validation={(val) => /^\d{11}$/.test(val)} - showIcon + validation={(val) => /^\d{11}$/.test(val) && !/^0+$/.test(val)} + showIcon={phone.length > 0} useDefaultValidation={false} {...editableInputProps} /> )} + {/* Si se requiere mostrar algo para CASH o CARD, agregar aquí */} ); }; diff --git a/src/components/PaymentStatusMessage.tsx b/src/components/PaymentStatusMessage.tsx index 0446869..51748ba 100644 --- a/src/components/PaymentStatusMessage.tsx +++ b/src/components/PaymentStatusMessage.tsx @@ -1,44 +1,98 @@ import React from 'react'; -import { CheckCircleIcon, XCircleIcon } from 'react-native-heroicons/outline'; +import { + CheckCircleIcon, + XCircleIcon, + ClockIcon, +} from 'react-native-heroicons/outline'; import theme from '../styles/theme'; import { Text, View, StyleSheet } from 'react-native'; +type OrderStatus = + | 'requested' + | 'approved' + | 'ready_for_pickup' + | 'in_progress' + | 'canceled' + | 'completed'; + interface PaymentStatusMessageProps { - status: 'approved' | 'rejected'; + orderStatus: OrderStatus; orderNumber: string; userName: string; } +const statusConfig: Record< + OrderStatus, + { + icon: React.ElementType; + color: string; + title: string; + message: (orderNumber: string, userName: string) => string; + } +> = { + requested: { + icon: ClockIcon, + color: theme.Colors.semanticWarning, + title: 'Orden en espera', + message: () => + `Estamos procesando tu orden. En un momento actualizaremos el estado de tu orden. Si tienes alguna duda, por favor contacta a nuestro equipo de soporte.`, + }, + approved: { + icon: CheckCircleIcon, + color: theme.Colors.semanticSuccess, + title: 'Orden aprobada', + message: (_orderNumber, userName) => + `¡Gracias por tu compra, ${userName}! Tu orden ha sido aprobada.`, + }, + ready_for_pickup: { + icon: CheckCircleIcon, + color: theme.Colors.semanticSuccess, + title: 'Lista para recoger', + message: () => + 'Tu pedido está listo para ser recogido en la sucursal seleccionada.', + }, + in_progress: { + icon: ClockIcon, + color: theme.Colors.semanticWarning, + title: 'Orden en progreso', + message: () => + 'Tu pedido está en progreso. Pronto comenzaremos a procesarlo.', + }, + canceled: { + icon: XCircleIcon, + color: theme.Colors.semanticDanger, + title: 'Orden rechazada', + message: () => + 'No pudimos procesar tu orden. Lamentamos informarte que hubo un problema al generar tu pedido.', + }, + completed: { + icon: CheckCircleIcon, + color: theme.Colors.semanticSuccess, + title: 'Orden completada', + message: (_orderNumber, userName) => + `¡Gracias por tu compra, ${userName}! Tu orden ha sido completada exitosamente.`, + }, +}; + const PaymentStatusMessage: React.FC = ({ - status, + orderStatus, orderNumber, userName, }) => { + const config = statusConfig[orderStatus] || statusConfig['requested']; + const Icon = config.icon; + return ( - {status === 'approved' ? ( - - ) : ( - - )} + - Orden #{orderNumber} + {config.title} #{orderNumber} - {status === 'approved' - ? `¡Gracias por tu compra, ${userName}!` - : 'No pudimos procesar tu orden. Lamentamos informarte que hubo un problema al generar tu pedido.'} + {config.message(orderNumber, userName)} diff --git a/src/lib/deliverySocket/deliverySocket.ts b/src/lib/deliverySocket/deliverySocket.ts new file mode 100644 index 0000000..5e5e3ec --- /dev/null +++ b/src/lib/deliverySocket/deliverySocket.ts @@ -0,0 +1,52 @@ +import io, { Socket } from 'socket.io-client'; +import { SOCKET_URL } from '../socketUrl'; +import * as SecureStore from 'expo-secure-store'; + +let socket: Socket | null = null; + +export const initializeSocket = async (): Promise => { + if (socket) return socket; + + const token = await SecureStore.getItemAsync('auth_token'); + if (!token) { + console.error('Token de autenticación no encontrado'); + throw new Error('Token de autenticación no encontrado'); + } + + console.log('Inicializando WebSocket con URL:', SOCKET_URL); + console.log('Enviando token JWT:', token); + + socket = io(SOCKET_URL, { + autoConnect: false, + transports: ['polling'], // Asegurarnos de que ambos transportes estén habilitados + transportOptions: { + polling: { + extraHeaders: { + Authorization: `Bearer ${token}`, + }, + }, + }, + }); + + socket.on('connect', () => { + console.log('WebSocket conectado exitosamente'); + }); + + socket.on('connect_error', (error) => { + console.error('Error de conexión al WebSocket:', error); + }); + + socket.on('disconnect', (reason) => { + console.warn('WebSocket desconectado. Razón:', reason); + }); + + return socket; +}; + +export const disconnectSocket = () => { + if (socket) { + console.log('Desconectando WebSocket'); + socket.disconnect(); + socket = null; + } +}; diff --git a/src/lib/socketUrl.ts b/src/lib/socketUrl.ts index 65016c6..77fccb4 100644 --- a/src/lib/socketUrl.ts +++ b/src/lib/socketUrl.ts @@ -1,5 +1,6 @@ const devModeFlag = process.env.PHARMATECH_DEV_MODE === 'true'; -const devUrl = 'ws://api-dev-8jfx.onrender.com'; -const prodUrl = 'ws://api-d8h5.onrender.com'; + +const devUrl = 'https://api-dev-8jfx.onrender.com'; +const prodUrl = 'https://api-d8h5.onrender.com'; export const SOCKET_URL = devModeFlag ? devUrl : prodUrl; diff --git a/src/screens/CheckoutScreen.tsx b/src/screens/CheckoutScreen.tsx index fc2f671..f14c8af 100644 --- a/src/screens/CheckoutScreen.tsx +++ b/src/screens/CheckoutScreen.tsx @@ -1,18 +1,13 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { View, StyleSheet, ScrollView, - TouchableOpacity, Animated, BackHandler, + RefreshControl, } from 'react-native'; -import { - ShoppingBagIcon, - TruckIcon, - MapPinIcon, - ChevronLeftIcon, -} from 'react-native-heroicons/outline'; +import { ShoppingBagIcon, TruckIcon } from 'react-native-heroicons/outline'; import { Colors, FontSizes } from '../styles/theme'; import RadioCard from '../components/RadioCard'; import OrderSummary from '../components/OrderSummary'; @@ -22,80 +17,54 @@ import Steps from '../components/Steps'; import PaymentMethods from '../components/PaymentMethods'; import PoppinsText from '../components/PoppinsText'; import LocationSelector from '../components/LocationSelector'; -import PaymentInfoForm from '../components/PaymentInfoForm'; import Coupon from '../components/Coupon'; -import PaymentStatusMessage from '../components/PaymentStatusMessage'; import { useRouter } from 'expo-router'; import { OrderService } from '../services/order'; -import { UserService } from '../services/user'; -import { OrderType, CreateOrder, CreateOrderDetail } from '../types/api.d'; import BranchMapModal from '../components/BranchMapModal'; import { useDispatch, useSelector } from 'react-redux'; import { clearCart } from '../redux/slices/cartSlice'; import { useFocusEffect } from '@react-navigation/native'; -import Popup from '../components/Popup'; // Import the Popup component +import Popup from '../components/Popup'; import EmailVerificationModal from './tab/EmailVerificationModal'; import { RootState, AppDispatch } from '../redux/store'; import { - setStep, setOption, setPayment, setLocationId, - setPaymentInfoValid, - resetCheckout, setCouponDiscount, setCouponApplied, } from '../redux/slices/checkoutSlice'; +import { + OrderType, + CreateOrder, + CreateOrderDetail, + PaymentMethod, +} from '@pharmatech/sdk'; + const CheckoutScreen = () => { const dispatch = useDispatch(); - const { - step, - option, - payment, - locationId, - paymentInfoValid, - couponDiscount, - couponApplied, - } = useSelector((state: RootState) => state.checkout); + const { step, option, payment, locationId, couponDiscount, couponApplied } = + useSelector((state: RootState) => state.checkout); const router = useRouter(); const { cartItems } = useCart(); - const [status, setStatus] = useState<'approved' | 'rejected'>('approved'); const [errorMessage, setErrorMessage] = useState(null); - const [userName, setUserName] = useState('Usuario'); + //const [userName, setUserName] = useState('Usuario'); const [modalVisible, setModalVisible] = useState(false); const [selectedBranch, setSelectedBranch] = useState<{ name: string; latitude: number; longitude: number; } | null>(null); - const [orderNumber, setOrderNumber] = useState(null); + // const [orderNumber, setOrderNumber] = useState(null); // commented: unused const [popupVisible, setPopupVisible] = useState(false); const [popupMessages, setPopupMessages] = useState([]); const [validationPopupVisible, setValidationPopupVisible] = useState(false); const [emailVerificationModalVisible, setEmailVerificationModalVisible] = useState(false); // Track modal visibility - - useEffect(() => { - const fetchUserName = async () => { - const response = await UserService.getProfile(); - if (response.success && response.data) { - const { firstName, isValidated } = response.data; - setUserName(firstName); - if (!isValidated) { - setValidationPopupVisible(true); - } - } else if (!response.success) { - console.error( - 'Error al obtener el nombre del usuario:', - response.error, - ); - } - }; - - fetchUserName(); - }, []); + // const [orderStatus, setOrderStatus] = useState(null); // commented: unused + // Suscribe al socket usando el hook; solo se conecta cuando orderNumber no es null useFocusEffect( React.useCallback(() => { @@ -114,13 +83,8 @@ const CheckoutScreen = () => { }, [step]), ); - const isSimplifiedSteps = - (option === 'pickup' && payment === 'punto_de_venta') || - (option === 'delivery' && payment === 'efectivo'); - - const stepsLabels = isSimplifiedSteps - ? ['Opciones de Compra', 'Confirmación de orden'] - : ['Opciones de Compra', 'Visualización de datos', 'Confirmación de orden']; + // Solo mostrar el paso 1 + const stepsLabels = ['Opciones de Compra']; const subtotal = cartItems.reduce( (sum, item) => sum + item.price * item.quantity, @@ -148,25 +112,21 @@ const CheckoutScreen = () => { return null; }; - const isValidUUID = (value: string | null): boolean => { - const uuidRegex = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - return value !== null && uuidRegex.test(value); - }; + // const isValidUUID = (value: string | null): boolean => { + // const uuidRegex = + // /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + // return value !== null && uuidRegex.test(value); + // }; const handleContinue = async () => { const missingFields: string[] = []; - if (step < stepsLabels.length - 1) { - // Validation for steps before the confirmation step - if (step === 1) { - if (!option) missingFields.push('Seleccionar una opción de compra.'); - if (!locationId) - missingFields.push('Seleccionar una opción de locación.'); - if (!payment) missingFields.push('Seleccionar un método de pago.'); - } else if (step === 2 && !isSimplifiedSteps && !paymentInfoValid) { - missingFields.push('Completar la información de pago.'); - } + // Validación solo para el paso 1 + if (step === 1) { + if (!option) missingFields.push('Seleccionar una opción de compra.'); + if (!locationId) + missingFields.push('Seleccionar una opción de locación.'); + if (!payment) missingFields.push('Seleccionar un método de pago.'); if (missingFields.length > 0) { setPopupMessages(missingFields); @@ -174,197 +134,109 @@ const CheckoutScreen = () => { return; } - dispatch(setStep(step + 1)); - } else if (step === stepsLabels.length - 1) { - // Ensure payment form is valid before creating the order - if (!isSimplifiedSteps && !paymentInfoValid) { - setPopupMessages(['Completar la información de pago correctamente.']); - setPopupVisible(true); - return; + // Validar los productos del carrito + const products: CreateOrderDetail[] = cartItems + .filter((item) => item.quantity > 0) + .map((item) => ({ + productPresentationId: item.id, + quantity: item.quantity, + })); + + setErrorMessage(null); + + let sdkPaymentMethod: PaymentMethod; + switch (payment) { + case 'efectivo': + sdkPaymentMethod = PaymentMethod.CASH; + break; + case 'punto_de_venta': + sdkPaymentMethod = PaymentMethod.CARD; + break; + case 'transferencia': + sdkPaymentMethod = PaymentMethod.BANK_TRANSFER; + break; + case 'pago_movil': + sdkPaymentMethod = PaymentMethod.MOBILE_PAYMENT; + break; + default: + sdkPaymentMethod = PaymentMethod.CASH; } - // Create the order on the penultimate step - try { - if (option === 'pickup' && !isValidUUID(locationId)) { - setErrorMessage('La sucursal seleccionada no es válida.'); - setStatus('rejected'); - return; - } - - if (option === 'delivery' && !isValidUUID(locationId)) { - setErrorMessage('La dirección seleccionada no es válida.'); - setStatus('rejected'); - return; - } - - if (!option) { - setErrorMessage('Debe seleccionar una opción de compra.'); - setStatus('rejected'); - return; - } - - setErrorMessage(null); - - // Validar los productos del carrito - const products: CreateOrderDetail[] = cartItems - .filter((item) => item.quantity > 0) - .map((item) => ({ - productPresentationId: item.id, - quantity: item.quantity, // Solo incluir los campos esperados - })); - - if (products.length === 0) { - setErrorMessage('No hay productos válidos en el carrito.'); - setStatus('rejected'); - return; - } - - // Log del carrito y los productos seleccionados - console.log('Productos en el carrito:', cartItems); - console.log('Productos seleccionados para la orden:', products); - - // Construir el payload de la orden - const orderPayload: CreateOrder = { - type: option === 'pickup' ? OrderType.PICKUP : OrderType.DELIVERY, - branchId: option === 'pickup' ? locationId || undefined : undefined, - userAddressId: - option === 'delivery' ? locationId || undefined : undefined, - products, - }; - - // Log del payload que se enviará al backend - console.log('Payload enviado al backend:', orderPayload); + // Construir el payload de la orden usando el método seleccionado + const orderPayload: CreateOrder = { + type: option === 'pickup' ? OrderType.PICKUP : OrderType.DELIVERY, + branchId: option === 'pickup' ? locationId || undefined : undefined, + userAddressId: + option === 'delivery' ? locationId || undefined : undefined, + products, + paymentMethod: sdkPaymentMethod, + ...(couponApplied && { + couponCode: + typeof couponDiscount === 'number' && couponDiscount > 0 + ? 'COUPON' + : undefined, + }), + }; + try { // Enviar la orden al backend const orderResponse = await OrderService.create(orderPayload); - // Log de la respuesta del backend - console.log('Respuesta del backend:', orderResponse); - - // Validar la respuesta if (!orderResponse?.id) { setErrorMessage( 'No pudimos procesar tu orden. Inténtalo nuevamente.', ); - setStatus('rejected'); - return; } - // Guardar el número de orden generado - setOrderNumber(orderResponse.id); - - console.log('Orden creada exitosamente:', orderResponse); - - setStatus('approved'); - dispatch(setStep(step + 1)); // Move to the confirmation step + // Redirigir a la pantalla de orden en progreso (step 2 y 3) + router.push({ + pathname: '/in-progress-order', + params: { + orderNumber: orderResponse.id, + }, + }); } catch (error) { console.error('Error al procesar la orden:', error); setErrorMessage('Ocurrió un error inesperado. Inténtalo nuevamente.'); - setStatus('rejected'); } - } else if (step === stepsLabels.length) { - // Limpiar el carrito al salir del flujo de checkout - dispatch(clearCart()); - dispatch(resetCheckout()); - router.dismissAll(); - router.replace({ - pathname: '/(tabs)', - }); - } - }; - - const handleGoBack = () => { - if (step === stepsLabels.length) { - // Si estamos en el último paso, no permitir retroceder return; } - - if (step === 1) { - router.back(); - } else if (step > 1) { - dispatch(setStep(step - 1)); - } }; - const handleOpenMapModal = () => { - if (selectedBranch) { - setModalVisible(true); // Open the modal only if a branch is selected - } else { - console.error('No branch selected to display on the map.'); - } - }; - - const renderConfirmationContent = (status: 'approved' | 'rejected') => { - if (option === 'pickup' && status === 'approved') { - return ( - <> - - Tu pedido ya está listo para que pases por él en la sucursal - indicada. En el mapa adjunto podrás ver la ubicación exacta para que - llegues sin problemas. - - - Sucursal de retiro:{' '} - {selectedBranch?.name || '[Nombre de la sucursal]'} - -