feat: user registration with WhatsApp OTP verification, platform order registration with auto WhatsApp alerts, and splash screen overflow fix
This commit is contained in:
parent
fef85c3ff4
commit
b7f4d600fc
@ -35,7 +35,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var allProducts: List<Product> = emptyList()
|
||||
private var availableCities: List<City> = City.defaultCities()
|
||||
private var selectedCity: City = City.defaultCities().first()
|
||||
var selectedCity: City = City.defaultCities().first()
|
||||
private var selectedCategory: String = "Todos"
|
||||
private var searchQuery: String = ""
|
||||
|
||||
|
||||
@ -10,8 +10,10 @@ import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.TlsVersion
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -189,5 +191,160 @@ object ApiClient {
|
||||
}
|
||||
co.dripzone.app.model.City.defaultCities()
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía código OTP de 6 dígitos al WhatsApp del cliente
|
||||
*/
|
||||
suspend fun sendOtp(phone: String, name: String): Result<String> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jsonPayload = gson.toJson(
|
||||
mapOf(
|
||||
"identifier" to phone,
|
||||
"name" to name
|
||||
)
|
||||
)
|
||||
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://dripzone.almeira.com.co/api/auth/send-otp")
|
||||
.post(requestBody)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||
|
||||
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||
val message = json.get("message")?.asString ?: "Código OTP enviado por WhatsApp"
|
||||
Result.success(message)
|
||||
} else {
|
||||
val errorMsg = json.get("error")?.asString ?: "Error al enviar código OTP"
|
||||
Result.failure(Exception(errorMsg))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error sending OTP: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida el código OTP de WhatsApp y registra/actualiza el usuario en la plataforma
|
||||
*/
|
||||
suspend fun verifyOtp(
|
||||
phone: String,
|
||||
code: String,
|
||||
name: String,
|
||||
city: String,
|
||||
address: String
|
||||
): Result<Boolean> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jsonPayload = gson.toJson(
|
||||
mapOf(
|
||||
"identifier" to phone,
|
||||
"code" to code,
|
||||
"name" to name,
|
||||
"city" to city,
|
||||
"address" to address
|
||||
)
|
||||
)
|
||||
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://dripzone.almeira.com.co/api/auth/verify-otp")
|
||||
.post(requestBody)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||
|
||||
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||
Result.success(true)
|
||||
} else {
|
||||
val errorMsg = json.get("error")?.asString ?: "Código inválido o expirado"
|
||||
Result.failure(Exception(errorMsg))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error verifying OTP: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra un pedido en la plataforma (Appwrite/DataService) y dispara notificación por WhatsApp a Carlos
|
||||
*/
|
||||
suspend fun submitOrder(orderData: Map<String, Any?>): Result<String> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jsonPayload = gson.toJson(orderData)
|
||||
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://dripzone.almeira.com.co/api/orders")
|
||||
.post(requestBody)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||
|
||||
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||
val orderObj = json.getAsJsonObject("order")
|
||||
val orderId = orderObj?.get("id")?.asString ?: "ORD-${System.currentTimeMillis()}"
|
||||
Result.success(orderId)
|
||||
} else {
|
||||
val errorMsg = json.get("error")?.asString ?: "Error al registrar pedido en la plataforma"
|
||||
Result.failure(Exception(errorMsg))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error submitting order: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra una solicitud de encargo especial en la plataforma y dispara notificación a Carlos
|
||||
*/
|
||||
suspend fun submitCustomRequest(requestData: Map<String, Any?>): Result<String> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jsonPayload = gson.toJson(requestData)
|
||||
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://dripzone.almeira.com.co/api/custom-requests")
|
||||
.post(requestBody)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||
|
||||
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||
val msg = json.get("message")?.asString ?: "¡Solicitud de encargo recibida con éxito!"
|
||||
Result.success(msg)
|
||||
} else {
|
||||
val errorMsg = json.get("error")?.asString ?: "Error al registrar encargo"
|
||||
Result.failure(Exception(errorMsg))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error submitting custom request: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
package co.dripzone.app.model
|
||||
|
||||
import android.content.Context
|
||||
import co.dripzone.app.security.SecurityManager
|
||||
|
||||
data class UserProfile(
|
||||
val name: String = "",
|
||||
val phone: String = "",
|
||||
val city: String = "Cali",
|
||||
val address: String = "",
|
||||
val isPhoneVerified: Boolean = false
|
||||
)
|
||||
|
||||
object UserManager {
|
||||
|
||||
private const val KEY_USER_NAME = "user_profile_name"
|
||||
private const val KEY_USER_PHONE = "user_profile_phone"
|
||||
private const val KEY_USER_CITY = "user_profile_city"
|
||||
private const val KEY_USER_ADDRESS = "user_profile_address"
|
||||
private const val KEY_PHONE_VERIFIED = "user_phone_verified"
|
||||
|
||||
fun getUser(context: Context): UserProfile {
|
||||
val prefs = SecurityManager.getSecurePreferences(context)
|
||||
return UserProfile(
|
||||
name = prefs.getString(KEY_USER_NAME, "") ?: "",
|
||||
phone = prefs.getString(KEY_USER_PHONE, "") ?: "",
|
||||
city = prefs.getString(KEY_USER_CITY, "Cali") ?: "Cali",
|
||||
address = prefs.getString(KEY_USER_ADDRESS, "") ?: "",
|
||||
isPhoneVerified = prefs.getBoolean(KEY_PHONE_VERIFIED, false)
|
||||
)
|
||||
}
|
||||
|
||||
fun saveUser(
|
||||
context: Context,
|
||||
name: String,
|
||||
phone: String,
|
||||
city: String,
|
||||
address: String,
|
||||
isVerified: Boolean
|
||||
) {
|
||||
val prefs = SecurityManager.getSecurePreferences(context)
|
||||
prefs.edit()
|
||||
.putString(KEY_USER_NAME, name.trim())
|
||||
.putString(KEY_USER_PHONE, phone.trim())
|
||||
.putString(KEY_USER_CITY, city.trim())
|
||||
.putString(KEY_USER_ADDRESS, address.trim())
|
||||
.putBoolean(KEY_PHONE_VERIFIED, isVerified)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun setPhoneVerified(context: Context, isVerified: Boolean) {
|
||||
val prefs = SecurityManager.getSecurePreferences(context)
|
||||
prefs.edit().putBoolean(KEY_PHONE_VERIFIED, isVerified).apply()
|
||||
}
|
||||
}
|
||||
@ -60,34 +60,13 @@ class CartBottomSheet : BottomSheetDialogFragment() {
|
||||
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
|
||||
val verificationSheet = OrderVerificationBottomSheet.newInstance(
|
||||
city = selectedCity,
|
||||
isDirect = false
|
||||
)
|
||||
parentFragmentManager.let {
|
||||
verificationSheet.show(it, OrderVerificationBottomSheet.TAG)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,13 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import co.dripzone.app.api.ApiClient
|
||||
import co.dripzone.app.databinding.DialogCustomOrderBinding
|
||||
import co.dripzone.app.model.UserManager
|
||||
import co.dripzone.app.security.SecurityManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
@ -17,6 +21,13 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
||||
private var initialModel: String = ""
|
||||
private var initialBrand: String = ""
|
||||
|
||||
private var customerName: String = ""
|
||||
private var customerPhone: String = ""
|
||||
private var customerCity: String = ""
|
||||
private var modelDesired: String = ""
|
||||
private var sizeDesired: String = ""
|
||||
private var notes: String = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
@ -37,36 +48,34 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.btnCloseCustomModal.setOnClickListener { dismiss() }
|
||||
|
||||
// Pre-fill user data if registered locally
|
||||
val user = UserManager.getUser(requireContext())
|
||||
if (user.name.isNotBlank()) binding.etCustomCustomerName.setText(user.name)
|
||||
if (user.phone.isNotBlank()) binding.etCustomCustomerPhone.setText(user.phone)
|
||||
if (user.city.isNotBlank()) binding.etCustomCustomerCity.setText(user.city)
|
||||
|
||||
if (initialModel.isNotBlank()) {
|
||||
binding.etCustomModel.setText(initialModel)
|
||||
}
|
||||
|
||||
binding.btnSubmitCustomOrder.setOnClickListener {
|
||||
val rawModel = binding.etCustomModel.text?.toString()
|
||||
val rawSize = binding.etCustomSize.text?.toString()
|
||||
val rawNotes = binding.etCustomNotes.text?.toString()
|
||||
handleContinueToCustomOtp()
|
||||
}
|
||||
|
||||
val sanitizedModel = SecurityManager.sanitizeInput(rawModel)
|
||||
val sanitizedSize = SecurityManager.sanitizeInput(rawSize)
|
||||
val sanitizedNotes = SecurityManager.sanitizeInput(rawNotes)
|
||||
binding.btnVerifyCustomOtp.setOnClickListener {
|
||||
handleVerifyCustomOtp()
|
||||
}
|
||||
|
||||
if (sanitizedModel.isBlank()) {
|
||||
binding.etCustomModel.error = "Por favor indica el modelo deseado"
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.tvResendCustomOtp.setOnClickListener {
|
||||
sendCustomOtp()
|
||||
}
|
||||
|
||||
val message = buildString {
|
||||
append("¡Hola DripZone! 🚀 Quiero consultar por un *Pedido Especial / Encargo*:\n\n")
|
||||
append("👟 *Modelo deseado:* $sanitizedModel\n")
|
||||
if (sanitizedSize.isNotBlank()) {
|
||||
append("📏 *Talla estimada:* $sanitizedSize\n")
|
||||
}
|
||||
if (sanitizedNotes.isNotBlank()) {
|
||||
append("📝 *Detalles / Colorway:* $sanitizedNotes\n")
|
||||
}
|
||||
append("\n¿Tienen disponibilidad o pueden cotizarlo con sus proveedores mayoristas?")
|
||||
}
|
||||
binding.btnFinishCustom.setOnClickListener { dismiss() }
|
||||
|
||||
binding.btnCustomOpenWhatsApp.setOnClickListener {
|
||||
val message = "¡Hola Carlos! Acabo de registrar un encargo en la app de DripZone para las *$modelDesired* en talla *$sizeDesired*. ¿Me confirmas cotización con los proveedores mayoristas?"
|
||||
SecurityManager.openWhatsApp(
|
||||
requireContext(),
|
||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||
@ -76,6 +85,141 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleContinueToCustomOtp() {
|
||||
val rawName = binding.etCustomCustomerName.text?.toString()?.trim() ?: ""
|
||||
val rawPhone = binding.etCustomCustomerPhone.text?.toString()?.replace("\\D".toRegex(), "") ?: ""
|
||||
val rawCity = binding.etCustomCustomerCity.text?.toString()?.trim() ?: ""
|
||||
val rawModel = binding.etCustomModel.text?.toString()?.trim() ?: ""
|
||||
val rawSize = binding.etCustomSize.text?.toString()?.trim() ?: ""
|
||||
val rawNotes = binding.etCustomNotes.text?.toString()?.trim() ?: ""
|
||||
|
||||
if (rawName.isBlank()) {
|
||||
binding.etCustomCustomerName.error = "Ingresa tu nombre completo"
|
||||
return
|
||||
}
|
||||
if (rawPhone.length < 10) {
|
||||
binding.etCustomCustomerPhone.error = "Ingresa tu WhatsApp a 10 dígitos"
|
||||
return
|
||||
}
|
||||
if (rawModel.isBlank()) {
|
||||
binding.etCustomModel.error = "Indica el modelo o referencia"
|
||||
return
|
||||
}
|
||||
if (rawSize.isBlank()) {
|
||||
binding.etCustomSize.error = "Indica la talla deseada"
|
||||
return
|
||||
}
|
||||
|
||||
customerName = rawName
|
||||
customerPhone = rawPhone
|
||||
customerCity = if (rawCity.isNotBlank()) rawCity else "Colombia"
|
||||
modelDesired = rawModel
|
||||
sizeDesired = rawSize
|
||||
notes = rawNotes
|
||||
|
||||
val user = UserManager.getUser(requireContext())
|
||||
if (user.isPhoneVerified && user.phone == customerPhone) {
|
||||
submitFinalCustomRequest()
|
||||
} else {
|
||||
sendCustomOtp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendCustomOtp() {
|
||||
binding.layoutCustomForm.visibility = View.GONE
|
||||
binding.layoutCustomOtp.visibility = View.VISIBLE
|
||||
binding.pbCustomLoading.visibility = View.VISIBLE
|
||||
binding.btnVerifyCustomOtp.isEnabled = false
|
||||
|
||||
val masked = if (customerPhone.length >= 10) {
|
||||
"${customerPhone.take(3)}***${customerPhone.takeLast(4)}"
|
||||
} else customerPhone
|
||||
|
||||
binding.tvCustomOtpInstruction.text = "Enviamos un código de seguridad de 6 dígitos a tu WhatsApp (+57 $masked) para validar tu cuenta y registrar tu encargo:"
|
||||
|
||||
lifecycleScope.launch {
|
||||
val result = ApiClient.sendOtp(customerPhone, customerName)
|
||||
binding.pbCustomLoading.visibility = View.GONE
|
||||
binding.btnVerifyCustomOtp.isEnabled = true
|
||||
|
||||
result.onSuccess {
|
||||
Toast.makeText(requireContext(), "Código enviado a tu WhatsApp", Toast.LENGTH_SHORT).show()
|
||||
}.onFailure { err ->
|
||||
Toast.makeText(requireContext(), "Aviso: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerifyCustomOtp() {
|
||||
val code = binding.etCustomOtpCode.text?.toString()?.trim() ?: ""
|
||||
if (code.length < 6) {
|
||||
binding.etCustomOtpCode.error = "Ingresa el código de 6 dígitos"
|
||||
return
|
||||
}
|
||||
|
||||
binding.pbCustomLoading.visibility = View.VISIBLE
|
||||
binding.btnVerifyCustomOtp.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
val verifyResult = ApiClient.verifyOtp(
|
||||
phone = customerPhone,
|
||||
code = code,
|
||||
name = customerName,
|
||||
city = customerCity,
|
||||
address = ""
|
||||
)
|
||||
|
||||
if (verifyResult.isSuccess && verifyResult.getOrNull() == true) {
|
||||
UserManager.saveUser(
|
||||
requireContext(),
|
||||
customerName,
|
||||
customerPhone,
|
||||
customerCity,
|
||||
"",
|
||||
isVerified = true
|
||||
)
|
||||
submitFinalCustomRequest()
|
||||
} else {
|
||||
binding.pbCustomLoading.visibility = View.GONE
|
||||
binding.btnVerifyCustomOtp.isEnabled = true
|
||||
val errorMsg = verifyResult.exceptionOrNull()?.message ?: "Código incorrecto o expirado"
|
||||
binding.etCustomOtpCode.error = errorMsg
|
||||
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitFinalCustomRequest() {
|
||||
binding.pbCustomLoading.visibility = View.VISIBLE
|
||||
|
||||
val effectiveBrand = if (initialBrand.isNotBlank()) initialBrand else "Sneakers"
|
||||
val requestData = mapOf(
|
||||
"customerName" to customerName,
|
||||
"customerPhone" to customerPhone,
|
||||
"customerCity" to customerCity,
|
||||
"brand" to effectiveBrand,
|
||||
"modelName" to modelDesired,
|
||||
"size" to sizeDesired,
|
||||
"colorway" to notes,
|
||||
"notes" to "Solicitud registrada desde App Android Nativa (WhatsApp OTP Verificado)"
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val submitResult = ApiClient.submitCustomRequest(requestData)
|
||||
binding.pbCustomLoading.visibility = View.GONE
|
||||
|
||||
submitResult.onSuccess {
|
||||
binding.layoutCustomForm.visibility = View.GONE
|
||||
binding.layoutCustomOtp.visibility = View.GONE
|
||||
binding.layoutCustomSuccess.visibility = View.VISIBLE
|
||||
binding.tvCustomSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue verificado y tu encargo para las '$modelDesired' fue registrado en la plataforma!\n\nLa plataforma notificó al administrador Carlos (+57 316 745 5877) y te contactaremos por WhatsApp con la cotización exacta."
|
||||
}.onFailure { err ->
|
||||
binding.btnVerifyCustomOtp.isEnabled = true
|
||||
Toast.makeText(requireContext(), "Error al registrar: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
|
||||
@ -0,0 +1,299 @@
|
||||
package co.dripzone.app.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import co.dripzone.app.databinding.BottomSheetOrderVerificationBinding
|
||||
import co.dripzone.app.api.ApiClient
|
||||
import co.dripzone.app.model.CartItem
|
||||
import co.dripzone.app.model.CartManager
|
||||
import co.dripzone.app.model.City
|
||||
import co.dripzone.app.model.UserManager
|
||||
import co.dripzone.app.security.SecurityManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private var _binding: BottomSheetOrderVerificationBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var selectedCity: City = City.defaultCities().first()
|
||||
private var isDirectProductOrder: Boolean = false
|
||||
private var directItem: CartItem? = null
|
||||
|
||||
private var customerName: String = ""
|
||||
private var customerPhone: String = ""
|
||||
private var customerAddress: String = ""
|
||||
private var createdOrderId: String = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
@Suppress("DEPRECATION")
|
||||
val cityArg = it.getSerializable(ARG_CITY) as? City
|
||||
if (cityArg != null) selectedCity = cityArg
|
||||
isDirectProductOrder = it.getBoolean(ARG_IS_DIRECT, false)
|
||||
@Suppress("DEPRECATION")
|
||||
directItem = it.getSerializable(ARG_DIRECT_ITEM) as? CartItem
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetOrderVerificationBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.btnCloseOrderModal.setOnClickListener { dismiss() }
|
||||
|
||||
// Pre-fill user data if already registered locally
|
||||
val user = UserManager.getUser(requireContext())
|
||||
if (user.name.isNotBlank()) binding.etCustomerName.setText(user.name)
|
||||
if (user.phone.isNotBlank()) binding.etCustomerPhone.setText(user.phone)
|
||||
if (user.address.isNotBlank()) binding.etCustomerAddress.setText(user.address)
|
||||
|
||||
// Setup economic calculations
|
||||
val subtotal = if (isDirectProductOrder && directItem != null) {
|
||||
directItem!!.itemTotal
|
||||
} else {
|
||||
CartManager.getSubtotal()
|
||||
}
|
||||
val total = subtotal + selectedCity.shippingCost
|
||||
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
|
||||
|
||||
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO")).apply {
|
||||
maximumFractionDigits = 0
|
||||
}
|
||||
|
||||
binding.tvOrderSummaryCity.text = "Destino: ${selectedCity.name} (${selectedCity.formattedShipping()})"
|
||||
binding.tvOrderSummaryTotal.text = "${format.format(total)} COP"
|
||||
binding.tvOrderSummaryBreakdown.text = "Pago Contraentrega • Separas con $10.000 COP y cancelas ${format.format(balanceDue)} COP al recibir."
|
||||
|
||||
binding.btnContinueToOtp.setOnClickListener {
|
||||
handleContinueToOtp()
|
||||
}
|
||||
|
||||
binding.btnVerifyOtpAndSubmit.setOnClickListener {
|
||||
handleVerifyOtpAndSubmit()
|
||||
}
|
||||
|
||||
binding.tvResendOtp.setOnClickListener {
|
||||
sendOtpCode()
|
||||
}
|
||||
|
||||
binding.btnFinishOrder.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||
val message = "¡Hola Carlos! Acabo de registrar mi pedido en la app oficial de DripZone con la orden *$createdOrderId*. ¿Me confirmas la programación del despacho contraentrega a ${selectedCity.name}?"
|
||||
SecurityManager.openWhatsApp(
|
||||
requireContext(),
|
||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||
message
|
||||
)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleContinueToOtp() {
|
||||
val rawName = binding.etCustomerName.text?.toString()?.trim() ?: ""
|
||||
val rawPhone = binding.etCustomerPhone.text?.toString()?.replace("\\D".toRegex(), "") ?: ""
|
||||
val rawAddress = binding.etCustomerAddress.text?.toString()?.trim() ?: ""
|
||||
|
||||
if (rawName.isBlank()) {
|
||||
binding.etCustomerName.error = "Por favor ingresa tu nombre completo"
|
||||
return
|
||||
}
|
||||
if (rawPhone.length < 10) {
|
||||
binding.etCustomerPhone.error = "Ingresa tu celular de WhatsApp a 10 dígitos"
|
||||
return
|
||||
}
|
||||
if (rawAddress.isBlank()) {
|
||||
binding.etCustomerAddress.error = "Ingresa tu dirección completa de entrega"
|
||||
return
|
||||
}
|
||||
|
||||
customerName = rawName
|
||||
customerPhone = rawPhone
|
||||
customerAddress = rawAddress
|
||||
|
||||
val user = UserManager.getUser(requireContext())
|
||||
// Si el usuario ya está verificado para este número, procede directo a la orden
|
||||
if (user.isPhoneVerified && user.phone == customerPhone) {
|
||||
submitFinalOrder()
|
||||
} else {
|
||||
sendOtpCode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOtpCode() {
|
||||
binding.layoutStepCustomer.visibility = View.GONE
|
||||
binding.layoutStepOtp.visibility = View.VISIBLE
|
||||
binding.pbOtpLoading.visibility = View.VISIBLE
|
||||
binding.btnVerifyOtpAndSubmit.isEnabled = false
|
||||
|
||||
val masked = if (customerPhone.length >= 10) {
|
||||
"${customerPhone.take(3)}***${customerPhone.takeLast(4)}"
|
||||
} else customerPhone
|
||||
|
||||
binding.tvOtpInstruction.text = "Enviamos un código de seguridad de 6 dígitos a tu WhatsApp (+57 $masked) para validar tu cuenta y registrar tu orden:"
|
||||
|
||||
lifecycleScope.launch {
|
||||
val result = ApiClient.sendOtp(customerPhone, customerName)
|
||||
binding.pbOtpLoading.visibility = View.GONE
|
||||
binding.btnVerifyOtpAndSubmit.isEnabled = true
|
||||
|
||||
result.onSuccess { msg ->
|
||||
Toast.makeText(requireContext(), "Código enviado por WhatsApp: Revisa tu chat", Toast.LENGTH_LONG).show()
|
||||
}.onFailure { err ->
|
||||
Toast.makeText(requireContext(), "Aviso: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerifyOtpAndSubmit() {
|
||||
val otpCode = binding.etOtpCode.text?.toString()?.trim() ?: ""
|
||||
if (otpCode.length < 6) {
|
||||
binding.etOtpCode.error = "Ingresa el código de 6 dígitos"
|
||||
return
|
||||
}
|
||||
|
||||
binding.pbOtpLoading.visibility = View.VISIBLE
|
||||
binding.btnVerifyOtpAndSubmit.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
val verifyResult = ApiClient.verifyOtp(
|
||||
phone = customerPhone,
|
||||
code = otpCode,
|
||||
name = customerName,
|
||||
city = selectedCity.name,
|
||||
address = customerAddress
|
||||
)
|
||||
|
||||
if (verifyResult.isSuccess && verifyResult.getOrNull() == true) {
|
||||
// Save verified user locally
|
||||
UserManager.saveUser(
|
||||
requireContext(),
|
||||
customerName,
|
||||
customerPhone,
|
||||
selectedCity.name,
|
||||
customerAddress,
|
||||
isVerified = true
|
||||
)
|
||||
submitFinalOrder()
|
||||
} else {
|
||||
binding.pbOtpLoading.visibility = View.GONE
|
||||
binding.btnVerifyOtpAndSubmit.isEnabled = true
|
||||
val errorMsg = verifyResult.exceptionOrNull()?.message ?: "Código inválido o expirado"
|
||||
binding.etOtpCode.error = errorMsg
|
||||
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitFinalOrder() {
|
||||
binding.pbOtpLoading.visibility = View.VISIBLE
|
||||
|
||||
val items = if (isDirectProductOrder && directItem != null) {
|
||||
listOf(directItem!!)
|
||||
} else {
|
||||
CartManager.getItems()
|
||||
}
|
||||
|
||||
val subtotal = items.sumOf { it.itemTotal }
|
||||
val total = subtotal + selectedCity.shippingCost
|
||||
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
|
||||
|
||||
val itemsPayload = items.map {
|
||||
mapOf(
|
||||
"productId" to it.product.id,
|
||||
"productName" to it.product.name,
|
||||
"brand" to it.product.brand,
|
||||
"size" to it.selectedSize,
|
||||
"quantity" to it.quantity,
|
||||
"price" to it.product.retailPrice,
|
||||
"image" to it.product.firstImage()
|
||||
)
|
||||
}
|
||||
|
||||
val orderData = mapOf(
|
||||
"customerName" to customerName,
|
||||
"customerPhone" to customerPhone,
|
||||
"address" to customerAddress,
|
||||
"cityId" to selectedCity.id,
|
||||
"cityName" to selectedCity.name,
|
||||
"items" to itemsPayload,
|
||||
"subtotal" to subtotal,
|
||||
"shippingCost" to selectedCity.shippingCost,
|
||||
"totalAmount" to total,
|
||||
"depositAmount" to 10000.0,
|
||||
"balanceDue" to balanceDue,
|
||||
"paymentMethodName" to "Pago Contraentrega",
|
||||
"notes" to "Pedido registrado desde App Android Nativa (WhatsApp OTP Verificado)"
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val orderResult = ApiClient.submitOrder(orderData)
|
||||
binding.pbOtpLoading.visibility = View.GONE
|
||||
|
||||
orderResult.onSuccess { orderId ->
|
||||
createdOrderId = orderId
|
||||
// Limpiar carrito
|
||||
if (!isDirectProductOrder) {
|
||||
CartManager.clear(requireContext())
|
||||
}
|
||||
|
||||
// Mostrar pantalla de éxito
|
||||
binding.layoutStepCustomer.visibility = View.GONE
|
||||
binding.layoutStepOtp.visibility = View.GONE
|
||||
binding.layoutStepSuccess.visibility = View.VISIBLE
|
||||
binding.tvSuccessOrderId.text = "Orden #$orderId"
|
||||
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\nLa plataforma notificó automáticamente al administrador Carlos (+57 316 745 5877) para preparar tu paquete."
|
||||
}.onFailure { err ->
|
||||
binding.btnVerifyOtpAndSubmit.isEnabled = true
|
||||
Toast.makeText(requireContext(), "Error al registrar orden: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "OrderVerificationBottomSheet"
|
||||
private const val ARG_CITY = "arg_city"
|
||||
private const val ARG_IS_DIRECT = "arg_is_direct"
|
||||
private const val ARG_DIRECT_ITEM = "arg_direct_item"
|
||||
|
||||
fun newInstance(
|
||||
city: City,
|
||||
isDirect: Boolean = false,
|
||||
directItem: CartItem? = null
|
||||
): OrderVerificationBottomSheet {
|
||||
return OrderVerificationBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ARG_CITY, city)
|
||||
putBoolean(ARG_IS_DIRECT, isDirect)
|
||||
if (directItem != null) {
|
||||
putSerializable(ARG_DIRECT_ITEM, directItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,27 +78,23 @@ class ProductDetailBottomSheet : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// WhatsApp Order Checkout Button
|
||||
// Direct Order Checkout Button with Verification
|
||||
binding.btnOrderWhatsApp.setOnClickListener {
|
||||
val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "Por coordinar"
|
||||
val whatsappMessage = buildString {
|
||||
append("¡Hola DripZone! 🔥 Me interesa ordenar este producto:\n\n")
|
||||
append("👟 *Producto:* ${p.name}\n")
|
||||
append("🏷️ *Marca:* ${p.brand}\n")
|
||||
append("📏 *Talla solicitada:* $sizeToOrder\n")
|
||||
append("💰 *Precio:* ${p.formattedPrice()}\n")
|
||||
append("🚚 *Método:* Envío con Pago Contraentrega\n")
|
||||
if (imageUrl.isNotBlank()) {
|
||||
append("\n🔗 *Foto:* $imageUrl\n")
|
||||
}
|
||||
append("\n¿Tienen disponibilidad inmediata para despacho?")
|
||||
}
|
||||
|
||||
SecurityManager.openWhatsApp(
|
||||
requireContext(),
|
||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||
whatsappMessage
|
||||
val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "39"
|
||||
val directItem = co.dripzone.app.model.CartItem(
|
||||
product = p,
|
||||
selectedSize = sizeToOrder,
|
||||
quantity = 1
|
||||
)
|
||||
val city = (activity as? co.dripzone.app.MainActivity)?.selectedCity ?: co.dripzone.app.model.City.defaultCities().first()
|
||||
val verificationSheet = OrderVerificationBottomSheet.newInstance(
|
||||
city = city,
|
||||
isDirect = true,
|
||||
directItem = directItem
|
||||
)
|
||||
parentFragmentManager.let {
|
||||
verificationSheet.show(it, OrderVerificationBottomSheet.TAG)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
@ -8,16 +8,18 @@
|
||||
android:id="@+id/ivSplash"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="DripZone Splash Screen"
|
||||
android:scaleType="centerCrop"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/dripzone_splash_clean" />
|
||||
|
||||
<!-- Subtle bottom indicator -->
|
||||
<ProgressBar
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:layout_marginBottom="36dp"
|
||||
android:indeterminateTint="@color/drip_fuchsia" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@ -0,0 +1,358 @@
|
||||
<?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">
|
||||
|
||||
<!-- Drag handle -->
|
||||
<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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOrderModalTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="CONFIRMAR PEDIDO"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="17sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnCloseOrderModal"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Cerrar"
|
||||
android:src="@drawable/ic_close"
|
||||
app:tint="@color/text_secondary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- STEP 1: Customer & Shipping Data Layout -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutStepCustomer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Ingresa tus datos para registrar tu pedido en la plataforma y coordinar el despacho contraentrega."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<!-- Nombre -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Nombre y Apellido *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomerName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. Juan Carlos Pérez"
|
||||
android:inputType="textPersonName"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- Celular WhatsApp -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Celular / WhatsApp (10 dígitos) *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomerPhone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. 3167455877"
|
||||
android:inputType="phone"
|
||||
android:maxLength="10"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- Dirección -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Dirección de entrega y Barrio *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomerAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. Calle 15 #4-22, Barrio San Antonio"
|
||||
android:inputType="textPostalAddress"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- Resumen económico -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="@drawable/bg_btn_secondary"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOrderSummaryCity"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Destino: Cali"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOrderSummaryTotal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="$ 280.000 COP"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="13sp" />
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOrderSummaryBreakdown"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Pago Contraentrega • Separas con $10.000 COP y pagas el saldo al recibir"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Botón Continuar -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnContinueToOtp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Validar Celular y Registrar Pedido"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/drip_purple"
|
||||
app:cornerRadius="14dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="18dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- STEP 2: OTP Verification Layout -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutStepOtp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="🔐 Verificación de WhatsApp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOtpInstruction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="Ingresa el código de seguridad de 6 dígitos que enviamos por WhatsApp a tu celular para validar tu cuenta y confirmar el pedido:"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<!-- Input OTP de 6 dígitos -->
|
||||
<EditText
|
||||
android:id="@+id/etOtpCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="54dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="center"
|
||||
android:hint="• • • • • •"
|
||||
android:inputType="number"
|
||||
android:letterSpacing="0.3"
|
||||
android:maxLength="6"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<ProgressBar
|
||||
android:id="@+id/pbOtpLoading"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="14dp"
|
||||
android:indeterminateTint="@color/drip_fuchsia"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Botón Verificar y Completar -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnVerifyOtpAndSubmit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Confirmar Código OTP"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/drip_purple"
|
||||
app:cornerRadius="14dp" />
|
||||
|
||||
<!-- Reenviar código -->
|
||||
<TextView
|
||||
android:id="@+id/tvResendOtp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="14dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:text="¿No recibiste el código? Reenviar por WhatsApp"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- STEP 3: Success Confirmation Layout -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutStepSuccess"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:src="@drawable/ic_sparkle"
|
||||
app:tint="@color/drip_fuchsia" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="¡PEDIDO REGISTRADO CON ÉXITO!"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSuccessOrderId"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:fontFamily="monospace"
|
||||
android:text="Orden #P-123456"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSuccessMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center"
|
||||
android:text="Tu número fue verificado con éxito y tu pedido ya está registrado en nuestra plataforma.\n\nLa plataforma ha notificado automáticamente al administrador Carlos (+57 316 745 5877) y recibirás confirmación oficial a tu WhatsApp."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnOpenWhatsAppChat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Abrir Chat con el Asesor en WhatsApp"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:cornerRadius="14dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="18dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnFinishOrder"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Cerrar y Seguir Comprando"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@ -19,109 +19,346 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="#40FFFFFF" />
|
||||
|
||||
<TextView
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="🔥 Pedido Especial / Encargo"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="20sp" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="🔥 Pedido Especial / Encargo"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnCloseCustomModal"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Cerrar"
|
||||
android:src="@drawable/ic_close"
|
||||
app:tint="@color/text_secondary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
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? Te lo conseguimos bajo pedido."
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="¿Buscas un modelo exclusivo no listado? Te lo cotizamos directamente con nuestras bodegas mayoristas."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
android:textSize="12sp" />
|
||||
|
||||
<!-- Reference / Model input -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
<!-- Form Step Layout -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutCustomForm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
android:hint="Modelo / Referencia (ej. Jordan 4 Military Black)"
|
||||
android:textColorHint="@color/text_muted"
|
||||
app:boxBackgroundColor="@color/bg_input"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="@color/drip_fuchsia">
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
<!-- Nombre -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Tu Nombre Completo *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomCustomerName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. Carlos Mendoza"
|
||||
android:inputType="textPersonName"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- WhatsApp -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Tu WhatsApp (10 dígitos) *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomCustomerPhone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. 3167455877"
|
||||
android:inputType="phone"
|
||||
android:maxLength="10"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- Ciudad -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Ciudad *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomCustomerCity"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. Cali"
|
||||
android:inputType="textCapWords"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- Modelo -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Modelo / Referencia de Zapatilla *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomModel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. Jordan 4 Retro Military Black"
|
||||
android:inputType="textCapWords"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- Size input -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="Talla deseada (ej. 41 EUR / 8 US)"
|
||||
android:textColorHint="@color/text_muted"
|
||||
app:boxBackgroundColor="@color/bg_input"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="@color/drip_fuchsia">
|
||||
<!-- Talla -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Talla deseada *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
<EditText
|
||||
android:id="@+id/etCustomSize"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. 41 EUR (8 US)"
|
||||
android:inputType="text"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- Notes input -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
<!-- Notas / Presupuesto -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Colorway / Presupuesto / Detalles"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomNotes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. Color blanco/negro, presupuesto hasta $300.000"
|
||||
android:inputType="text"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSubmitCustomOrder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Validar y Registrar Encargo"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/drip_purple"
|
||||
app:cornerRadius="14dp"
|
||||
app:icon="@drawable/ic_encargos"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="20dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- OTP Step Layout for Custom Request -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutCustomOtp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="Colorway o detalles adicionales"
|
||||
android:textColorHint="@color/text_muted"
|
||||
app:boxBackgroundColor="@color/bg_input"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="@color/drip_fuchsia">
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etCustomNotes"
|
||||
android:layout_width="match_parent"
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="2"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="🔐 Verificación de WhatsApp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:textSize="15sp" />
|
||||
|
||||
<!-- Submit to WhatsApp Button with Tapjacking Protection -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSubmitCustomOrder"
|
||||
<TextView
|
||||
android:id="@+id/tvCustomOtpInstruction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="Ingresa el código de 6 dígitos que enviamos a tu WhatsApp para validar tu cuenta y registrar tu encargo:"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCustomOtpCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="54dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="center"
|
||||
android:hint="• • • • • •"
|
||||
android:inputType="number"
|
||||
android:letterSpacing="0.3"
|
||||
android:maxLength="6"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_muted"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pbCustomLoading"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
android:indeterminateTint="@color/drip_fuchsia"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnVerifyCustomOtp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Confirmar Código y Registrar Encargo"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/drip_purple"
|
||||
app:cornerRadius="14dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvResendCustomOtp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:text="¿No recibiste el código? Reenviar por WhatsApp"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Success Step Layout -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutCustomSuccess"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Consultar Encargo en WhatsApp"
|
||||
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" />
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:src="@drawable/ic_sparkle"
|
||||
app:tint="@color/drip_fuchsia" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="¡ENCARGO REGISTRADO CON ÉXITO!"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCustomSuccessMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="Hemos validado tu número de WhatsApp y registrado tu solicitud en la plataforma.\n\nLa plataforma notificó al administrador Carlos (+57 316 745 5877) y te contactaremos por WhatsApp con la cotización exacta."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnCustomOpenWhatsApp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Abrir WhatsApp del Asesor Carlos"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:cornerRadius="14dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="18dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnFinishCustom"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="Listo, Volver a la Tienda"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
<!-- Splash Theme -->
|
||||
<style name="Theme.DripZone.Splash" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="android:windowBackground">@drawable/dripzone_splash_clean</item>
|
||||
<item name="android:windowBackground">@color/bg_dark</item>
|
||||
<item name="android:statusBarColor">@color/bg_dark</item>
|
||||
<item name="android:navigationBarColor">@color/bg_dark</item>
|
||||
</style>
|
||||
|
||||
@ -5,7 +5,7 @@ import { sendWhatsAppMessage } from '@/lib/evolution-api';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { identifier } = await request.json();
|
||||
const { identifier, name } = await request.json();
|
||||
if (!identifier) {
|
||||
return NextResponse.json({ success: false, error: 'Ingresa tu número de celular o correo' }, { status: 400 });
|
||||
}
|
||||
@ -15,14 +15,13 @@ export async function POST(request: Request) {
|
||||
const user = dataService.authenticate(cleanInput);
|
||||
|
||||
let phoneToSend = cleanInput.replace(/\D/g, '');
|
||||
let userName = 'Cliente';
|
||||
let userName = name?.trim() || (user ? user.name : 'Cliente');
|
||||
|
||||
if (user) {
|
||||
if (!user.isActive) {
|
||||
return NextResponse.json({ success: false, error: 'Esta cuenta se encuentra inactiva.' }, { status: 403 });
|
||||
}
|
||||
phoneToSend = user.phone.replace(/\D/g, '');
|
||||
userName = user.name;
|
||||
}
|
||||
|
||||
if (!phoneToSend || phoneToSend.length < 7) {
|
||||
|
||||
@ -4,7 +4,7 @@ import { otpStore } from '@/lib/otp-store';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { identifier, code } = await request.json();
|
||||
const { identifier, code, name, city, address } = await request.json();
|
||||
if (!identifier || !code) {
|
||||
return NextResponse.json({ success: false, error: 'Identificador y código son requeridos' }, { status: 400 });
|
||||
}
|
||||
@ -16,18 +16,20 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ success: false, error: verification.reason || 'Código inválido o expirado' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Código válido -> Obtener o crear sesión
|
||||
// Código válido -> Obtener o crear usuario registrado en la plataforma
|
||||
let user = dataService.authenticate(cleanInput);
|
||||
|
||||
if (!user) {
|
||||
// Si es un nuevo cliente autenticado por primera vez con su celular
|
||||
const cleanPhone = cleanInput.replace(/\D/g, '');
|
||||
const assignedName = name?.trim() || `Cliente Verificado (${cleanPhone.slice(-4)})`;
|
||||
user = dataService.addUser({
|
||||
id: `usr-${Date.now()}`,
|
||||
name: `Cliente Verificado (${cleanPhone.slice(-4)})`,
|
||||
name: assignedName,
|
||||
email: `${cleanPhone}@cliente.dripzone.co`,
|
||||
phone: cleanPhone,
|
||||
city: 'Cali',
|
||||
city: city?.trim() || 'Cali',
|
||||
address: address?.trim() || '',
|
||||
role: 'customer',
|
||||
isActive: true,
|
||||
phoneVerified: true,
|
||||
@ -39,9 +41,12 @@ export async function POST(request: Request) {
|
||||
lastLogin: 'Justo ahora',
|
||||
});
|
||||
} else {
|
||||
// Usuario existente: si verificó su teléfono, actualizar estado
|
||||
// Usuario existente: si verificó su teléfono, actualizar estado y datos si se suministraron
|
||||
const hadRejections = (user.rejectedOrdersCount || 0) > 0;
|
||||
dataService.updateUser(user.id, {
|
||||
name: name?.trim() || user.name,
|
||||
city: city?.trim() || user.city,
|
||||
address: address?.trim() || user.address,
|
||||
phoneVerified: true,
|
||||
requiresShippingDeposit: hadRejections ? true : false,
|
||||
depositReason: hadRejections
|
||||
|
||||
@ -140,6 +140,7 @@ ${customerEmail ? `✉️ *Correo:* ${customerEmail}\n` : ''}
|
||||
⚡ ¡Consulta de inmediato con tus proveedores mayoristas para enviar cotización al cliente!`;
|
||||
|
||||
sendWhatsAppMessage(CARLOS_PHONE, adminMessage).catch(() => {});
|
||||
sendWhatsAppMessage('120363428012905103@g.us', adminMessage).catch(() => {});
|
||||
|
||||
// 2. Confirmación automática al cliente por WhatsApp
|
||||
const clientMessage = `👋 ¡Hola *${customerName}*!
|
||||
|
||||
@ -5,7 +5,11 @@ import {
|
||||
updateOrderInAppwrite,
|
||||
deleteOrderInAppwrite,
|
||||
} from '@/lib/server-appwrite';
|
||||
import { sendWhatsAppMessage, buildOrderConfirmationMessage } from '@/lib/evolution-api';
|
||||
import {
|
||||
sendWhatsAppMessage,
|
||||
buildOrderConfirmationMessage,
|
||||
notifyAdminNewOrder,
|
||||
} from '@/lib/evolution-api';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
|
||||
export async function GET() {
|
||||
@ -24,13 +28,20 @@ export async function POST(request: Request) {
|
||||
const newOrder = await createOrderInAppwrite(body);
|
||||
dataService.addOrder(newOrder);
|
||||
|
||||
// Enviar confirmación por WhatsApp en background si tiene teléfono válido
|
||||
// 1. Notificación INMEDIATA al WhatsApp de Carlos (+57 316 745 5877)
|
||||
try {
|
||||
await notifyAdminNewOrder(newOrder);
|
||||
} catch (adminWaErr) {
|
||||
console.warn('No se pudo enviar WhatsApp al administrador Carlos:', adminWaErr);
|
||||
}
|
||||
|
||||
// 2. Enviar confirmación por WhatsApp al cliente en background si tiene teléfono válido
|
||||
if (newOrder.customerPhone) {
|
||||
try {
|
||||
const msg = buildOrderConfirmationMessage(newOrder);
|
||||
await sendWhatsAppMessage(newOrder.customerPhone, msg);
|
||||
} catch (waErr) {
|
||||
console.warn('No se pudo enviar WhatsApp automático:', waErr);
|
||||
console.warn('No se pudo enviar WhatsApp automático al cliente:', waErr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,6 +56,10 @@ export default function CheckoutPage() {
|
||||
const [orderCompleted, setOrderCompleted] = useState<Order | null>(null);
|
||||
const [userRequiresDeposit, setUserRequiresDeposit] = useState<boolean>(false);
|
||||
const [userDepositReason, setUserDepositReason] = useState<string>('');
|
||||
const [showOtpModal, setShowOtpModal] = useState<boolean>(false);
|
||||
const [otpCode, setOtpCode] = useState<string>('');
|
||||
const [otpLoading, setOtpLoading] = useState<boolean>(false);
|
||||
const [otpError, setOtpError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/cities')
|
||||
@ -117,15 +121,8 @@ export default function CheckoutPage() {
|
||||
const officialWhatsapp = storeSettings?.whatsappNumber || '573167455877';
|
||||
const boldLink = storeSettings?.boldPaymentLink || 'https://bold.co/p/carlos-lasso';
|
||||
|
||||
const handleSubmitOrder = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!customerName || !customerPhone || !address) {
|
||||
alert('Por favor completa tu nombre, celular y dirección para el despacho.');
|
||||
return;
|
||||
}
|
||||
|
||||
const processOrderSubmission = async () => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const orderPayload = {
|
||||
customerName,
|
||||
@ -158,14 +155,90 @@ export default function CheckoutPage() {
|
||||
spread: 70,
|
||||
origin: { y: 0.6 },
|
||||
});
|
||||
} else {
|
||||
alert(data.error || 'Error al registrar pedido.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creando orden:', err);
|
||||
alert('Error de conexión al registrar pedido.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitOrder = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!customerName || !customerPhone || !address) {
|
||||
alert('Por favor completa tu nombre, celular y dirección para el despacho.');
|
||||
return;
|
||||
}
|
||||
const cleanPhone = customerPhone.replace(/\D/g, '');
|
||||
if (cleanPhone.length < 10) {
|
||||
alert('Por favor ingresa un número de celular/WhatsApp válido a 10 dígitos.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (user?.phoneVerified && user.phone === cleanPhone) {
|
||||
await processOrderSubmission();
|
||||
return;
|
||||
}
|
||||
|
||||
// Solicitar OTP por WhatsApp
|
||||
setOtpLoading(true);
|
||||
setOtpError('');
|
||||
try {
|
||||
const res = await fetch('/api/auth/send-otp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identifier: cleanPhone, name: customerName }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setShowOtpModal(true);
|
||||
} else {
|
||||
alert(data.error || 'No se pudo enviar el código OTP a WhatsApp.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error al solicitar código OTP por WhatsApp.');
|
||||
} finally {
|
||||
setOtpLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyOtpAndPlaceOrder = async () => {
|
||||
if (otpCode.length < 6) {
|
||||
setOtpError('Ingresa el código de 6 dígitos enviado a tu WhatsApp.');
|
||||
return;
|
||||
}
|
||||
setOtpLoading(true);
|
||||
setOtpError('');
|
||||
try {
|
||||
const cleanPhone = customerPhone.replace(/\D/g, '');
|
||||
const res = await fetch('/api/auth/verify-otp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
identifier: cleanPhone,
|
||||
code: otpCode,
|
||||
name: customerName,
|
||||
city: activeCity.name,
|
||||
address: `${address} - Barrio: ${neighborhood || 'Centro'}`,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setShowOtpModal(false);
|
||||
await processOrderSubmission();
|
||||
} else {
|
||||
setOtpError(data.error || 'Código incorrecto o expirado.');
|
||||
}
|
||||
} catch (err) {
|
||||
setOtpError('Error verificando código.');
|
||||
} finally {
|
||||
setOtpLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenWhatsAppOrder = (order: Order) => {
|
||||
const itemsSummary = order.items
|
||||
.map((i) => `• ${i.productName} (Talla ${i.size}) x${i.quantity} = ${formatCOP(i.price * i.quantity)}`)
|
||||
@ -540,6 +613,49 @@ export default function CheckoutPage() {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{showOtpModal && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in">
|
||||
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-md w-full p-6 text-white space-y-4 shadow-2xl">
|
||||
<div className="flex justify-between items-center border-b border-neutral-800 pb-3">
|
||||
<h3 className="text-base font-black flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5 text-purple-400" />
|
||||
Verificación de WhatsApp (OTP)
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowOtpModal(false)}
|
||||
className="text-neutral-400 hover:text-white text-sm cursor-pointer p-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-300 leading-relaxed">
|
||||
Para validar tu cuenta y registrar tu orden contraentrega en la plataforma, te enviamos un código de seguridad de 6 dígitos a tu WhatsApp <strong>+57 {customerPhone}</strong>:
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={6}
|
||||
value={otpCode}
|
||||
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, ''))}
|
||||
placeholder="• • • • • •"
|
||||
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-4 py-3 text-center text-2xl font-mono tracking-widest text-white outline-none"
|
||||
/>
|
||||
{otpError && <p className="text-xs text-rose-400 text-center">{otpError}</p>}
|
||||
<button
|
||||
type="button"
|
||||
disabled={otpLoading || otpCode.length < 6}
|
||||
onClick={handleVerifyOtpAndPlaceOrder}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-600 text-white font-bold py-3.5 rounded-xl hover:opacity-95 disabled:opacity-50 text-sm shadow-md transition-all cursor-pointer"
|
||||
>
|
||||
{otpLoading ? 'Verificando...' : 'Confirmar Código y Registrar Pedido'}
|
||||
</button>
|
||||
<p className="text-[11px] text-neutral-500 text-center">
|
||||
El código expira en 5 minutos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -46,6 +46,10 @@ export const CustomRequestModal: React.FC<CustomRequestModalProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [showOtp, setShowOtp] = useState(false);
|
||||
const [otpCode, setOtpCode] = useState('');
|
||||
const [otpLoading, setOtpLoading] = useState(false);
|
||||
const [otpError, setOtpError] = useState('');
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@ -68,20 +72,12 @@ export const CustomRequestModal: React.FC<CustomRequestModalProps> = ({
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const processCustomRequest = async () => {
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
const effectiveBrand = brand === 'Otra' && customBrand.trim() ? customBrand.trim() : brand;
|
||||
const effectiveSize = size === 'custom' && customSize.trim() ? customSize.trim() : size;
|
||||
|
||||
if (!customerName.trim() || !customerPhone.trim() || !modelName.trim()) {
|
||||
setErrorMessage('Por favor completa tu nombre, WhatsApp y el modelo de las zapatillas.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/custom-requests', {
|
||||
method: 'POST',
|
||||
@ -113,6 +109,72 @@ export const CustomRequestModal: React.FC<CustomRequestModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrorMessage(null);
|
||||
|
||||
const cleanPhone = customerPhone.replace(/\D/g, '');
|
||||
if (!customerName.trim() || cleanPhone.length < 10 || !modelName.trim()) {
|
||||
setErrorMessage('Por favor completa tu nombre, número de WhatsApp a 10 dígitos y el modelo deseado.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Solicitar código OTP por WhatsApp
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/auth/send-otp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identifier: cleanPhone, name: customerName.trim() }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setShowOtp(true);
|
||||
} else {
|
||||
setErrorMessage(data.error || 'No se pudo enviar el código OTP a WhatsApp.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setErrorMessage('Error al solicitar código OTP por WhatsApp.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyOtpAndSubmitRequest = async () => {
|
||||
if (otpCode.length < 6) {
|
||||
setOtpError('Ingresa el código de 6 dígitos que enviamos a tu WhatsApp.');
|
||||
return;
|
||||
}
|
||||
setOtpLoading(true);
|
||||
setOtpError('');
|
||||
try {
|
||||
const cleanPhone = customerPhone.replace(/\D/g, '');
|
||||
const verifyRes = await fetch('/api/auth/verify-otp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
identifier: cleanPhone,
|
||||
code: otpCode,
|
||||
name: customerName.trim(),
|
||||
city: customerCity.trim() || 'Colombia',
|
||||
}),
|
||||
});
|
||||
const verifyData = await verifyRes.json();
|
||||
if (!verifyData.success) {
|
||||
setOtpError(verifyData.error || 'Código incorrecto o expirado.');
|
||||
setOtpLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowOtp(false);
|
||||
await processCustomRequest();
|
||||
} catch (err) {
|
||||
setOtpError('Error verificando código.');
|
||||
} finally {
|
||||
setOtpLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetAndClose = () => {
|
||||
setIsSuccess(false);
|
||||
setImageBase64(null);
|
||||
@ -426,6 +488,49 @@ export const CustomRequestModal: React.FC<CustomRequestModalProps> = ({
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{showOtp && (
|
||||
<div className="fixed inset-0 z-60 bg-black/85 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in">
|
||||
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-md w-full p-6 text-white space-y-4 shadow-2xl">
|
||||
<div className="flex justify-between items-center border-b border-neutral-800 pb-3">
|
||||
<h3 className="text-base font-black flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5 text-purple-400" />
|
||||
Verificación de WhatsApp (OTP)
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowOtp(false)}
|
||||
className="text-neutral-400 hover:text-white text-sm cursor-pointer p-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-300 leading-relaxed">
|
||||
Para validar tu cuenta y registrar tu encargo en la plataforma, enviamos un código de 6 dígitos a tu WhatsApp <strong>+57 {customerPhone}</strong>:
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={6}
|
||||
value={otpCode}
|
||||
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, ''))}
|
||||
placeholder="• • • • • •"
|
||||
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-4 py-3 text-center text-2xl font-mono tracking-widest text-white outline-none"
|
||||
/>
|
||||
{otpError && <p className="text-xs text-rose-400 text-center">{otpError}</p>}
|
||||
<button
|
||||
type="button"
|
||||
disabled={otpLoading || otpCode.length < 6}
|
||||
onClick={handleVerifyOtpAndSubmitRequest}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-600 text-white font-bold py-3.5 rounded-xl hover:opacity-95 disabled:opacity-50 text-sm shadow-md transition-all cursor-pointer"
|
||||
>
|
||||
{otpLoading ? 'Verificando...' : 'Confirmar Código y Registrar Encargo'}
|
||||
</button>
|
||||
<p className="text-[11px] text-neutral-500 text-center">
|
||||
El código expira en 5 minutos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -247,13 +247,14 @@ export const CartDrawer: React.FC<CartDrawerProps> = ({
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleWhatsAppCheckout}
|
||||
<Link
|
||||
href="/checkout"
|
||||
onClick={onClose}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs py-2.5 px-3 rounded-xl flex items-center justify-center space-x-1.5 transition-all cursor-pointer"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 fill-white" />
|
||||
<span>Completar Pedido por WhatsApp</span>
|
||||
</button>
|
||||
<span>Completar Pedido con Verificación WhatsApp</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -178,3 +178,71 @@ export function buildOrderConfirmationMessage(order: any): string {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifica inmediatamente al administrador Carlos (+57 316 745 5877)
|
||||
* y al canal de notificaciones cuando entra un nuevo pedido en la plataforma (web o app móvil).
|
||||
*/
|
||||
export async function notifyAdminNewOrder(order: any): Promise<boolean> {
|
||||
const CARLOS_PHONE = '573167455877';
|
||||
const NOTIFICATION_GROUP = '120363428012905103@g.us';
|
||||
|
||||
const itemsSummary = (order.items || [])
|
||||
.map((item: any) => `• 👟 *${item.productName || item.name}*\n 📏 Talla: *${item.size || 'Única'}* | Cantidad: *${item.quantity || 1}* | Valor: *${formatCOP((item.price || item.retailPrice || 0) * (item.quantity || 1))}*`)
|
||||
.join('\n\n');
|
||||
|
||||
const paymentCondition = (order.depositAmount && order.depositAmount > 0)
|
||||
? `⚡ Separa con *${formatCOP(order.depositAmount)}* • Saldo Contraentrega: *${formatCOP(order.balanceDue || (order.totalAmount - order.depositAmount))}*`
|
||||
: `🤝 100% Pago Contraentrega (*${formatCOP(order.totalAmount || 0)}*)`;
|
||||
|
||||
const orderId = order.id || `P-${Date.now().toString().slice(-6)}`;
|
||||
const dateStr = new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota' });
|
||||
|
||||
const adminMessage = (
|
||||
`🚨 *¡NUEVO PEDIDO REGISTRADO EN DRIPZONE!* 🛍️📦\n` +
|
||||
`━━━━━━━━━━━━━━━━━━━━━━\n` +
|
||||
`🆔 *Orden #:* ${orderId}\n` +
|
||||
`📅 *Fecha:* ${dateStr}\n\n` +
|
||||
`👤 *DATOS DEL CLIENTE:*\n` +
|
||||
`• *Nombre:* ${order.customerName || 'Cliente DripZone'}\n` +
|
||||
`• *WhatsApp:* ${order.customerPhone || 'Sin registrar'}\n` +
|
||||
`• *Ciudad Destino:* ${order.cityName || 'Colombia'}\n` +
|
||||
`• *Dirección:* ${order.address || 'Por coordinar'}\n` +
|
||||
(order.notes ? `• *Notas:* ${order.notes}\n` : '') +
|
||||
`\n🛒 *PRODUCTOS SOLICITADOS:*\n` +
|
||||
`${itemsSummary || '1x Zapatillas DripZone'}\n\n` +
|
||||
`💰 *LIQUIDACIÓN ECONÓMICA:*\n` +
|
||||
`• *Subtotal:* ${formatCOP(order.subtotal || order.totalAmount || 0)}\n` +
|
||||
`• *Envío:* ${order.shippingCost === 0 ? 'GRATIS' : formatCOP(order.shippingCost || 0)}\n` +
|
||||
`• *TOTAL PEDIDO:* *${formatCOP(order.totalAmount || 0)}*\n` +
|
||||
`• *Condición:* ${paymentCondition}\n` +
|
||||
`• *Saldo a Cobrar Contraentrega:* *${formatCOP(order.balanceDue || order.totalAmount || 0)}*\n` +
|
||||
`━━━━━━━━━━━━━━━━━━━━━━\n` +
|
||||
`⚡ *Gestionar en el Panel de Administración:*\n` +
|
||||
`👉 https://dripzone.almeira.com.co/admin/pedidos`
|
||||
);
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
// 1. Notificación principal al WhatsApp personal de Carlos
|
||||
const resCarlos = await sendWhatsAppMessage(CARLOS_PHONE, adminMessage);
|
||||
if (resCarlos) success = true;
|
||||
|
||||
// 2. Si el producto principal tiene imagen web, enviar foto como soporte
|
||||
const firstImage = order.items?.[0]?.image || (Array.isArray(order.items?.[0]?.images) ? order.items[0].images[0] : null);
|
||||
if (firstImage && (firstImage.startsWith('http://') || firstImage.startsWith('https://'))) {
|
||||
await sendWhatsAppMedia(
|
||||
CARLOS_PHONE,
|
||||
firstImage,
|
||||
`👟 Foto de referencia: ${order.items[0]?.productName || order.items[0]?.name || 'Producto DripZone'}`
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
// 3. Notificación al canal de alertas
|
||||
await sendWhatsAppMessage(NOTIFICATION_GROUP, adminMessage).catch(() => {});
|
||||
} catch (err: any) {
|
||||
console.error('Error enviando notificación de pedido a Carlos:', err.message);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
10
scripts/check_vps.py
Normal file
10
scripts/check_vps.py
Normal file
@ -0,0 +1,10 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('161.97.93.156', username='carlos', password='Innova1978+*?', timeout=20)
|
||||
stdin, stdout, stderr = client.exec_command('docker inspect dripzone-production --format "{{.Config.WorkingDir}} - {{.Config.Cmd}}"')
|
||||
print("dripzone-production cmd:", stdout.read().decode('utf-8'))
|
||||
stdin, stdout, stderr = client.exec_command('docker inspect bfmnoga3lb0ckomhdmxe0rco-021053217798 --format "{{.Config.WorkingDir}} - {{.Config.Cmd}}"')
|
||||
print("coolify dripzone cmd:", stdout.read().decode('utf-8'))
|
||||
client.close()
|
||||
@ -64,28 +64,23 @@ 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 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"
|
||||
"📲 *DripZone - App Nativa Android v1.3 (OTP WhatsApp + Registro Plataforma)*\n\n"
|
||||
"🔥 *¡Actualización Desplegada! Corrección de Splash Screen y Sistema de Validación OTP:*\n\n"
|
||||
"✨ *1. Corrección Total de Splash Screen:*\n"
|
||||
"• Solucionado el desbordamiento de pantalla: ahora se ajusta perfectamente con escalado proporcional (`fitCenter` / `adjustViewBounds`) y fondo `#0A0A0A` sin distorsión ni recortes en ningún modelo de celular.\n\n"
|
||||
"🔐 *2. Registro de Usuario y Validación de WhatsApp con OTP:*\n"
|
||||
"• Al solicitar cualquier pedido o encargo, el usuario registra su nombre, celular y dirección.\n"
|
||||
"• La plataforma envía un código de seguridad de 6 dígitos al WhatsApp del cliente vía Evolution API.\n"
|
||||
"• El cliente valida su número introduciendo el código OTP en la app o web, quedando registrado y verificado en la plataforma.\n\n"
|
||||
"📦 *3. Registro en Plataforma y Notificación Automática a Carlos:*\n"
|
||||
"• Los pedidos (`/api/orders`) y encargos (`/api/custom-requests`) se guardan directamente en la base de datos de la plataforma.\n"
|
||||
"• El servidor de la plataforma es el encargado de enviarte la información completa de inmediato a tu WhatsApp (+57 316 745 5877) con datos del cliente, ciudad, productos, tallas, liquidación contraentrega y foto de soporte.\n"
|
||||
"• El cliente recibe su confirmación oficial por WhatsApp.\n\n"
|
||||
"🌐 *4. Misma Funcionalidad en Web y App Móvil:*\n"
|
||||
"• Tanto la tienda web (`checkout/page.tsx`, `CustomRequestModal.tsx`) como la app nativa en Kotlin comparten exactamente los mismos endpoints, reglas de verificación OTP y plantillas de Evolution API.\n\n"
|
||||
f"⬇️ *Descarga directa del APK actualizado:* {APK_URL}\n"
|
||||
f"⬇️ *Enlace alternativo:* https://dripzone.almeira.com.co/DripZone.apk\n\n"
|
||||
"Adjunto el instalador APK para su instalación y prueba inmediata."
|
||||
"Adjunto el archivo APK instalador actualizado para prueba inmediata en tu dispositivo."
|
||||
)
|
||||
|
||||
req_text = urllib.request.Request(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user