Skip to main content

Overview

The digital wallet system manages user credit balances, payment processing through Mercado Pago, and transaction history. Credits are used for parking reservations with automatic conversion rates and bonus rewards.

Credit System

Balance Display

The wallet displays the current credit balance in real-time:
const [saldoCreditos, setSaldoCreditos] = useState(0);

useEffect(() => {
  if (!user) return;
  
  const unsub = onSnapshot(
    doc(db, 'users', user.uid), 
    (docSnap) => {
      if (docSnap.exists()) {
        setSaldoCreditos(docSnap.data().credits_balance || 0);
      }
    }
  );
  
  return () => unsub();
}, [user]);

Real-Time Sync

Balance updates instantly across all devices using Firebase real-time listeners.

Minimum Balance Requirement

Reservations require a minimum credit balance:
const SALDO_MINIMO = 120;

if (currentCredits < SALDO_MINIMO) {
  Alert.alert(
    "Saldo Insuficiente",
    "Necesitas al menos 120 créditos para hacer una reserva."
  );
  return;
}
Users cannot create reservations if their balance falls below 120 credits.

Recharge Process

Step 1: Enter Amount

Users can enter any amount or use quick select buttons:
<View style={styles.inputContainer}>
  <Text style={styles.currencySymbol}>$</Text>
  <TextInput 
    style={styles.amountInput}
    placeholder="0"
    keyboardType="numeric"
    value={monto}
    onChangeText={setMonto}
    maxLength={5}
  />
  <Text style={styles.currencyCode}>MXN</Text>
</View>

<View style={styles.quickButtonsRow}>
  {[30, 50, 100].map((val) => (
    <TouchableOpacity 
      key={val} 
      style={styles.quickBtn} 
      onPress={() => setMonto(val.toString())}
    >
      <Text style={styles.quickBtnText}>${val}</Text>
    </TouchableOpacity>
  ))}
</View>
The minimum recharge amount is $30.00 MXN.

Step 2: Credit Conversion

Money is automatically converted to credits with bonus rewards:
useEffect(() => {
  const dinero = parseFloat(monto);
  if (isNaN(dinero)) {
    setCreditosPreview(0);
    return;
  }
  
  let creditos = dinero * 6;  // Base rate: $1 = 6 credits
  
  // Bonus for purchases of 100+ credits
  if (creditos >= 100) {
    creditos += 20;
  }
  
  setCreditosPreview(Math.floor(creditos));
}, [monto]);

Conversion Rate

Amount (MXN)CreditsBonusTotal
$301800180
$503000300
$20 (100+ credits)Base+20Base + 20
{creditosPreview >= 100 && monto !== '' && (
  <View style={styles.bonusBadge}>
    <Text style={styles.bonusText}>
      ¡Incluye +20 Créditos de regalo! 🎉
    </Text>
  </View>
)}

Step 3: Payment Processing

Payments are processed through Mercado Pago integration:
const iniciarRecarga = async () => {
  const dinero = parseFloat(monto);

  if (isNaN(dinero) || dinero < 30) {
    Alert.alert("Monto Inválido", "La recarga mínima es de $30.00 MXN");
    return;
  }

  const usuarioFresco = auth.currentUser;
  if (!usuarioFresco) {
    Alert.alert("Error de Sesión", "No se detecta el usuario.");
    return;
  }

  setLoadingPago(true);
  
  try {
    const crearPreferencia = httpsCallable(functions, 'crearPreferenciaMP');
    
    const resultado: any = await crearPreferencia({ 
      monto: dinero,
      uid: usuarioFresco.uid 
    });

    const datos = resultado.data;

    if (datos.success === false) {
      Alert.alert("Error del Servidor", datos.message);
      return;
    }
    
    const urlPago = datos.url;
    
    if (urlPago) {
      Linking.openURL(urlPago);
    }
  } catch (error: any) {
    Alert.alert("Error de Conexión", error.message);
  } finally {
    setLoadingPago(false);
  }
};
1

Create Payment Preference

Call Firebase Cloud Function to create Mercado Pago preference
2

Open Payment URL

Launch Mercado Pago checkout in device browser
3

Process Payment

Mercado Pago handles the payment flow
4

Webhook Notification

Server receives webhook and updates user balance

Payment Card Display

The wallet shows linked payment cards:
<View style={styles.cardInfo}>
  <Ionicons name="card-outline" size={18} color="#AAA" />
  <Text style={styles.cardText}>
    Vinculado a Tarjeta Titular •••• {tarjetaTitular || '----'}
  </Text>
</View>
<Text style={styles.cardSubText}>
  Este saldo se comparte con todas tus tarjetas asociadas.
</Text>

Primary Card Detection

useEffect(() => {
  const fetchCard = async () => {
    if (!user) return;
    try {
      const q = query(
        collection(db, 'users', user.uid, 'cards'),
        where('isPrimary', '==', true)
      );
      const snap = await getDocs(q);
      if (!snap.empty) {
        const cardData = snap.docs[0].data();
        setTarjetaTitular(cardData.last4 || '****');
      }
    } catch (error) {
      console.log("Error loading card");
    }
  };
  fetchCard();
}, [user]);

Balance Card Design

The wallet features a premium dark card design:
<View style={styles.balanceCard}>
  <View style={styles.balanceHeader}>
    <Text style={styles.balanceLabel}>SALDO DISPONIBLE</Text>
    <Ionicons name="wallet" size={24} color="#FFE100" />
  </View>
  
  <Text style={styles.balanceAmount}>{saldoCreditos}</Text>
  <Text style={styles.balanceUnit}>CRÉDITOS</Text>
  
  <View style={styles.divider} />
  
  <View style={styles.cardInfo}>
    <Ionicons name="card-outline" size={18} color="#AAA" />
    <Text style={styles.cardText}>
      Vinculado a Tarjeta Titular •••• {tarjetaTitular || '----'}
    </Text>
  </View>
</View>

Styling

balanceCard: {
  backgroundColor: '#1a1a1a',
  borderRadius: 20,
  padding: 25,
  marginBottom: 30,
  shadowColor: "#000",
  shadowOffset: { width: 0, height: 5 },
  shadowOpacity: 0.3,
  elevation: 8
}
The dark theme creates a premium feel and improves text contrast for the yellow accent colors.

Transaction Flow

Credit Usage

Credits are deducted during reservation confirmation:
// Reservations require minimum balance check
if (currentCredits < SALDO_MINIMO) {
  Alert.alert(
    "Saldo Insuficiente",
    `Necesitas al menos ${SALDO_MINIMO} créditos. Tu saldo actual: ${currentCredits}"
  );
  return;
}

// Credit deduction happens server-side when reservation activates
Credit deductions are processed atomically using Firebase transactions to prevent race conditions.

Shared Balance

Multi-Card Support

The credit balance is shared across all payment cards linked to the account. Users don’t need to maintain separate balances for each card.

Payment Security

All payments are processed through Mercado Pago’s secure platform. The app never handles sensitive card information.
Payment preferences are created server-side via Firebase Cloud Functions to protect API credentials.
Server validates webhook signatures from Mercado Pago before updating user balances.

Error Handling

try {
  const resultado = await crearPreferencia({ monto: dinero, uid: userId });
  
  if (resultado.data.success === false) {
    Alert.alert(
      "Error del Servidor", 
      resultado.data.message + "\n" + (resultado.data.details || '')
    );
    return;
  }
  
  Linking.openURL(resultado.data.url);
} catch (error: any) {
  Alert.alert("Error de Conexión", error.message);
}
Comprehensive error handling provides clear feedback for connection issues, server errors, and validation failures.

Build docs developers (and LLMs) love