feat(mobile): match 100% web functionality and aesthetic for DripZone native Kotlin app

- Integrated requested splash screen dripzone_splash_clean_1787718656035 with SplashActivity
- Aligned dark streetwear palette: #0A0A0A, purple #9333EA, fuchsia #D946EF, glow borders
- Added CitySelectorBottomSheet with Colombian cities, delivery times, and 1st free exchange guarantee
- Implemented multi-product CartManager, CartBottomSheet, breakdown and WhatsApp checkout
- Updated ProductDetailBottomSheet with size chips, Add to Cart, and direct WhatsApp buy
- Added MobileBottomDock with 5 tabs and real-time cart badge counter
- Hardened security: network security config (cleartext blocked), encrypted preferences, tapjacking protection
This commit is contained in:
AI Bot 2026-09-05 10:57:43 -05:00
parent 9f25aabcd8
commit fef85c3ff4
55 changed files with 2004 additions and 202 deletions

View File

@ -28,6 +28,20 @@
android:theme="@style/Theme.DripZone"
tools:targetApi="31">
<!-- Splash Activity: Entry point featuring dripzone_splash_clean -->
<activity
android:name=".SplashActivity"
android:theme="@style/Theme.DripZone.Splash"
android:screenOrientation="portrait"
android:launchMode="singleTop"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Main Store Activity -->
<activity
android:name=".MainActivity"
android:label="@string/title_activity_main"
@ -36,14 +50,7 @@
android:windowSoftInputMode="adjustPan"
android:configChanges="orientation|keyboardHidden|screenSize"
android:launchMode="singleTop"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
android:exported="false" />
</application>

View File

@ -1,19 +1,25 @@
package co.dripzone.app
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import co.dripzone.app.api.ApiClient
import co.dripzone.app.databinding.ActivityMainBinding
import co.dripzone.app.model.CartManager
import co.dripzone.app.model.Category
import co.dripzone.app.model.City
import co.dripzone.app.model.Product
import co.dripzone.app.security.SecurityManager
import co.dripzone.app.ui.CartBottomSheet
import co.dripzone.app.ui.CategoryAdapter
import co.dripzone.app.ui.CitySelectorBottomSheet
import co.dripzone.app.ui.CustomOrderBottomSheet
import co.dripzone.app.ui.ProductAdapter
import co.dripzone.app.ui.ProductDetailBottomSheet
@ -28,20 +34,39 @@ class MainActivity : AppCompatActivity() {
private lateinit var categoryAdapter: CategoryAdapter
private var allProducts: List<Product> = emptyList()
private var availableCities: List<City> = City.defaultCities()
private var selectedCity: City = City.defaultCities().first()
private var selectedCategory: String = "Todos"
private var searchQuery: String = ""
private val cartListener = {
updateCartBadges()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
CartManager.init(this)
CartManager.addListener(cartListener)
loadSavedCity()
setupRecyclerViews()
setupCategories()
setupSearch()
setupListeners()
setupCitySelector()
setupBottomDock()
setupHeaderListeners()
updateCartBadges()
loadProducts(forceRefresh = false)
loadCities()
}
override fun onDestroy() {
super.onDestroy()
CartManager.removeListener(cartListener)
}
private fun setupRecyclerViews() {
@ -51,21 +76,8 @@ class MainActivity : AppCompatActivity() {
.show(supportFragmentManager, ProductDetailBottomSheet.TAG)
},
onQuickOrderClick = { product ->
val whatsappMsg = buildString {
append("¡Hola DripZone! 🔥 Quiero pedir este producto con *Pago Contraentrega*:\n\n")
append("👟 *${product.name}*\n")
append("🏷️ *Marca:* ${product.brand}\n")
append("💰 *Precio:* ${product.formattedPrice()}\n")
if (product.firstImage().isNotBlank()) {
append("🔗 *Foto:* ${product.firstImage()}\n")
}
append("\n¿Tienen disponibilidad para envío inmediato?")
}
SecurityManager.openWhatsApp(
this,
SecurityManager.WHATSAPP_STORE_PHONE,
whatsappMsg
)
ProductDetailBottomSheet.newInstance(product)
.show(supportFragmentManager, ProductDetailBottomSheet.TAG)
}
)
@ -96,15 +108,7 @@ class MainActivity : AppCompatActivity() {
private fun setupSearch() {
binding.btnToggleSearch.setOnClickListener {
if (binding.layoutSearchBar.visibility == View.VISIBLE) {
binding.layoutSearchBar.visibility = View.GONE
binding.etSearch.text?.clear()
hideKeyboard()
} else {
binding.layoutSearchBar.visibility = View.VISIBLE
binding.etSearch.requestFocus()
showKeyboard()
}
toggleSearch()
}
binding.btnClearSearch.setOnClickListener {
@ -142,28 +146,170 @@ class MainActivity : AppCompatActivity() {
}
}
private fun setupListeners() {
// Swipe to Refresh: fetches newest drops published from WhatsApp supplier groups
binding.swipeRefresh.setColorSchemeResources(R.color.accent_green)
private fun toggleSearch() {
if (binding.layoutSearchBar.visibility == View.VISIBLE) {
binding.layoutSearchBar.visibility = View.GONE
binding.etSearch.text?.clear()
hideKeyboard()
} else {
binding.layoutSearchBar.visibility = View.VISIBLE
binding.etSearch.requestFocus()
showKeyboard()
}
}
private fun setupCitySelector() {
updateCityDisplay()
binding.btnChangeCityBar.setOnClickListener {
openCitySelector()
}
}
private fun openCitySelector() {
CitySelectorBottomSheet.newInstance(
cities = availableCities,
selectedCity = selectedCity
) { city ->
selectedCity = city
saveSelectedCity(city)
updateCityDisplay()
}.show(supportFragmentManager, CitySelectorBottomSheet.TAG)
}
private fun updateCityDisplay() {
val freeExchangeText = if (selectedCity.firstExchangeFree) " • 1er Cambio Gratis" else ""
binding.tvHeaderCityInfo.text = "Enviando a ${selectedCity.name} • ${selectedCity.deliveryTime}$freeExchangeText"
}
private fun loadSavedCity() {
try {
val prefs = SecurityManager.getSecurePreferences(this)
val cityName = prefs.getString("saved_city_name", null)
if (!cityName.isNullOrBlank()) {
val found = availableCities.find { it.name.equals(cityName, ignoreCase = true) }
if (found != null) {
selectedCity = found
}
}
} catch (e: Exception) {
// Ignore
}
}
private fun saveSelectedCity(city: City) {
try {
val prefs = SecurityManager.getSecurePreferences(this)
prefs.edit().putString("saved_city_name", city.name).apply()
} catch (e: Exception) {
// Ignore
}
}
private fun setupBottomDock() {
// 1. Inicio
binding.navBtnHome.setOnClickListener {
setDockActive("home")
binding.rvProducts.smoothScrollToPosition(0)
}
// 2. Buscar
binding.navBtnSearch.setOnClickListener {
setDockActive("search")
toggleSearch()
}
// 3. Carrito
binding.navBtnCart.setOnClickListener {
openCart()
}
// 4. Encargos
binding.navBtnEncargos.setOnClickListener {
CustomOrderBottomSheet.newInstance()
.show(supportFragmentManager, CustomOrderBottomSheet.TAG)
}
// 5. WhatsApp Asesor
binding.navBtnWhatsApp.setOnClickListener {
openGeneralWhatsApp()
}
}
private fun setDockActive(tab: String) {
val fuchsia = ContextCompat.getColor(this, R.color.drip_fuchsia)
val muted = ContextCompat.getColor(this, R.color.text_secondary)
binding.ivNavHome.setColorFilter(if (tab == "home") fuchsia else muted)
binding.tvNavHome.setTextColor(if (tab == "home") fuchsia else muted)
binding.ivNavSearch.setColorFilter(if (tab == "search") fuchsia else muted)
binding.tvNavSearch.setTextColor(if (tab == "search") fuchsia else muted)
}
private fun setupHeaderListeners() {
// Header Cart icon
binding.btnHeaderCart.setOnClickListener {
openCart()
}
// Header WhatsApp button
binding.btnHeaderWhatsApp.setOnClickListener {
openGeneralWhatsApp()
}
// SwipeRefresh: pulls latest live drops from suppliers
binding.swipeRefresh.setColorSchemeResources(R.color.drip_fuchsia, R.color.drip_purple)
binding.swipeRefresh.setProgressBackgroundColorSchemeResource(R.color.bg_card)
binding.swipeRefresh.setOnRefreshListener {
loadProducts(forceRefresh = true)
}
}
// WhatsApp direct chat from header
binding.btnHeaderWhatsApp.setOnClickListener {
val generalInquiry = "¡Hola DripZone! 🔥 Me comunico desde la aplicación móvil. Quiero información sobre los productos y envíos contraentrega."
SecurityManager.openWhatsApp(
this,
SecurityManager.WHATSAPP_STORE_PHONE,
generalInquiry
)
private fun openCart() {
CartBottomSheet.newInstance(selectedCity) {
openCitySelector()
}.show(supportFragmentManager, CartBottomSheet.TAG)
}
private fun openGeneralWhatsApp() {
val inquiry = "¡Hola DripZone! 🔥 Me comunico desde la app móvil. Quiero información sobre los productos disponibles para entrega contraentrega en ${selectedCity.name}."
SecurityManager.openWhatsApp(
this,
SecurityManager.WHATSAPP_STORE_PHONE,
inquiry
)
}
private fun updateCartBadges() {
val count = CartManager.getTotalCount()
if (count > 0) {
binding.tvHeaderCartBadge.visibility = View.VISIBLE
binding.tvHeaderCartBadge.text = count.toString()
binding.tvBottomCartBadge.visibility = View.VISIBLE
binding.tvBottomCartBadge.text = count.toString()
} else {
binding.tvHeaderCartBadge.visibility = View.GONE
binding.tvBottomCartBadge.visibility = View.GONE
}
}
// Floating Action Button: Encargos Especiales
binding.fabCustomOrder.setOnClickListener {
CustomOrderBottomSheet.newInstance()
.show(supportFragmentManager, CustomOrderBottomSheet.TAG)
private fun loadCities() {
lifecycleScope.launch {
try {
val fetched = ApiClient.getCities(this@MainActivity)
if (fetched.isNotEmpty()) {
availableCities = fetched
val currentCityId = selectedCity.id
val matched = availableCities.find { it.id == currentCityId }
if (matched != null) {
selectedCity = matched
}
updateCityDisplay()
}
} catch (e: Exception) {
// Keep default
}
}
}
@ -181,17 +327,17 @@ class MainActivity : AppCompatActivity() {
if (forceRefresh) {
Snackbar.make(
binding.rootCoordinator,
"Catálogo actualizado con los últimos drops",
"Catálogo sincronizado con los últimos drops de WhatsApp",
Snackbar.LENGTH_SHORT
).setBackgroundTint(getColor(R.color.bg_card_elevated))
.setTextColor(getColor(R.color.accent_green))
.setTextColor(getColor(R.color.drip_fuchsia))
.show()
}
} catch (e: Exception) {
Snackbar.make(
binding.rootCoordinator,
"Cargando catálogo en modo sin conexión",
Snackbar.LENGTH_LONG
"Catálogo cargado en modo sin conexión",
Snackbar.LENGTH_SHORT
).show()
} finally {
binding.progressBar.visibility = View.GONE
@ -242,12 +388,12 @@ class MainActivity : AppCompatActivity() {
}
private fun showKeyboard() {
val imm = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(binding.etSearch, InputMethodManager.SHOW_IMPLICIT)
}
private fun hideKeyboard() {
val imm = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(binding.etSearch.windowToken, 0)
}
}

View File

@ -0,0 +1,29 @@
package co.dripzone.app
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import co.dripzone.app.databinding.ActivitySplashBinding
@SuppressLint("CustomSplashScreen")
class SplashActivity : AppCompatActivity() {
private lateinit var binding: ActivitySplashBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySplashBinding.inflate(layoutInflater)
setContentView(binding.root)
// Smooth 1.2s delay to showcase DripZone clean splash screen branding
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(this@SplashActivity, MainActivity::class.java)
startActivity(intent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}, 1200)
}
}

View File

@ -162,4 +162,32 @@ object ApiClient {
)
)
}
suspend fun getCities(context: Context): List<co.dripzone.app.model.City> =
withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url("https://dripzone.almeira.com.co/api/cities")
.header("Accept", "application/json")
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
val body = response.body?.string()
if (!body.isNullOrBlank()) {
val obj = gson.fromJson(body, com.google.gson.JsonObject::class.java)
if (obj.has("cities")) {
val listType = object : TypeToken<List<co.dripzone.app.model.City>>() {}.type
val list: List<co.dripzone.app.model.City>? = gson.fromJson(obj.get("cities"), listType)
if (!list.isNullOrEmpty()) {
return@withContext list
}
}
}
}
} catch (e: Exception) {
Log.w(TAG, "Error fetching cities from API: ${e.message}")
}
co.dripzone.app.model.City.defaultCities()
}
}

