Construyendo flujos de checkout que convierten: lecciones del e-commerce de salud
- Validar recetas médicas antes de permitir la compra
- Verificar la cobertura del seguro en tiempo real
- Manejar múltiples métodos de pago, incluyendo efectivo
- Cumplir con regulaciones del sector salud
La complejidad del checkout en salud
- Productos OTC (vitaminas, vendas) - Sin requerimientos especiales
- Medicamentos con receta - Requieren subir una receta válida + revisión de un farmacéutico
- Sustancias controladas - Verificación adicional + límites de cantidad
- Productos cubiertos por seguro - Autorización en tiempo real con las aseguradoras
Principio #1: Revelación progresiva
Mal: Todo por adelantado
Step 1: Upload prescriptions for ALL items
Step 2: Enter insurance info
Step 3: Verify identity
Step 4: Enter address
Step 5: PayBien: Mostrar según se necesite
Step 1: Enter address (everyone does this)
Step 2: [IF prescription items] Upload Rx
Step 3: [IF insured] Enter insurance
Step 4: PayImplementación
function CheckoutFlow({ cart }) {
const steps = useMemo(() => {
const baseSteps = ['address', 'payment']
if (cart.hasPrescriptionItems) {
baseSteps.splice(1, 0, 'prescription')
}
if (cart.hasInsurableItems && user.hasInsurance) {
baseSteps.splice(-1, 0, 'insurance')
}
return baseSteps
}, [cart, user])
return <StepWizard steps={steps} />
}Principio #2: Validación asíncrona, UI optimista
Mal: Flujo bloqueante
User uploads Rx → Spinner for 2 minutes → ContinueBien: Progreso no bloqueante
User uploads Rx → "We're reviewing this" → Continue to address
→ Background: Validation happens
→ At payment: Show resultImplementación
function PrescriptionStep({ onNext }) {
const [uploadState, setUploadState] = useState('idle')
const handleUpload = async (file) => {
setUploadState('uploading')
// Upload and start background validation
const { validationId } = await uploadPrescription(file)
// Store validation ID, don't wait for result
setValidationPending(validationId)
setUploadState('pending-review')
// Let user continue immediately
onNext()
}
return (
<div>
<FileUpload onUpload={handleUpload} />
{uploadState === 'pending-review' && (
<Banner type="info">
✓ Uploaded! Our pharmacist is reviewing.
You can continue while we verify.
</Banner>
)}
</div>
)
}function PaymentStep() {
const validationResult = useValidationStatus(validationPending)
if (validationResult === 'rejected') {
return <PrescriptionIssue reason={validationResult.reason} />
}
if (validationResult === 'pending') {
return (
<div>
<PaymentForm disabled />
<Banner>
Almost ready! Prescription review in progress...
</Banner>
</div>
)
}
return <PaymentForm />
}Principio #3: Defaults inteligentes
Defaults de dirección
function AddressStep({ user }) {
// Pre-fill with last used address
const defaultAddress = user.addresses.find(a => a.isDefault)
|| user.addresses[0]
const [address, setAddress] = useState(defaultAddress)
return (
<div>
{user.addresses.length > 0 ? (
// Show saved addresses as selectable cards
<AddressSelector
addresses={user.addresses}
selected={address}
onSelect={setAddress}
/>
) : (
// Only show form if no saved addresses
<AddressForm onChange={setAddress} />
)}
</div>
)
}Defaults de pago
function PaymentStep({ user, cart }) {
// Default to last used payment method
const defaultMethod = user.paymentMethods[0]
// But suggest installments for large orders
const suggestInstallments = cart.total > 2000
return (
<div>
<PaymentMethodSelector
methods={user.paymentMethods}
default={defaultMethod}
/>
{suggestInstallments && (
<InstallmentsBanner>
💡 Pay in 3 months interest-free with select cards
</InstallmentsBanner>
)}
</div>
)
}Principio #4: Recuperación de errores, no bloqueo por errores
Mal: Bloqueo total
❌ Insurance authorization failed.
Please contact support.
[Dead end]Bien: Ofrecer alternativas
⚠️ Insurance authorization failed for this item.
You can:
○ Pay full price ($45.00) and submit for reimbursement
○ Try a different insurance
○ Remove this item and continue
[Continue with selected option]Implementación
function InsuranceFailure({ item, error, onResolve }) {
const options = [
{
id: 'pay-cash',
label: `Pay full price (${formatCurrency(item.cashPrice)})`,
sublabel: 'You can submit for reimbursement later',
},
{
id: 'retry-insurance',
label: 'Try different insurance',
sublabel: 'If you have another policy',
},
{
id: 'remove-item',
label: 'Remove from cart',
sublabel: 'Continue with other items',
},
]
return (
<div>
<Alert type="warning">
Insurance authorization failed: {error.message}
</Alert>
<RadioGroup
options={options}
onChange={(option) => onResolve(option)}
/>
</div>
)
}Principio #5: Señales de confianza en los puntos de decisión
En el carrito
<CartSummary>
<TrustBadge icon="shield">
Licensed pharmacy · COFEPRIS certified
</TrustBadge>
<TrustBadge icon="lock">
Your health data is encrypted
</TrustBadge>
</CartSummary>En el pago
<PaymentForm>
<SecureIndicator>
🔒 256-bit SSL encryption
</SecureIndicator>
<PaymentLogos>
<img src="/visa.svg" alt="Visa" />
<img src="/mastercard.svg" alt="Mastercard" />
<img src="/oxxo.svg" alt="OXXO" />
</PaymentLogos>
</PaymentForm>Después del pedido
<OrderConfirmation>
<Timeline>
<TimelineItem status="complete">
Order received
</TimelineItem>
<TimelineItem status="active">
Pharmacist review (usually 30 min)
</TimelineItem>
<TimelineItem status="pending">
Ready for delivery
</TimelineItem>
</Timeline>
</OrderConfirmation>La arquitectura
┌─────────────────────────────────────────────────────┐
│ Checkout Context │
│ (cart, user, validation states, payment states) │
└─────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Address │ │ Prescription │ │ Payment │
│ Module │ │ Module │ │ Module │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ Background Services │
│ • Address validation • Rx verification │
│ • Shipping calculation • Insurance auth │
│ • Inventory check • Payment processing │
└──────────────────────────────────────────────────────┘- Se puede agregar o quitar según el contenido del carrito
- Tiene sus propios estados de carga y error
- No bloquea a los demás módulos innecesariamente
Métricas que importan
| Métrica | Qué te dice |
|---|---|
| Tasa de finalización por paso | Dónde abandonan los usuarios |
| Tiempo por paso | Dónde batallan los usuarios |
| Tasa de recuperación de errores | Qué tan bien manejas las fallas |
| Distribución de métodos de pago | Qué opciones priorizar |
| Conversión de usuarios recurrentes | Qué tan buenos son tus defaults |
Checkout Funnel (Last 7 Days)
━━━━━━━━━━━━━━━━━━━━━━━━━━━
Cart ████████████████████ 100% (8,420)
Address █████████████████░░░ 85% (7,157)
Rx Upload ████████████████░░░░ 78% (6,567)
Payment ██████████████░░░░░░ 72% (6,062)
Complete █████████████░░░░░░░ 68% (5,725)
Biggest drop: Cart → Address (15%)
Action: Added guest checkout optionResultados
| Métrica | Antes | Después | Cambio |
|---|---|---|---|
| Conversión del checkout | 52% | 68% | +31% |
| Tiempo promedio en checkout | 8:45 | 4:20 | -50% |
| Abandono por errores | 34% | 10% | -70% |
| Conversión de usuarios recurrentes | 61% | 82% | +34% |
Aprendizajes clave
- Oculta la complejidad hasta que sea necesaria - La revelación progresiva mantiene a los usuarios avanzando
- No bloquees por operaciones asíncronas - Deja que los usuarios continúen mientras los procesos corren en background
- Los defaults inteligentes reducen la fricción - Cada decisión es un posible abandono
- Los errores deben ofrecer caminos hacia adelante - Nunca dejes a tus usuarios en un callejón sin salida
- Las señales de confianza importan en compras sensibles - Muestra seguridad donde los usuarios dudan
- Mide paso a paso - La conversión agregada esconde dónde estás perdiendo gente
¿Estás construyendo un sistema de checkout complejo? Llevo años resolviendo estos problemas. Hablemos →


