diff --git a/dripzone-app/android/app/src/main/AndroidManifest.xml b/dripzone-app/android/app/src/main/AndroidManifest.xml
index 43b150e..25774f4 100644
--- a/dripzone-app/android/app/src/main/AndroidManifest.xml
+++ b/dripzone-app/android/app/src/main/AndroidManifest.xml
@@ -28,6 +28,20 @@
android:theme="@style/Theme.DripZone"
tools:targetApi="31">
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+ android:exported="false" />
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/MainActivity.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/MainActivity.kt
index 30471d9..69301c4 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/MainActivity.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/MainActivity.kt
@@ -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 = emptyList()
+ private var availableCities: List = 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)
}
}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/SplashActivity.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/SplashActivity.kt
new file mode 100644
index 0000000..60b0e4a
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/SplashActivity.kt
@@ -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)
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/api/ApiClient.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/api/ApiClient.kt
index ff88b93..9f29d4a 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/api/ApiClient.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/api/ApiClient.kt
@@ -162,4 +162,32 @@ object ApiClient {
)
)
}
+
+ suspend fun getCities(context: Context): List =
+ 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>() {}.type
+ val list: List? = 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()
+ }
}
+
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/model/CartItem.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/CartItem.kt
new file mode 100644
index 0000000..0caee85
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/CartItem.kt
@@ -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"
+ }
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/model/CartManager.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/CartManager.kt
new file mode 100644
index 0000000..071e200
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/CartManager.kt
@@ -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()
+ 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>() {}.type
+ val loaded: List? = 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 = 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() }
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/model/City.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/City.kt
new file mode 100644
index 0000000..923d153
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/City.kt
@@ -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 {
+ 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
+ )
+ )
+ }
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CartAdapter.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CartAdapter.kt
new file mode 100644
index 0000000..521d769
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CartAdapter.kt
@@ -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,
+ private val onQuantityChanged: (CartItem, Int) -> Unit,
+ private val onItemRemoved: (CartItem) -> Unit
+) : RecyclerView.Adapter() {
+
+ 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) {
+ items.clear()
+ items.addAll(newItems)
+ notifyDataSetChanged()
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CartBottomSheet.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CartBottomSheet.kt
new file mode 100644
index 0000000..3326625
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CartBottomSheet.kt
@@ -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
+ }
+ }
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CityAdapter.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CityAdapter.kt
new file mode 100644
index 0000000..6374a2d
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CityAdapter.kt
@@ -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,
+ private var selectedCityId: String,
+ private val onCitySelected: (City) -> Unit
+) : RecyclerView.Adapter() {
+
+ 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
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CitySelectorBottomSheet.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CitySelectorBottomSheet.kt
new file mode 100644
index 0000000..28d5777
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CitySelectorBottomSheet.kt
@@ -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 = 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,
+ selectedCity: City,
+ onSelected: (City) -> Unit
+ ): CitySelectorBottomSheet {
+ return CitySelectorBottomSheet().apply {
+ this.cities = cities
+ this.selectedCity = selectedCity
+ this.onCitySelectedListener = onSelected
+ }
+ }
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
index d8b6f4d..73e74df 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
@@ -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"
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-hdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-land-hdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..ffa1d55
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-land-hdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-hdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-land-hdpi/splash.png
index 3b78ce2..ffa1d55 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-land-hdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-land-hdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-mdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-land-mdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..985ddff
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-land-mdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-mdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-land-mdpi/splash.png
index 878f728..985ddff 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-land-mdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-land-mdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-xhdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-land-xhdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..3b8a581
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-land-xhdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-xhdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-land-xhdpi/splash.png
index b8f66cf..3b8a581 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-land-xhdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-land-xhdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-xxhdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-land-xxhdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..a7d399b
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-land-xxhdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png
index edb90e9..a7d399b 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-xxxhdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-land-xxxhdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..f24903a
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-land-xxxhdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png
index 6848ef6..f24903a 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-hdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-port-hdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..93e455f
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-port-hdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-hdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-port-hdpi/splash.png
index ec658a5..93e455f 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-port-hdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-port-hdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-mdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-port-mdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..b5c713e
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-port-mdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-mdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-port-mdpi/splash.png
index 3cbbce5..b5c713e 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-port-mdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-port-mdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-xhdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-port-xhdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..de29117
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-port-xhdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-xhdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-port-xhdpi/splash.png
index e2f34aa..de29117 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-port-xhdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-port-xhdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-xxhdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-port-xxhdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..1bb2c9f
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-port-xxhdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png
index 817dfae..1bb2c9f 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-xxxhdpi/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable-port-xxxhdpi/dripzone_splash_clean.png
new file mode 100644
index 0000000..0b85dad
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable-port-xxxhdpi/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/dripzone-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png
index 0e408fd..0b85dad 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png and b/dripzone-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable/bg_badge_counter.xml b/dripzone-app/android/app/src/main/res/drawable/bg_badge_counter.xml
new file mode 100644
index 0000000..7ebe972
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/bg_badge_counter.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/bg_badge_drip.xml b/dripzone-app/android/app/src/main/res/drawable/bg_badge_drip.xml
new file mode 100644
index 0000000..73c2651
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/bg_badge_drip.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/bg_banner_gradient.xml b/dripzone-app/android/app/src/main/res/drawable/bg_banner_gradient.xml
new file mode 100644
index 0000000..cfa4220
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/bg_banner_gradient.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/bg_btn_drip.xml b/dripzone-app/android/app/src/main/res/drawable/bg_btn_drip.xml
new file mode 100644
index 0000000..9ef303c
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/bg_btn_drip.xml
@@ -0,0 +1,14 @@
+
+
+ -
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/bg_chip_category.xml b/dripzone-app/android/app/src/main/res/drawable/bg_chip_category.xml
index b3ed10f..3fcf711 100644
--- a/dripzone-app/android/app/src/main/res/drawable/bg_chip_category.xml
+++ b/dripzone-app/android/app/src/main/res/drawable/bg_chip_category.xml
@@ -2,7 +2,11 @@
-
-
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/bg_chip_size.xml b/dripzone-app/android/app/src/main/res/drawable/bg_chip_size.xml
index 7971f32..51101ed 100644
--- a/dripzone-app/android/app/src/main/res/drawable/bg_chip_size.xml
+++ b/dripzone-app/android/app/src/main/res/drawable/bg_chip_size.xml
@@ -2,7 +2,11 @@
-
-
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/dripzone_splash_clean.png b/dripzone-app/android/app/src/main/res/drawable/dripzone_splash_clean.png
new file mode 100644
index 0000000..b693e55
Binary files /dev/null and b/dripzone-app/android/app/src/main/res/drawable/dripzone_splash_clean.png differ
diff --git a/dripzone-app/android/app/src/main/res/drawable/ic_home.xml b/dripzone-app/android/app/src/main/res/drawable/ic_home.xml
new file mode 100644
index 0000000..4914da3
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/ic_pin.xml b/dripzone-app/android/app/src/main/res/drawable/ic_pin.xml
new file mode 100644
index 0000000..a00e5af
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/ic_pin.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/ic_shopping_bag.xml b/dripzone-app/android/app/src/main/res/drawable/ic_shopping_bag.xml
new file mode 100644
index 0000000..34ae3c7
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/ic_shopping_bag.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/splash.png b/dripzone-app/android/app/src/main/res/drawable/splash.png
index ec658a5..b693e55 100644
Binary files a/dripzone-app/android/app/src/main/res/drawable/splash.png and b/dripzone-app/android/app/src/main/res/drawable/splash.png differ
diff --git a/dripzone-app/android/app/src/main/res/layout/activity_main.xml b/dripzone-app/android/app/src/main/res/layout/activity_main.xml
index 3cfe194..933a596 100644
--- a/dripzone-app/android/app/src/main/res/layout/activity_main.xml
+++ b/dripzone-app/android/app/src/main/res/layout/activity_main.xml
@@ -14,39 +14,82 @@
android:background="@color/bg_dark"
app:elevation="0dp">
-
+
+
+
+
+
+
+
+
+
+ android:paddingTop="10dp"
+ android:paddingBottom="6dp">
-
+
-
+ android:orientation="horizontal">
+
+
+
+
+
@@ -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" />
-
+
+
+
+
+
+
+
+
+
+ app:iconPadding="4dp"
+ app:iconSize="14dp" />
-
+
+
+
+
+
+
+
+
+
+
+
+ android:paddingHorizontal="12dp"
+ android:visibility="gone">
+ android:src="@drawable/ic_search"
+ app:tint="@color/drip_purple_light" />
-
-
-
-
-
-
-
-
@@ -205,7 +298,8 @@
+ android:src="@drawable/ic_search"
+ app:tint="@color/drip_fuchsia" />
@@ -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" />
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/activity_splash.xml b/dripzone-app/android/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..690f492
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_cart.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_cart.xml
new file mode 100644
index 0000000..5193e3a
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_cart.xml
@@ -0,0 +1,302 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_city_selector.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_city_selector.xml
new file mode 100644
index 0000000..eb3a71e
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_city_selector.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
index 4675970..7099bfc 100644
--- a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
@@ -21,7 +21,7 @@
android:layout_marginBottom="16dp"
android:background="#40FFFFFF" />
-
+
@@ -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" />
+ android:src="@drawable/ic_close"
+ app:tint="@color/text_secondary" />
@@ -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" />
-
+
@@ -103,7 +104,8 @@
+ android:src="@drawable/ic_truck"
+ app:tint="@color/accent_gold" />
@@ -149,14 +151,33 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_size_chip" />
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/dialog_custom_order.xml b/dripzone-app/android/app/src/main/res/layout/dialog_custom_order.xml
index 6066dac..cfa003a 100644
--- a/dripzone-app/android/app/src/main/res/layout/dialog_custom_order.xml
+++ b/dripzone-app/android/app/src/main/res/layout/dialog_custom_order.xml
@@ -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">
+ app:boxStrokeColor="@color/drip_fuchsia">
+ app:boxStrokeColor="@color/drip_fuchsia">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/item_city.xml b/dripzone-app/android/app/src/main/res/layout/item_city.xml
new file mode 100644
index 0000000..a422d77
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/layout/item_city.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/item_product.xml b/dripzone-app/android/app/src/main/res/layout/item_product.xml
index b00a1f0..9c6baab 100644
--- a/dripzone-app/android/app/src/main/res/layout/item_product.xml
+++ b/dripzone-app/android/app/src/main/res/layout/item_product.xml
@@ -32,19 +32,19 @@
android:contentDescription="Foto del producto"
android:scaleType="centerCrop" />
-
+
@@ -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" />
diff --git a/dripzone-app/android/app/src/main/res/values/colors.xml b/dripzone-app/android/app/src/main/res/values/colors.xml
index c82c3bc..ea9e031 100644
--- a/dripzone-app/android/app/src/main/res/values/colors.xml
+++ b/dripzone-app/android/app/src/main/res/values/colors.xml
@@ -1,31 +1,43 @@
- #10B981
- #059669
- #10B981
+
+ #A855F7
+ #7E22CE
+ #D946EF
- #0B0B0E
- #16161A
- #22222A
- #2D2D38
- #1A1A22
+
+ #0A0A0A
+ #171717
+ #212126
+ #282830
+ #A855F7
+ #1A1A20
- #10B981
- #34D399
+
+ #9333EA
+ #A855F7
+ #D946EF
+ #EC4899
+ #C084FC
+
+
#25D366
#1EBE5D
- #F97316
#F59E0B
+ #10B981
- #FFFFFF
- #9CA3AF
- #6B7280
- #27272A
+
+ #EDEDED
+ #A1A1AA
+ #71717A
+ #262626
- #2E1065
- #C084FC
+
+ #2E1065
+ #581C87
+ #D8B4FE
#064E3B
#34D399
#1E3A8A
- #60A5FA
+ #93C5FD
diff --git a/dripzone-app/android/app/src/main/res/values/styles.xml b/dripzone-app/android/app/src/main/res/values/styles.xml
index 163ae4c..48176e4 100644
--- a/dripzone-app/android/app/src/main/res/values/styles.xml
+++ b/dripzone-app/android/app/src/main/res/values/styles.xml
@@ -1,13 +1,13 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/scripts/upload_apk_and_send.py b/scripts/upload_apk_and_send.py
index ed15738..fec1c7d 100644
--- a/scripts/upload_apk_and_send.py
+++ b/scripts/upload_apk_and_send.py
@@ -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(