View File

@ -0,0 +1,25 @@
package co.dripzone.app.model
import java.io.Serializable
import java.text.NumberFormat
import java.util.Locale
data class CartItem(
val product: Product,
val selectedSize: String,
var quantity: Int = 1
) : Serializable {
val itemTotal: Double
get() = product.effectivePrice * quantity
fun formattedItemTotal(): String {
return try {
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO"))
format.maximumFractionDigits = 0
format.format(itemTotal) + " COP"
} catch (e: Exception) {
"$ ${itemTotal.toLong()} COP"
}
}
}

View File

@ -0,0 +1,119 @@
package co.dripzone.app.model
import android.content.Context
import co.dripzone.app.security.SecurityManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.text.NumberFormat
import java.util.Locale
object CartManager {
private const val CART_PREFS_KEY = "dripzone_cart_items"
private val cartItems = mutableListOf<CartItem>()
private val listeners = mutableListOf<() -> Unit>()
private val gson = Gson()
private var initialized = false
fun init(context: Context) {
if (initialized) return
initialized = true
try {
val prefs = SecurityManager.getSecurePreferences(context)
val json = prefs.getString(CART_PREFS_KEY, null)
if (!json.isNullOrBlank()) {
val type = object : TypeToken<List<CartItem>>() {}.type
val loaded: List<CartItem>? = gson.fromJson(json, type)
if (loaded != null) {
cartItems.clear()
cartItems.addAll(loaded)
}
}
} catch (e: Exception) {
// Safe fallback
}
}
private fun persist(context: Context) {
try {
val prefs = SecurityManager.getSecurePreferences(context)
val json = gson.toJson(cartItems)
prefs.edit().putString(CART_PREFS_KEY, json).apply()
} catch (e: Exception) {
// Ignore
}
}
fun addItem(context: Context, product: Product, size: String, quantity: Int = 1) {
init(context)
val existingIndex = cartItems.indexOfFirst {
it.product.id == product.id && it.selectedSize == size
}
if (existingIndex >= 0) {
cartItems[existingIndex].quantity += quantity
} else {
cartItems.add(CartItem(product = product, selectedSize = size, quantity = quantity))
}
persist(context)
notifyListeners()
}
fun removeItem(context: Context, productId: String, size: String) {
init(context)
cartItems.removeAll { it.product.id == productId && it.selectedSize == size }
persist(context)
notifyListeners()
}
fun updateQuantity(context: Context, productId: String, size: String, delta: Int) {
init(context)
val index = cartItems.indexOfFirst { it.product.id == productId && it.selectedSize == size }
if (index >= 0) {
val newQty = cartItems[index].quantity + delta
if (newQty <= 0) {
cartItems.removeAt(index)
} else {
cartItems[index].quantity = newQty
}
persist(context)
notifyListeners()
}
}
fun clear(context: Context) {
cartItems.clear()
persist(context)
notifyListeners()
}
fun getItems(): List<CartItem> = cartItems.toList()
fun getTotalCount(): Int = cartItems.sumOf { it.quantity }
fun getSubtotal(): Double = cartItems.sumOf { it.itemTotal }
fun formattedSubtotal(): String {
val total = getSubtotal()
return try {
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO"))
format.maximumFractionDigits = 0
format.format(total) + " COP"
} catch (e: Exception) {
"$ ${total.toLong()} COP"
}
}
fun addListener(listener: () -> Unit) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}
fun removeListener(listener: () -> Unit) {
listeners.remove(listener)
}
private fun notifyListeners() {
listeners.forEach { it.invoke() }
}
}

View File

@ -0,0 +1,122 @@
package co.dripzone.app.model
import com.google.gson.annotations.SerializedName
import java.io.Serializable
import java.text.NumberFormat
import java.util.Locale
data class City(
@SerializedName("id")
val id: String = "",
@SerializedName("name")
val name: String = "",
@SerializedName("department")
val department: String = "",
@SerializedName("shippingCost")
val shippingCost: Double = 0.0,
@SerializedName("deliveryTime")
val deliveryTime: String = "24-48 horas",
@SerializedName("allowsContraentrega")
val allowsContraentrega: Boolean = true,
@SerializedName("firstExchangeFree")
val firstExchangeFree: Boolean = false,
@SerializedName("isActive")
val isActive: Boolean = true
) : Serializable {
fun formattedShipping(): String {
return if (shippingCost <= 0) {
"Envío GRATIS"
} else {
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO"))
format.maximumFractionDigits = 0
format.format(shippingCost) + " COP"
}
}
companion object {
fun defaultCities(): List<City> {
return listOf(
City(
id = "cali",
name = "Cali",
department = "Valle del Cauca",
shippingCost = 0.0,
deliveryTime = "Mismo día / 24h",
allowsContraentrega = true,
firstExchangeFree = true
),
City(
id = "bogota",
name = "Bogotá",
department = "Cundinamarca",
shippingCost = 15000.0,
deliveryTime = "1 a 2 días",
allowsContraentrega = true,
firstExchangeFree = false
),
City(
id = "medellin",
name = "Medellín",
department = "Antioquia",
shippingCost = 15000.0,
deliveryTime = "1 a 2 días",
allowsContraentrega = true,
firstExchangeFree = false
),
City(
id = "barranquilla",
name = "Barranquilla",
department = "Atlántico",
shippingCost = 18000.0,
deliveryTime = "2 a 3 días",
allowsContraentrega = true,
firstExchangeFree = false
),
City(
id = "bucaramanga",
name = "Bucaramanga",
department = "Santander",
shippingCost = 16000.0,
deliveryTime = "2 a 3 días",
allowsContraentrega = true,
firstExchangeFree = false
),
City(
id = "pereira",
name = "Pereira / Eje Cafetero",
department = "Risaralda",
shippingCost = 14000.0,
deliveryTime = "24 a 48 horas",
allowsContraentrega = true,
firstExchangeFree = false
),
City(
id = "cartagena",
name = "Cartagena",
department = "Bolívar",
shippingCost = 18000.0,
deliveryTime = "2 a 3 días",
allowsContraentrega = true,
firstExchangeFree = false
),
City(
id = "otra",
name = "Otro municipio de Colombia",
department = "Nacional",
shippingCost = 18000.0,
deliveryTime = "2 a 4 días",
allowsContraentrega = true,
firstExchangeFree = false
)
)
}
}
}

View File

@ -0,0 +1,75 @@
package co.dripzone.app.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import co.dripzone.app.R
import co.dripzone.app.databinding.ItemCartItemBinding
import co.dripzone.app.model.CartItem
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
class CartAdapter(
private val items: MutableList<CartItem>,
private val onQuantityChanged: (CartItem, Int) -> Unit,
private val onItemRemoved: (CartItem) -> Unit
) : RecyclerView.Adapter<CartAdapter.CartViewHolder>() {
inner class CartViewHolder(private val binding: ItemCartItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: CartItem) {
val context = binding.root.context
binding.tvCartName.text = item.product.name
binding.tvCartSize.text = "Talla: ${item.selectedSize}"
binding.tvCartPrice.text = item.formattedItemTotal()
binding.tvQuantity.text = item.quantity.toString()
val imageUrl = item.product.firstImage()
if (imageUrl.isNotBlank()) {
Glide.with(context)
.load(imageUrl)
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
.placeholder(R.drawable.bg_product_card)
.into(binding.ivCartThumb)
} else {
binding.ivCartThumb.setImageResource(R.drawable.splash)
}
binding.btnPlus.setOnClickListener {
onQuantityChanged(item, 1)
}
binding.btnMinus.setOnClickListener {
onQuantityChanged(item, -1)
}
binding.btnRemoveCartItem.setOnClickListener {
onItemRemoved(item)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder {
val binding = ItemCartItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return CartViewHolder(binding)
}
override fun onBindViewHolder(holder: CartViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
fun updateData(newItems: List<CartItem>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,152 @@
package co.dripzone.app.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import co.dripzone.app.databinding.BottomSheetCartBinding
import co.dripzone.app.model.CartItem
import co.dripzone.app.model.CartManager
import co.dripzone.app.model.City
import co.dripzone.app.security.SecurityManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import java.text.NumberFormat
import java.util.Locale
class CartBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetCartBinding? = null
private val binding get() = _binding!!
private var cartAdapter: CartAdapter? = null
private var selectedCity: City = City.defaultCities().first()
private var onOpenCitySelectorListener: (() -> Unit)? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetCartBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnCloseCart.setOnClickListener {
dismiss()
}
binding.cardChangeCity.setOnClickListener {
onOpenCitySelectorListener?.invoke()
}
val items = CartManager.getItems().toMutableList()
cartAdapter = CartAdapter(
items = items,
onQuantityChanged = { item, delta ->
CartManager.updateQuantity(requireContext(), item.product.id, item.selectedSize, delta)
refreshCartUI()
},
onItemRemoved = { item ->
CartManager.removeItem(requireContext(), item.product.id, item.selectedSize)
refreshCartUI()
}
)
binding.rvCartItems.adapter = cartAdapter
binding.btnCheckoutWhatsApp.setOnClickListener {
val cartList = CartManager.getItems()
if (cartList.isEmpty()) return@setOnClickListener
val subtotal = CartManager.getSubtotal()
val total = subtotal + selectedCity.shippingCost
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
val itemsText = cartList.joinToString("\n") {
"👟 *${it.product.name}* (Talla: *${it.selectedSize}*, Cant: ${it.quantity}) - ${it.formattedItemTotal()}"
}
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO"))
format.maximumFractionDigits = 0
val formattedTotal = format.format(total) + " COP"
val formattedBalance = format.format(balanceDue) + " COP"
val message = buildString {
append("¡Hola DripZone! 🔥 Quiero ordenar este pedido desde la app móvil:\n\n")
append("📦 *RESUMEN DEL PEDIDO:*\n")
append(itemsText)
append("\n\n📍 *Ciudad de Despacho:* ${selectedCity.name} (${selectedCity.formattedShipping()})\n")
append("💰 *TOTAL A PAGAR:* $formattedTotal\n")
append("⚡ *Condición:* Pago Contraentrega (Separo con $10.000 COP y cancelo saldo de $formattedBalance al recibir en mi puerta).\n\n")
append("¿Me confirman disponibilidad para programar el despacho?")
}
SecurityManager.openWhatsApp(
requireContext(),
SecurityManager.WHATSAPP_STORE_PHONE,
message
)
dismiss()
}
refreshCartUI()
}
fun setSelectedCity(city: City) {
selectedCity = city
if (_binding != null) {
refreshCartUI()
}
}
private fun refreshCartUI() {
val items = CartManager.getItems()
cartAdapter?.updateData(items)
val count = CartManager.getTotalCount()
binding.tvCartCountSubtitle.text = "$count ${if (count == 1) "artículo seleccionado" else "artículos seleccionados"}"
if (items.isEmpty()) {
binding.layoutCartEmpty.visibility = View.VISIBLE
binding.layoutCartContent.visibility = View.GONE
} else {
binding.layoutCartEmpty.visibility = View.GONE
binding.layoutCartContent.visibility = View.VISIBLE
binding.tvCartCityName.text = "Destino: ${selectedCity.name}"
binding.tvCartCityInfo.text = "${selectedCity.deliveryTime} • Toca para cambiar ciudad"
binding.tvCartShippingFee.text = selectedCity.formattedShipping()
val subtotal = CartManager.getSubtotal()
val shipping = selectedCity.shippingCost
val total = subtotal + shipping
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO"))
format.maximumFractionDigits = 0
binding.tvSubtotal.text = format.format(subtotal) + " COP"
binding.tvShippingTotal.text = selectedCity.formattedShipping()
binding.tvCartTotalAmount.text = format.format(total) + " COP"
binding.tvSeparationBreakdown.text = "Separas tu pedido con $10.000 COP y cancelas el saldo restante (${format.format(balanceDue)} COP) en efectivo al recibir."
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val TAG = "CartBottomSheet"
fun newInstance(city: City, onOpenCitySelector: () -> Unit): CartBottomSheet {
return CartBottomSheet().apply {
this.selectedCity = city
this.onOpenCitySelectorListener = onOpenCitySelector
}
}
}
}

View File

@ -0,0 +1,61 @@
package co.dripzone.app.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import co.dripzone.app.R
import co.dripzone.app.databinding.ItemCityBinding
import co.dripzone.app.model.City
class CityAdapter(
private val cities: List<City>,
private var selectedCityId: String,
private val onCitySelected: (City) -> Unit
) : RecyclerView.Adapter<CityAdapter.CityViewHolder>() {
inner class CityViewHolder(private val binding: ItemCityBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(city: City) {
val isSelected = city.id == selectedCityId
val context = binding.root.context
binding.tvCityName.text = city.name
binding.tvCityDeliveryTime.text = "${city.deliveryTime} • ${city.department}"
binding.tvShippingCost.text = city.formattedShipping()
if (isSelected) {
binding.ivCheck.visibility = View.VISIBLE
binding.cardCity.strokeColor = ContextCompat.getColor(context, R.color.drip_fuchsia)
binding.cardCity.setCardBackgroundColor(ContextCompat.getColor(context, R.color.bg_card_elevated))
} else {
binding.ivCheck.visibility = View.INVISIBLE
binding.cardCity.strokeColor = ContextCompat.getColor(context, R.color.bg_card_stroke)
binding.cardCity.setCardBackgroundColor(ContextCompat.getColor(context, R.color.bg_card))
}
binding.cardCity.setOnClickListener {
selectedCityId = city.id
notifyDataSetChanged()
onCitySelected(city)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CityViewHolder {
val binding = ItemCityBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return CityViewHolder(binding)
}
override fun onBindViewHolder(holder: CityViewHolder, position: Int) {
holder.bind(cities[position])
}
override fun getItemCount(): Int = cities.size
}

View File

@ -0,0 +1,67 @@
package co.dripzone.app.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import co.dripzone.app.databinding.BottomSheetCitySelectorBinding
import co.dripzone.app.model.City
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class CitySelectorBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetCitySelectorBinding? = null
private val binding get() = _binding!!
private var cities: List<City> = emptyList()
private var selectedCity: City? = null
private var onCitySelectedListener: ((City) -> Unit)? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetCitySelectorBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnCloseCity.setOnClickListener {
dismiss()
}
val adapter = CityAdapter(
cities = cities,
selectedCityId = selectedCity?.id ?: "cali"
) { city ->
onCitySelectedListener?.invoke(city)
dismiss()
}
binding.rvCities.adapter = adapter
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val TAG = "CitySelectorBottomSheet"
fun newInstance(
cities: List<City>,
selectedCity: City,
onSelected: (City) -> Unit
): CitySelectorBottomSheet {
return CitySelectorBottomSheet().apply {
this.cities = cities
this.selectedCity = selectedCity
this.onCitySelectedListener = onSelected
}
}
}
}

View File

@ -66,6 +66,18 @@ class ProductDetailBottomSheet : BottomSheetDialogFragment() {
dismiss()
}
// Add to Cart Button
binding.btnAddToCart.setOnClickListener {
val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "38-43"
co.dripzone.app.model.CartManager.addItem(requireContext(), p, sizeToOrder, 1)
android.widget.Toast.makeText(
requireContext(),
"¡Agregado al Carrito! (${p.name} - Talla $sizeToOrder)",
android.widget.Toast.LENGTH_SHORT
).show()
dismiss()
}
// WhatsApp Order Checkout Button
binding.btnOrderWhatsApp.setOnClickListener {
val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "Por coordinar"

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 KiB

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:angle="0"
android:startColor="#D946EF"
android:endColor="#EC4899"
android:type="linear" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/badge_drip_bg" />
<stroke
android:width="1dp"
android:color="@color/badge_drip_border" />
<corners android:radius="12dp" />
</shape>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="0"
android:startColor="#9333EA"
android:centerColor="#D946EF"
android:endColor="#EC4899"
android:type="linear" />
<corners android:radius="10dp" />
</shape>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#40FFFFFF">
<item>
<shape android:shape="rectangle">
<gradient
android:angle="0"
android:startColor="#9333EA"
android:endColor="#D946EF"
android:type="linear" />
<corners android:radius="16dp" />
</shape>
</item>
</ripple>

View File

@ -2,7 +2,11 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/accent_green" />
<gradient
android:angle="0"
android:startColor="#9333EA"
android:endColor="#D946EF"
android:type="linear" />
<corners android:radius="20dp" />
</shape>
</item>

View File

@ -2,7 +2,11 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/accent_green" />
<gradient
android:angle="0"
android:startColor="#9333EA"
android:endColor="#D946EF"
android:type="linear" />
<corners android:radius="12dp" />
</shape>
</item>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#EDEDED"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#D946EF"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zm0,9.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#EDEDED"
android:pathData="M18,6h-2c0,-2.21 -1.79,-4 -4,-4S8,3.79 8,6H6c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2zm-6,-2c1.1,0 2,0.9 2,2h-4c0,-1.1 0.9,-2 2,-2zm6,16H6V8h2v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1V8h4v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1V8h2v12z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -14,39 +14,82 @@
android:background="@color/bg_dark"
app:elevation="0dp">
<!-- Top Header Bar -->
<!-- Top Urgency Web Banner (Purple-Fuchsia-Pink Gradient) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_banner_gradient"
android:gravity="center"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp">
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/ic_sparkle"
app:tint="#FFFFFF" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:fontFamily="sans-serif-black"
android:letterSpacing="0.04"
android:text="🔥 ENVÍOS CONTRAENTREGA EN TODO COLOMBIA"
android:textColor="#FFFFFF"
android:textSize="10sp"
android:textStyle="bold" />
</LinearLayout>
<!-- Main Header Bar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingTop="12dp"
android:paddingBottom="8dp">
android:paddingTop="10dp"
android:paddingBottom="6dp">
<!-- Brand Logo / Title -->
<!-- DripZone Logo (DRIP in White, ZONE in Fuchsia) -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-black"
android:letterSpacing="0.08"
android:text="DRIPZONE"
android:textColor="@color/text_primary"
android:textSize="20sp" />
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-black"
android:letterSpacing="0.08"
android:text="DRIP"
android:textColor="@color/text_primary"
android:textSize="22sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-black"
android:letterSpacing="0.08"
android:text="ZONE"
android:textColor="@color/drip_fuchsia"
android:textSize="22sp" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:letterSpacing="0.05"
android:text="SNEAKERS &amp; STREETWEAR"
android:textColor="@color/accent_green"
android:text="ROPA URBANA Y ZAPATILLAS"
android:textColor="@color/badge_drip_text"
android:textSize="10sp"
android:textStyle="bold" />
</LinearLayout>
@ -58,52 +101,130 @@
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Buscar"
android:src="@drawable/ic_search" />
android:src="@drawable/ic_search"
app:tint="@color/text_secondary" />
<!-- WhatsApp Direct Support -->
<!-- Header Cart Icon with Dynamic Badge -->
<FrameLayout
android:id="@+id/btnHeaderCart"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_gravity="center"
android:src="@drawable/ic_shopping_bag"
app:tint="@color/text_primary" />
<TextView
android:id="@+id/tvHeaderCartBadge"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="top|end"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:background="@drawable/bg_badge_counter"
android:gravity="center"
android:text="0"
android:textColor="#FFFFFF"
android:textSize="9sp"
android:textStyle="bold"
android:visibility="gone" />
</FrameLayout>
<!-- Header WhatsApp Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnHeaderWhatsApp"
android:layout_width="wrap_content"
android:layout_height="38dp"
android:layout_marginStart="8dp"
android:layout_height="36dp"
android:layout_marginStart="6dp"
android:filterTouchesWhenObscured="true"
android:insetTop="0dp"
android:insetBottom="0dp"
android:paddingHorizontal="12dp"
android:text="WhatsApp"
android:paddingHorizontal="10dp"
android:text="Asesor"
android:textAllCaps="false"
android:textSize="12sp"
android:textSize="11sp"
android:textStyle="bold"
app:backgroundTint="@color/accent_whatsapp"
app:cornerRadius="12dp"
app:cornerRadius="10dp"
app:icon="@drawable/ic_whatsapp"
app:iconGravity="textStart"
app:iconPadding="6dp"
app:iconSize="16dp" />
app:iconPadding="4dp"
app:iconSize="14dp" />
</LinearLayout>
<!-- Search Input Bar (Visible or Toggleable) -->
<!-- City Selector Pill Bar (Matching Web Location Selector) -->
<LinearLayout
android:id="@+id/btnChangeCityBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="6dp"
android:background="@drawable/bg_btn_secondary"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="7dp">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_pin" />
<TextView
android:id="@+id/tvHeaderCityInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:maxLines="1"
android:text="Enviando a Cali • Mismo día / 24h • 1er Cambio Gratis"
android:textColor="@color/text_primary"
android:textSize="11sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-bold"
android:text="Cambiar"
android:textColor="@color/drip_purple_glow"
android:textSize="11sp" />
</LinearLayout>
<!-- Search Input Bar -->
<LinearLayout
android:id="@+id/layoutSearchBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="6dp"
android:background="@drawable/bg_search_input"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp">
android:paddingHorizontal="12dp"
android:visibility="gone">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_search" />
android:src="@drawable/ic_search"
app:tint="@color/drip_purple_light" />
<EditText
android:id="@+id/etSearch"
android:layout_width="0dp"
android:layout_height="42dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@null"
@ -125,34 +246,6 @@
android:visibility="gone" />
</LinearLayout>
<!-- Live Drops Banner -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_btn_secondary"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_sparkle" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="sans-serif-bold"
android:letterSpacing="0.04"
android:text="⚡ DROPS SINCRONIZADOS • PAGO CONTRAENTREGA"
android:textColor="@color/accent_green"
android:textSize="10sp" />
</LinearLayout>
<!-- Categories Horizontal Filter -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCategories"
@ -186,7 +279,7 @@
android:clipToPadding="false"
android:paddingHorizontal="10dp"
android:paddingTop="6dp"
android:paddingBottom="80dp"
android:paddingBottom="72dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:listitem="@layout/item_product" />
@ -205,7 +298,8 @@
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/ic_search" />
android:src="@drawable/ic_search"
app:tint="@color/drip_fuchsia" />
<TextView
android:layout_width="wrap_content"
@ -233,7 +327,7 @@
android:filterTouchesWhenObscured="true"
android:text="Ver todos los productos"
android:textAllCaps="false"
app:backgroundTint="@color/accent_green"
app:backgroundTint="@color/drip_fuchsia"
app:cornerRadius="12dp" />
</LinearLayout>
@ -243,28 +337,188 @@
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:indeterminateTint="@color/accent_green"
android:indeterminateTint="@color/drip_fuchsia"
android:visibility="gone" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- Floating Action Button for Custom Encargos / Requests -->
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fabCustomOrder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:filterTouchesWhenObscured="true"
android:text="Encargos"
android:textColor="#FFFFFF"
android:textSize="13sp"
android:textStyle="bold"
app:backgroundTint="@color/accent_green"
app:icon="@drawable/ic_encargos"
app:iconPadding="8dp"
app:iconTint="#FFFFFF" />
<!-- Mobile Bottom Navigation Dock (Matching Web MobileBottomDock) -->
<LinearLayout
android:id="@+id/bottomDock"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="bottom"
android:background="#FA0A0A0A"
android:elevation="12dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="8dp">
<!-- 1. Inicio (Catálogo) -->
<LinearLayout
android:id="@+id/navBtnHome"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/ivNavHome"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_home"
app:tint="@color/drip_fuchsia" />
<TextView
android:id="@+id/tvNavHome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:fontFamily="sans-serif-bold"
android:text="Inicio"
android:textColor="@color/drip_fuchsia"
android:textSize="10sp" />
</LinearLayout>
<!-- 2. Buscar -->
<LinearLayout
android:id="@+id/navBtnSearch"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/ivNavSearch"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_search"
app:tint="@color/text_secondary" />
<TextView
android:id="@+id/tvNavSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:fontFamily="sans-serif-medium"
android:text="Buscar"
android:textColor="@color/text_secondary"
android:textSize="10sp" />
</LinearLayout>
<!-- 3. Carrito con Contador Dinámico -->
<LinearLayout
android:id="@+id/navBtnCart"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivNavCart"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_shopping_bag"
app:tint="@color/text_secondary" />
<TextView
android:id="@+id/tvBottomCartBadge"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="top|end"
android:layout_marginTop="-4dp"
android:layout_marginEnd="-6dp"
android:background="@drawable/bg_badge_counter"
android:gravity="center"
android:text="0"
android:textColor="#FFFFFF"
android:textSize="9sp"
android:textStyle="bold"
android:visibility="gone" />
</FrameLayout>
<TextView
android:id="@+id/tvNavCart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:fontFamily="sans-serif-medium"
android:text="Carrito"
android:textColor="@color/text_secondary"
android:textSize="10sp" />
</LinearLayout>
<!-- 4. Encargos Especiales -->
<LinearLayout
android:id="@+id/navBtnEncargos"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/ivNavEncargos"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_encargos"
app:tint="@color/text_secondary" />
<TextView
android:id="@+id/tvNavEncargos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:fontFamily="sans-serif-medium"
android:text="Encargos"
android:textColor="@color/text_secondary"
android:textSize="10sp" />
</LinearLayout>
<!-- 5. WhatsApp Asesor -->
<LinearLayout
android:id="@+id/navBtnWhatsApp"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_whatsapp"
app:tint="@color/accent_whatsapp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:fontFamily="sans-serif-medium"
android:text="Asesor"
android:textColor="@color/text_secondary"
android:textSize="10sp" />
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dark">
<ImageView
android:id="@+id/ivSplash"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="DripZone Splash Screen"
android:scaleType="centerCrop"
android:src="@drawable/dripzone_splash_clean" />
<!-- Subtle bottom indicator -->
<ProgressBar
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="48dp"
android:indeterminateTint="@color/drip_fuchsia" />
</FrameLayout>

View File

@ -0,0 +1,302 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_bottom_sheet"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<View
android:layout_width="48dp"
android:layout_height="4dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:background="#40FFFFFF" />
<!-- Header -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/tvCartHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-black"
android:text="🛍️ Tu Carrito • DripZone"
android:textColor="@color/text_primary"
android:textSize="18sp" />
<TextView
android:id="@+id/tvCartCountSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="0 artículos seleccionados"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
<ImageButton
android:id="@+id/btnCloseCart"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Cerrar"
android:src="@drawable/ic_close"
app:tint="@color/text_secondary" />
</RelativeLayout>
<!-- Empty Cart View -->
<LinearLayout
android:id="@+id/layoutCartEmpty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingVertical="32dp"
android:visibility="gone">
<ImageView
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@drawable/ic_truck"
app:tint="@color/drip_fuchsia" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:fontFamily="sans-serif-bold"
android:text="Tu carrito está vacío"
android:textColor="@color/text_primary"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Agrega los sneakers o streetwear que más te gusten"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
<!-- Content when cart has items -->
<LinearLayout
android:id="@+id/layoutCartContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Items List -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCartItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_cart_item" />
<!-- City Destination Card (Clickable to change city) -->
<LinearLayout
android:id="@+id/cardChangeCity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/bg_btn_secondary"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_truck"
app:tint="@color/drip_fuchsia" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvCartCityName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-bold"
android:text="Destino: Cali"
android:textColor="@color/text_primary"
android:textSize="12sp" />
<TextView
android:id="@+id/tvCartCityInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Despacho en 24h • Toca para cambiar ciudad"
android:textColor="@color/text_secondary"
android:textSize="11sp" />
</LinearLayout>
<TextView
android:id="@+id/tvCartShippingFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-bold"
android:text="GRATIS"
android:textColor="@color/accent_emerald"
android:textSize="12sp" />
</LinearLayout>
<!-- Separation / Contraentrega Policy Banner -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/bg_badge_drip"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-bold"
android:text="⚡ Condición de Pago Contraentrega:"
android:textColor="@color/drip_purple_glow"
android:textSize="12sp" />
<TextView
android:id="@+id/tvSeparationBreakdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Separas tu pedido con $10.000 COP y cancelas el saldo restante en efectivo al recibir."
android:textColor="@color/badge_drip_text"
android:textSize="11sp" />
</LinearLayout>
<!-- Totals Breakdown -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Subtotal:"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvSubtotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:fontFamily="sans-serif-medium"
android:text="$ 0 COP"
android:textColor="@color/text_primary"
android:textSize="13sp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Envío a tu ciudad:"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvShippingTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:fontFamily="sans-serif-medium"
android:text="GRATIS"
android:textColor="@color/accent_emerald"
android:textSize="13sp" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="10dp"
android:background="@color/bg_card_stroke" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-black"
android:text="TOTAL A PAGAR:"
android:textColor="@color/text_primary"
android:textSize="15sp" />
<TextView
android:id="@+id/tvCartTotalAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:fontFamily="sans-serif-black"
android:text="$ 0 COP"
android:textColor="@color/drip_purple_glow"
android:textSize="18sp" />
</RelativeLayout>
</LinearLayout>
<!-- WhatsApp Checkout Button with Tapjacking Protection -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCheckoutWhatsApp"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginTop="18dp"
android:filterTouchesWhenObscured="true"
android:text="Pedir Carrito por WhatsApp (Contraentrega)"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="15sp"
android:textStyle="bold"
app:backgroundTint="@color/accent_whatsapp"
app:cornerRadius="16dp"
app:icon="@drawable/ic_whatsapp"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="22dp" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_bottom_sheet"
android:orientation="vertical"
android:padding="20dp">
<View
android:layout_width="48dp"
android:layout_height="4dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:background="#40FFFFFF" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-black"
android:text="📍 Selecciona tu Ciudad"
android:textColor="@color/text_primary"
android:textSize="18sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Tiempos oficiales y cobertura contraentrega"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
<ImageButton
android:id="@+id/btnCloseCity"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Cerrar"
android:src="@drawable/ic_close"
app:tint="@color/text_secondary" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCities"
android:layout_width="match_parent"
android:layout_height="320dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_city" />
</LinearLayout>

View File

@ -21,7 +21,7 @@
android:layout_marginBottom="16dp"
android:background="#40FFFFFF" />
<!-- Header with brand & close button -->
<!-- Header with brand badge & close button -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -31,12 +31,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@drawable/bg_chip_category"
android:background="@drawable/bg_badge_drip"
android:fontFamily="sans-serif-black"
android:paddingHorizontal="10dp"
android:paddingVertical="4dp"
android:text="NIKE AIR"
android:textColor="@color/accent_green"
android:textColor="@color/badge_drip_text"
android:textSize="11sp" />
<ImageButton
@ -46,7 +46,8 @@
android:layout_alignParentEnd="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Cerrar"
android:src="@drawable/ic_close" />
android:src="@drawable/ic_close"
app:tint="@color/text_secondary" />
</RelativeLayout>
<!-- Large Product Image -->
@ -75,11 +76,11 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-black"
android:text="Nike SB Dunk Low Pro 'Chicago'"
android:text="Nike SB Dunk Low Pro"
android:textColor="@color/text_primary"
android:textSize="18sp" />
<!-- Price -->
<!-- Price (Web Fuchsia/Purple Style) -->
<TextView
android:id="@+id/tvSheetPrice"
android:layout_width="match_parent"
@ -87,7 +88,7 @@
android:layout_marginTop="6dp"
android:fontFamily="sans-serif-black"
android:text="$ 280.000 COP"
android:textColor="@color/accent_green"
android:textColor="@color/drip_purple_glow"
android:textSize="22sp" />
<!-- Trust / Contraentrega Badge -->
@ -103,7 +104,8 @@
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_truck" />
android:src="@drawable/ic_truck"
app:tint="@color/accent_gold" />
<LinearLayout
android:layout_width="match_parent"
@ -123,7 +125,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Recibe tu pedido y paga en efectivo al recibir"
android:text="Recibe en tu puerta y paga en efectivo al recibir"
android:textColor="@color/text_secondary"
android:textSize="11sp" />
</LinearLayout>
@ -149,14 +151,33 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_size_chip" />
<!-- Add to Cart Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAddToCart"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="20dp"
android:filterTouchesWhenObscured="true"
android:text="Agregar al Carrito"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="15sp"
android:textStyle="bold"
app:backgroundTint="@color/drip_purple"
app:cornerRadius="16dp"
app:icon="@drawable/ic_encargos"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="20dp" />
<!-- WhatsApp Checkout Action Button with Tapjacking Protection -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnOrderWhatsApp"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginTop="22dp"
android:layout_marginTop="10dp"
android:filterTouchesWhenObscured="true"
android:text="Pedir por WhatsApp (Contraentrega)"
android:text="Pedir Directo por WhatsApp (Contraentrega)"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="15sp"
@ -178,10 +199,10 @@
android:filterTouchesWhenObscured="true"
android:text="¿No ves tu talla? Pedir por Encargo"
android:textAllCaps="false"
android:textColor="@color/accent_green"
android:textColor="@color/drip_purple_glow"
android:textSize="13sp"
app:cornerRadius="14dp"
app:strokeColor="@color/accent_green"
app:strokeColor="@color/drip_purple_light"
app:strokeWidth="1dp" />
</LinearLayout>

View File

@ -31,7 +31,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="¿Buscas un modelo exclusivo o una talla específica de nuestros proveedores? Nosotros te lo conseguimos."
android:text="¿Buscas un modelo exclusivo o una talla específica de nuestros proveedores? Te lo conseguimos bajo pedido."
android:textColor="@color/text_secondary"
android:textSize="13sp" />
@ -48,7 +48,7 @@
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusTopStart="12dp"
app:boxStrokeColor="@color/accent_green">
app:boxStrokeColor="@color/drip_fuchsia">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etCustomModel"
@ -71,7 +71,7 @@
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusTopStart="12dp"
app:boxStrokeColor="@color/accent_green">
app:boxStrokeColor="@color/drip_fuchsia">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etCustomSize"
@ -94,7 +94,7 @@
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusTopStart="12dp"
app:boxStrokeColor="@color/accent_green">
app:boxStrokeColor="@color/drip_fuchsia">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etCustomNotes"
@ -112,7 +112,7 @@
android:layout_height="52dp"
android:layout_marginTop="20dp"
android:filterTouchesWhenObscured="true"
android:text="Consultar con Asesor WhatsApp"
android:text="Consultar Encargo en WhatsApp"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="15sp"

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
app:cardBackgroundColor="@color/bg_card"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeColor="@color/bg_card_stroke"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="10dp">
<!-- Product Image -->
<ImageView
android:id="@+id/ivCartThumb"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="#121216"
android:contentDescription="Miniatura"
android:scaleType="centerCrop" />
<!-- Info Column -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvCartName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:maxLines="1"
android:text="Nike SB Dunk Low"
android:textColor="@color/text_primary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvCartSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Talla: 41"
android:textColor="@color/drip_purple_glow"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvCartPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="sans-serif-black"
android:text="$ 280.000 COP"
android:textColor="@color/text_primary"
android:textSize="13sp" />
</LinearLayout>
<!-- Stepper + Delete -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<ImageButton
android:id="@+id/btnRemoveCartItem"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Eliminar"
android:src="@drawable/ic_close"
app:tint="@color/text_muted" />
<!-- Quantity Stepper -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_marginTop="4dp"
android:background="@drawable/bg_btn_secondary"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="4dp">
<ImageButton
android:id="@+id/btnMinus"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Menos"
android:src="@drawable/ic_close"
app:tint="@color/text_secondary" />
<TextView
android:id="@+id/tvQuantity"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="1"
android:textColor="@color/text_primary"
android:textSize="12sp"
android:textStyle="bold" />
<ImageButton
android:id="@+id/btnPlus"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Más"
android:src="@drawable/ic_encargos"
app:tint="@color/drip_fuchsia" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cardCity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/bg_card"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeColor="@color/bg_card_stroke"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:id="@+id/ivCheck"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_shield_check"
app:tint="@color/drip_fuchsia" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvCityName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-black"
android:text="Cali"
android:textColor="@color/text_primary"
android:textSize="14sp" />
<TextView
android:id="@+id/tvCityDeliveryTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Despacho en 24h • Valle del Cauca"
android:textColor="@color/text_secondary"
android:textSize="11sp" />
</LinearLayout>
<!-- Shipping Cost Badge -->
<TextView
android:id="@+id/tvShippingCost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_badge_drip"
android:fontFamily="sans-serif-bold"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:text="Envío GRATIS"
android:textColor="@color/badge_drip_text"
android:textSize="11sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -32,19 +32,19 @@
android:contentDescription="Foto del producto"
android:scaleType="centerCrop" />
<!-- Brand Badge -->
<!-- Brand / Edition Badge (Web Purple Style) -->
<TextView
android:id="@+id/tvBrandBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_margin="8dp"
android:background="@drawable/bg_chip_category"
android:background="@drawable/bg_badge_drip"
android:fontFamily="sans-serif-black"
android:paddingHorizontal="8dp"
android:paddingVertical="3dp"
android:text="DRIP"
android:textColor="@color/accent_green"
android:textColor="@color/badge_drip_text"
android:textSize="10sp"
android:textStyle="bold" />
@ -55,7 +55,7 @@
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="8dp"
android:background="#99000000"
android:background="#B3000000"
android:paddingHorizontal="6dp"
android:paddingVertical="3dp"
android:text="CONTRAENTREGA"
@ -109,7 +109,7 @@
android:layout_weight="1"
android:fontFamily="sans-serif-black"
android:text="$ 280.000"
android:textColor="@color/accent_green"
android:textColor="@color/drip_purple_glow"
android:textSize="14sp" />
<!-- Quick Order Button with Tapjacking Protection -->

View File

@ -1,31 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#10B981</color>
<color name="colorPrimaryDark">#059669</color>
<color name="colorAccent">#10B981</color>
<!-- DripZone Web Brand Colors (from BRANDS.dripzone) -->
<color name="colorPrimary">#A855F7</color>
<color name="colorPrimaryDark">#7E22CE</color>
<color name="colorAccent">#D946EF</color>
<color name="bg_dark">#0B0B0E</color>
<color name="bg_card">#16161A</color>
<color name="bg_card_elevated">#22222A</color>
<color name="bg_card_stroke">#2D2D38</color>
<color name="bg_input">#1A1A22</color>
<!-- Web Neutrals and Backgrounds -->
<color name="bg_dark">#0A0A0A</color>
<color name="bg_card">#171717</color>
<color name="bg_card_elevated">#212126</color>
<color name="bg_card_stroke">#282830</color>
<color name="bg_card_stroke_active">#A855F7</color>
<color name="bg_input">#1A1A20</color>
<color name="accent_green">#10B981</color>
<color name="accent_green_glow">#34D399</color>
<!-- Web DripZone Purple & Fuchsia Accents -->
<color name="drip_purple">#9333EA</color>
<color name="drip_purple_light">#A855F7</color>
<color name="drip_fuchsia">#D946EF</color>
<color name="drip_pink">#EC4899</color>
<color name="drip_purple_glow">#C084FC</color>
<!-- WhatsApp & Utility Colors -->
<color name="accent_whatsapp">#25D366</color>
<color name="accent_whatsapp_pressed">#1EBE5D</color>
<color name="accent_orange">#F97316</color>
<color name="accent_gold">#F59E0B</color>
<color name="accent_emerald">#10B981</color>
<color name="text_primary">#FFFFFF</color>
<color name="text_secondary">#9CA3AF</color>
<color name="text_muted">#6B7280</color>
<color name="divider">#27272A</color>
<!-- Text Typography -->
<color name="text_primary">#EDEDED</color>
<color name="text_secondary">#A1A1AA</color>
<color name="text_muted">#71717A</color>
<color name="divider">#262626</color>
<color name="badge_exclusive_bg">#2E1065</color>
<color name="badge_exclusive_text">#C084FC</color>
<!-- Badges matching web config -->
<color name="badge_drip_bg">#2E1065</color>
<color name="badge_drip_border">#581C87</color>
<color name="badge_drip_text">#D8B4FE</color>
<color name="badge_stock_bg">#064E3B</color>
<color name="badge_stock_text">#34D399</color>
<color name="badge_pedido_bg">#1E3A8A</color>
<color name="badge_pedido_text">#60A5FA</color>
<color name="badge_pedido_text">#93C5FD</color>
</resources>

View File

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base DripZone Native Theme -->
<!-- Base DripZone Native Theme matching Web Style -->
<style name="Theme.DripZone" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/accent_green</item>
<item name="colorPrimary">@color/drip_purple</item>
<item name="colorPrimaryVariant">@color/colorPrimaryDark</item>
<item name="colorOnPrimary">@color/text_primary</item>
<item name="colorSecondary">@color/accent_whatsapp</item>
<item name="colorSecondaryVariant">@color/accent_whatsapp_pressed</item>
<item name="colorSecondary">@color/drip_fuchsia</item>
<item name="colorSecondaryVariant">@color/drip_pink</item>
<item name="colorOnSecondary">@color/text_primary</item>
<item name="android:statusBarColor">@color/bg_dark</item>
@ -17,7 +17,7 @@
<item name="bottomSheetDialogTheme">@style/DripBottomSheetDialogTheme</item>
</style>
<!-- Bottom Sheet Dialog with Dark Rounded Corners -->
<!-- Bottom Sheet Dialog with Web Dark Styling -->
<style name="DripBottomSheetDialogTheme" parent="Theme.Design.Light.BottomSheetDialog">
<item name="bottomSheetStyle">@style/DripBottomSheetStyle</item>
<item name="android:windowIsFloating">false</item>
@ -29,9 +29,12 @@
</style>
<!-- Splash Theme -->
<style name="AppTheme.NoActionBarLaunch" parent="Theme.DripZone">
<item name="android:windowBackground">@color/bg_dark</item>
<style name="Theme.DripZone.Splash" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowBackground">@drawable/dripzone_splash_clean</item>
<item name="android:statusBarColor">@color/bg_dark</item>
<item name="android:navigationBarColor">@color/bg_dark</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.DripZone.Splash" />
<style name="AppTheme" parent="Theme.DripZone" />
</resources>

View File

@ -64,30 +64,28 @@ def send_apk_whatsapp():
print("[3/3] Enviando APK y notificación oficial al WhatsApp de Carlos (+57 316 745 5877)...")
message_text = (
"📲 *DripZone - App Móvil Oficial en Kotlin Nativo*\n\n"
"🔥 *¡Desarrollo Nativo en Kotlin Completado y Desplegado!*\n\n"
"✨ *Características y Novedades de la App Nativa:*\n\n"
"⚡ *1. Código 100% Nativo en Kotlin (Android):*\n"
"• Reemplazo total de vistas genéricas por arquitectura nativa Jetpack con ViewBinding y Coroutines.\n"
"• Interfaz ultra fluida, rápida y con estética dark streetwear moderna para DripZone.\n"
"• Grid de productos a 2 columnas con tarjetas estilizadas, insignias de marca y precios en COP.\n"
"• Filtros por categorías en chips horizontales (Todos, Sneakers, Chanclas, Bolsos, Gorras, Bajo Pedido).\n"
"• Búsqueda instantánea en tiempo real integrada en el encabezado.\n"
"• SwipeRefreshLayout (Pull-to-refresh) para actualizar en vivo con los nuevos drops de WhatsApp.\n\n"
"🛡️ *2. Estándares de Seguridad Actuales Cumplidos:*\n"
"• *Network Security Config:* Tráfico en texto claro (HTTP) bloqueado globalmente, enforcing estricto de HTTPS/TLS 1.2 y TLS 1.3 con certificados de sistema.\n"
"• *Protección de Datos:* `allowBackup=\"false\"` y `dataExtractionRules` configurados para evitar extracción no autorizada vía adb o backups en la nube.\n"
"• *Cifrado Local:* Almacenamiento seguro mediante `EncryptedSharedPreferences` con Android KeyStore y MasterKey AES-256 GCM.\n"
"• *Protección Anti-Tapjacking:* `filterTouchesWhenObscured=\"true\"` en botones de compra y pedidos.\n"
"• *Lanzamiento Seguro de Intents:* Validación rigurosa de URIs de WhatsApp y esquemas para prevenir Spoofing o Redirecciones maliciosas.\n"
"• *Reglas Proguard/R8:* Ofuscación y protección de modelos JSON y dependencias.\n\n"
"👟 *3. Experiencia de Compra y Encargos:* \n"
"• Modal Bottom Sheet con vista ampliada, selección interactiva de tallas (chips) y detalles de despacho.\n"
"• Botón directo *'Pedir por WhatsApp (Contraentrega)'* que pre-arma el mensaje exacto con foto, talla y precio al WhatsApp de Carlos.\n"
"• Botón flotante y modal para *'Encargos Especiales / Pedidos'* de modelos no listados con proveedores mayoristas.\n\n"
"📲 *DripZone - App Nativa Android v1.2 (100% Funcionalidad Web)*\n\n"
"🔥 *¡Actualización Desplegada! Toda la funcionalidad y estética de la Web ahora en la App Nativa:*\n\n"
"🎨 *1. Estilo Visual, Splash Screen y Paleta Web Idénticos:*\n"
"• *Splash Screen:* Integrado con el recurso oficial solicitado `dripzone_splash_clean_1787718656035` con transición fluida.\n"
"• *Paleta Dark Streetwear DripZone:* Fondo `#0A0A0A`, detalles en púrpura `#9333EA` / `#A855F7`, fucsia neón `#D946EF`, rosa `#EC4899`, badges violeta `#2E1065` y glow borders.\n"
"• *Banner Web Superior:* Degradado en vivo 'ENVÍOS CONTRAENTREGA EN TODO COLOMBIA'.\n\n"
"⚡ *2. Misma Funcionalidad que la Versión Web:*\n"
"• *Selector de Ciudad:* Modal interactivo con ciudades colombianas (Cali, Bogotá, Medellín, Barranquilla, Bucaramanga, etc.), tiempos de entrega y garantía de 1er cambio gratis.\n"
"• *Carrito de Compras Multi-Producto:* Drawer interactivo (`CartBottomSheet`), sumatoria automática, desglose de separación ('Separa con $10.000 COP y paga saldo al recibir') y checkout multi-item a WhatsApp.\n"
"• *Detalle de Producto:* Selector interactivo de tallas en chips, botón 'Agregar al Carrito' y 'Comprar Directo por WhatsApp' con foto y formato de orden contraentrega.\n"
"• *Dock de Navegación Móvil (5 Pestañas):* Inicio, Buscar, Carrito con badge dinámico en tiempo real, Encargos y Asesor WhatsApp.\n"
"• *Módulo de Encargos / Pedidos Especiales:* Formulario para cotizar modelos no listados directamente con proveedores mayoristas.\n"
"• *Filtros y Búsqueda en Vivo:* Categorías dinámicas (Sneakers, Chanclas, Bolsos, Gorras) y filtrado instantáneo por texto.\n"
"• *Pull-to-Refresh:* Sincronización instantánea con los drops de WhatsApp.\n\n"
"🛡️ *3. Seguridad de Nivel Corporativo:*\n"
"• *Network Security Config:* Tráfico en texto claro (HTTP) bloqueado, TLS 1.2+ estricto.\n"
"• *Cifrado de Datos:* `EncryptedSharedPreferences` respaldado por Android KeyStore (AES-256 GCM).\n"
"• *Protección Anti-Tapjacking:* `filterTouchesWhenObscured=\"true\"` en botones clave.\n"
"• *Lanzamiento Seguro de Intents:* Validación rigurosa de URLs y esquemas de WhatsApp.\n\n"
f"⬇️ *Descarga directa del APK:* {APK_URL}\n"
f"⬇️ *Enlace alternativo:* https://dripzone.almeira.com.co/DripZone.apk\n\n"
"Adjunto el archivo APK instalador para que lo pruebes directamente en tu dispositivo."
"Adjunto el instalador APK para su instalación y prueba inmediata."
)
req_text = urllib.request.Request(