payments: ability to refund
This commit is contained in:
@@ -491,6 +491,7 @@ class MainActivity : ComponentActivity() {
|
||||
bookingId = currentRoute.bookingId,
|
||||
canAddCash = canManageRazorpaySettings(currentRoute.propertyId),
|
||||
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
|
||||
canRefund = canManageRazorpaySettings(currentRoute.propertyId),
|
||||
onBack = {
|
||||
route.value = AppRoute.BookingDetailsTabs(
|
||||
currentRoute.propertyId,
|
||||
|
||||
@@ -22,6 +22,8 @@ import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
|
||||
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkResponse
|
||||
import com.android.trisolarispms.data.api.model.PaymentDto
|
||||
import com.android.trisolarispms.data.api.model.PaymentCreateRequest
|
||||
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
|
||||
import com.android.trisolarispms.data.api.model.RazorpayRefundResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
@@ -132,6 +134,13 @@ interface BookingApi {
|
||||
@Body body: PaymentCreateRequest
|
||||
): Response<PaymentDto>
|
||||
|
||||
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/refund")
|
||||
suspend fun refundRazorpayPayment(
|
||||
@Path("propertyId") propertyId: String,
|
||||
@Path("bookingId") bookingId: String,
|
||||
@Body body: RazorpayRefundRequest
|
||||
): Response<RazorpayRefundResponse>
|
||||
|
||||
@DELETE("properties/{propertyId}/bookings/{bookingId}/payments/{paymentId}")
|
||||
suspend fun deletePayment(
|
||||
@Path("propertyId") propertyId: String,
|
||||
|
||||
@@ -23,3 +23,17 @@ data class PaymentDto(
|
||||
data class PaymentCreateRequest(
|
||||
val amount: Long
|
||||
)
|
||||
|
||||
data class RazorpayRefundRequest(
|
||||
val paymentId: String? = null,
|
||||
val razorpayPaymentId: String? = null,
|
||||
val amount: Long,
|
||||
val notes: String? = null
|
||||
)
|
||||
|
||||
data class RazorpayRefundResponse(
|
||||
val refundId: String? = null,
|
||||
val status: String? = null,
|
||||
val amount: Long? = null,
|
||||
val currency: String? = null
|
||||
)
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package com.android.trisolarispms.data.api.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class RazorpaySettingsRequest(
|
||||
val keyId: String,
|
||||
val keyId: String? = null,
|
||||
val keySecret: String? = null,
|
||||
val webhookSecret: String? = null,
|
||||
@SerializedName("isTest") val isTest: Boolean,
|
||||
val useSalt256: Boolean
|
||||
val keyIdTest: String? = null,
|
||||
val keySecretTest: String? = null,
|
||||
val webhookSecretTest: String? = null,
|
||||
val isTest: Boolean
|
||||
)
|
||||
|
||||
data class RazorpaySettingsResponse(
|
||||
val propertyId: String? = null,
|
||||
val configured: Boolean? = null,
|
||||
@SerializedName("test") val isTest: Boolean? = null,
|
||||
val useSalt256: Boolean? = null,
|
||||
@com.google.gson.annotations.SerializedName("test") val isTest: Boolean? = null,
|
||||
val hasKeyId: Boolean? = null,
|
||||
val hasKeySecret: Boolean? = null,
|
||||
val hasWebhookSecret: Boolean? = null,
|
||||
val hasSalt256: Boolean? = null
|
||||
val hasKeyIdTest: Boolean? = null,
|
||||
val hasKeySecretTest: Boolean? = null,
|
||||
val hasWebhookSecretTest: Boolean? = null
|
||||
)
|
||||
|
||||
@@ -25,14 +25,17 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
@@ -49,11 +52,15 @@ fun BookingPaymentsScreen(
|
||||
bookingId: String,
|
||||
canAddCash: Boolean,
|
||||
canDeleteCash: Boolean,
|
||||
canRefund: Boolean,
|
||||
onBack: () -> Unit,
|
||||
viewModel: BookingPaymentsViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val amountInput = remember { mutableStateOf("") }
|
||||
val refundTarget = remember { mutableStateOf<PaymentDto?>(null) }
|
||||
val refundAmount = rememberSaveable { mutableStateOf("") }
|
||||
val refundNotes = rememberSaveable { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(propertyId, bookingId) {
|
||||
viewModel.load(propertyId, bookingId)
|
||||
@@ -108,6 +115,10 @@ fun BookingPaymentsScreen(
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
state.message?.let {
|
||||
Text(text = it, color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
if (!state.isLoading && state.error == null && state.payments.isEmpty()) {
|
||||
Text(text = "No payments yet")
|
||||
}
|
||||
@@ -121,21 +132,87 @@ fun BookingPaymentsScreen(
|
||||
PaymentCard(
|
||||
payment = payment,
|
||||
canDeleteCash = canDeleteCash,
|
||||
canRefund = canRefund,
|
||||
onDelete = { paymentId ->
|
||||
viewModel.deleteCashPayment(propertyId, bookingId, paymentId)
|
||||
},
|
||||
onRefund = { target ->
|
||||
refundTarget.value = target
|
||||
refundAmount.value = target.amount?.toString().orEmpty()
|
||||
refundNotes.value = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refundTarget.value?.let { payment ->
|
||||
val hasGateway = !payment.gatewayPaymentId.isNullOrBlank()
|
||||
val hasPaymentId = !payment.id.isNullOrBlank()
|
||||
val amountValue = refundAmount.value.toLongOrNull()
|
||||
val canSubmit = amountValue != null && amountValue > 0 && (hasGateway || hasPaymentId)
|
||||
AlertDialog(
|
||||
onDismissRequest = { refundTarget.value = null },
|
||||
title = { Text("Confirm refund") },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = "This will initiate a Razorpay refund. Please confirm before proceeding.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = refundAmount.value,
|
||||
onValueChange = { refundAmount.value = it.filter { ch -> ch.isDigit() } },
|
||||
label = { Text("Refund amount") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = refundNotes.value,
|
||||
onValueChange = { refundNotes.value = it },
|
||||
label = { Text("Notes (optional)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.refundRazorpayPayment(
|
||||
propertyId = propertyId,
|
||||
bookingId = bookingId,
|
||||
paymentId = if (!payment.id.isNullOrBlank()) payment.id else null,
|
||||
razorpayPaymentId = if (payment.id.isNullOrBlank()) payment.gatewayPaymentId else null,
|
||||
amount = amountValue ?: 0L,
|
||||
notes = refundNotes.value
|
||||
)
|
||||
refundTarget.value = null
|
||||
},
|
||||
enabled = canSubmit
|
||||
) {
|
||||
Text("Refund")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { refundTarget.value = null }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentCard(
|
||||
payment: PaymentDto,
|
||||
canDeleteCash: Boolean,
|
||||
onDelete: (String) -> Unit
|
||||
canRefund: Boolean,
|
||||
onDelete: (String) -> Unit,
|
||||
onRefund: (PaymentDto) -> Unit
|
||||
) {
|
||||
val date = payment.receivedAt?.let {
|
||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||
@@ -163,6 +240,11 @@ private fun PaymentCard(
|
||||
payment.currency?.let { append(" $it") }
|
||||
}
|
||||
Text(text = amountText, style = MaterialTheme.typography.titleMedium)
|
||||
if (canRefund && (!payment.id.isNullOrBlank() || !payment.gatewayPaymentId.isNullOrBlank())) {
|
||||
TextButton(onClick = { onRefund(payment) }) {
|
||||
Text("Refund")
|
||||
}
|
||||
}
|
||||
if (canDeleteCash && isCash && !payment.id.isNullOrBlank()) {
|
||||
IconButton(onClick = { onDelete(payment.id) }) {
|
||||
Icon(
|
||||
|
||||
@@ -5,5 +5,6 @@ import com.android.trisolarispms.data.api.model.PaymentDto
|
||||
data class BookingPaymentsState(
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val message: String? = null,
|
||||
val payments: List<PaymentDto> = emptyList()
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.android.trisolarispms.ui.payment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.trisolarispms.data.api.ApiClient
|
||||
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -14,7 +15,7 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
|
||||
fun load(propertyId: String, bookingId: String) {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val response = api.listPayments(propertyId, bookingId)
|
||||
@@ -24,14 +25,16 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
payments = body,
|
||||
error = null
|
||||
error = null,
|
||||
message = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = "Load failed: ${response.code()}"
|
||||
error = "Load failed: ${response.code()}",
|
||||
message = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -39,7 +42,8 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = e.localizedMessage ?: "Load failed"
|
||||
error = e.localizedMessage ?: "Load failed",
|
||||
message = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -48,11 +52,11 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
|
||||
fun addCashPayment(propertyId: String, bookingId: String, amount: Long) {
|
||||
if (amount <= 0) {
|
||||
_state.update { it.copy(error = "Amount must be greater than 0") }
|
||||
_state.update { it.copy(error = "Amount must be greater than 0", message = null) }
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val response = api.createPayment(
|
||||
@@ -66,14 +70,16 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
current.copy(
|
||||
isLoading = false,
|
||||
payments = listOf(body) + current.payments,
|
||||
error = null
|
||||
error = null,
|
||||
message = "Cash payment added"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = "Create failed: ${response.code()}"
|
||||
error = "Create failed: ${response.code()}",
|
||||
message = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +87,8 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = e.localizedMessage ?: "Create failed"
|
||||
error = e.localizedMessage ?: "Create failed",
|
||||
message = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +97,7 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
|
||||
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val response = api.deletePayment(
|
||||
@@ -103,14 +110,16 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
current.copy(
|
||||
isLoading = false,
|
||||
payments = current.payments.filterNot { it.id == paymentId },
|
||||
error = null
|
||||
error = null,
|
||||
message = "Cash payment deleted"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = "Delete failed: ${response.code()}"
|
||||
error = "Delete failed: ${response.code()}",
|
||||
message = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -118,7 +127,69 @@ class BookingPaymentsViewModel : ViewModel() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = e.localizedMessage ?: "Delete failed"
|
||||
error = e.localizedMessage ?: "Delete failed",
|
||||
message = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refundRazorpayPayment(
|
||||
propertyId: String,
|
||||
bookingId: String,
|
||||
paymentId: String?,
|
||||
razorpayPaymentId: String?,
|
||||
amount: Long,
|
||||
notes: String?
|
||||
) {
|
||||
if (amount <= 0) {
|
||||
_state.update { it.copy(error = "Amount must be greater than 0", message = null) }
|
||||
return
|
||||
}
|
||||
if (paymentId.isNullOrBlank() && razorpayPaymentId.isNullOrBlank()) {
|
||||
_state.update { it.copy(error = "Missing payment ID", message = null) }
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val response = api.refundRazorpayPayment(
|
||||
propertyId = propertyId,
|
||||
bookingId = bookingId,
|
||||
body = RazorpayRefundRequest(
|
||||
paymentId = paymentId,
|
||||
razorpayPaymentId = razorpayPaymentId,
|
||||
amount = amount,
|
||||
notes = notes?.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
val body = response.body()
|
||||
if (response.isSuccessful && body != null) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = null,
|
||||
message = "Refund processed"
|
||||
)
|
||||
}
|
||||
load(propertyId, bookingId)
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = "Refund failed: ${response.code()}",
|
||||
message = null
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = e.localizedMessage ?: "Refund failed",
|
||||
message = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,11 +74,12 @@ fun RazorpaySettingsScreen(
|
||||
propertyId = propertyId,
|
||||
state = state,
|
||||
onKeyIdChange = viewModel::onMerchantKeyChange,
|
||||
onKeySecretChange = viewModel::onSalt32Change,
|
||||
onKeySecretChange = viewModel::onKeySecretChange,
|
||||
onWebhookSecretChange = viewModel::onWebhookSecretChange,
|
||||
onKeyIdTestChange = viewModel::onKeyIdTestChange,
|
||||
onKeySecretTestChange = viewModel::onKeySecretTestChange,
|
||||
onWebhookSecretTestChange = viewModel::onWebhookSecretTestChange,
|
||||
onIsTestChange = viewModel::onIsTestChange,
|
||||
onUseSalt256Change = viewModel::onUseSalt256Change,
|
||||
onSalt256Change = viewModel::onSalt256Change,
|
||||
onSave = { viewModel.save(propertyId) },
|
||||
clipboard = clipboard
|
||||
)
|
||||
@@ -93,9 +94,10 @@ private fun RazorpaySettingsTab(
|
||||
onKeyIdChange: (String) -> Unit,
|
||||
onKeySecretChange: (String) -> Unit,
|
||||
onWebhookSecretChange: (String) -> Unit,
|
||||
onKeyIdTestChange: (String) -> Unit,
|
||||
onKeySecretTestChange: (String) -> Unit,
|
||||
onWebhookSecretTestChange: (String) -> Unit,
|
||||
onIsTestChange: (Boolean) -> Unit,
|
||||
onUseSalt256Change: (Boolean) -> Unit,
|
||||
onSalt256Change: (String) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
clipboard: ClipboardManager
|
||||
) {
|
||||
@@ -132,15 +134,12 @@ private fun RazorpaySettingsTab(
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (state.hasKeyId) {
|
||||
Text(text = "Key ID saved", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (state.hasKeySecret) {
|
||||
Text(text = "Key Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (state.hasWebhookSecret) {
|
||||
Text(text = "Webhook Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (state.hasKeyId) Text(text = "Live Key ID saved", style = MaterialTheme.typography.bodySmall)
|
||||
if (state.hasKeySecret) Text(text = "Live Key Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||
if (state.hasWebhookSecret) Text(text = "Live Webhook Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||
if (state.hasKeyIdTest) Text(text = "Test Key ID saved", style = MaterialTheme.typography.bodySmall)
|
||||
if (state.hasKeySecretTest) Text(text = "Test Key Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||
if (state.hasWebhookSecretTest) Text(text = "Test Webhook Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
state.message?.let {
|
||||
@@ -153,22 +152,59 @@ private fun RazorpaySettingsTab(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.keyId,
|
||||
onValueChange = onKeyIdChange,
|
||||
label = { Text("Key ID") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = state.webhookSecret,
|
||||
onValueChange = onWebhookSecretChange,
|
||||
label = { Text("Webhook Secret") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = "Live", style = MaterialTheme.typography.titleSmall)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = state.keyId,
|
||||
onValueChange = onKeyIdChange,
|
||||
label = { Text("Key ID (Live)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = state.keySecret,
|
||||
onValueChange = onKeySecretChange,
|
||||
label = { Text("Key Secret (Live)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = state.webhookSecret,
|
||||
onValueChange = onWebhookSecretChange,
|
||||
label = { Text("Webhook Secret (Live)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(text = "Test", style = MaterialTheme.typography.titleSmall)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = state.keyIdTest,
|
||||
onValueChange = onKeyIdTestChange,
|
||||
label = { Text("Key ID (Test)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = state.keySecretTest,
|
||||
onValueChange = onKeySecretTestChange,
|
||||
label = { Text("Key Secret (Test)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = state.webhookSecretTest,
|
||||
onValueChange = onWebhookSecretTestChange,
|
||||
label = { Text("Webhook Secret (Test)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -183,53 +219,8 @@ private fun RazorpaySettingsTab(
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = "Use secondary secret")
|
||||
Switch(
|
||||
checked = state.useSalt256,
|
||||
onCheckedChange = onUseSalt256Change
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (state.useSalt256) {
|
||||
OutlinedTextField(
|
||||
value = state.salt256,
|
||||
onValueChange = onSalt256Change,
|
||||
label = { Text("Secret (secondary)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
if (state.hasSalt256) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Salt256 saved",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
} else {
|
||||
OutlinedTextField(
|
||||
value = state.keySecret,
|
||||
onValueChange = onKeySecretChange,
|
||||
label = { Text("Key Secret") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
if (state.hasKeySecret) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Key Secret saved",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = onSave,
|
||||
enabled = !state.isSaving,
|
||||
|
||||
@@ -4,13 +4,16 @@ data class RazorpaySettingsState(
|
||||
val keyId: String = "",
|
||||
val keySecret: String = "",
|
||||
val webhookSecret: String = "",
|
||||
val salt256: String = "",
|
||||
val keyIdTest: String = "",
|
||||
val keySecretTest: String = "",
|
||||
val webhookSecretTest: String = "",
|
||||
val isTest: Boolean = false,
|
||||
val useSalt256: Boolean = false,
|
||||
val hasKeyId: Boolean = false,
|
||||
val hasKeySecret: Boolean = false,
|
||||
val hasWebhookSecret: Boolean = false,
|
||||
val hasSalt256: Boolean = false,
|
||||
val hasKeyIdTest: Boolean = false,
|
||||
val hasKeySecretTest: Boolean = false,
|
||||
val hasWebhookSecretTest: Boolean = false,
|
||||
val configured: Boolean = false,
|
||||
val isLoading: Boolean = false,
|
||||
val isSaving: Boolean = false,
|
||||
|
||||
@@ -23,7 +23,7 @@ class RazorpaySettingsViewModel : ViewModel() {
|
||||
_state.update { it.copy(keyId = value, error = null) }
|
||||
}
|
||||
|
||||
fun onSalt32Change(value: String) {
|
||||
fun onKeySecretChange(value: String) {
|
||||
_state.update { it.copy(keySecret = value, error = null) }
|
||||
}
|
||||
|
||||
@@ -31,37 +31,51 @@ class RazorpaySettingsViewModel : ViewModel() {
|
||||
_state.update { it.copy(webhookSecret = value, error = null) }
|
||||
}
|
||||
|
||||
fun onSalt256Change(value: String) {
|
||||
_state.update { it.copy(salt256 = value, error = null) }
|
||||
fun onKeyIdTestChange(value: String) {
|
||||
_state.update { it.copy(keyIdTest = value, error = null) }
|
||||
}
|
||||
|
||||
fun onKeySecretTestChange(value: String) {
|
||||
_state.update { it.copy(keySecretTest = value, error = null) }
|
||||
}
|
||||
|
||||
fun onWebhookSecretTestChange(value: String) {
|
||||
_state.update { it.copy(webhookSecretTest = value, error = null) }
|
||||
}
|
||||
|
||||
fun onIsTestChange(value: Boolean) {
|
||||
_state.update { it.copy(isTest = value, error = null) }
|
||||
}
|
||||
|
||||
fun onUseSalt256Change(value: Boolean) {
|
||||
_state.update { it.copy(useSalt256 = value, error = null) }
|
||||
}
|
||||
|
||||
fun save(propertyId: String) {
|
||||
val current = state.value
|
||||
val keyId = current.keyId.trim()
|
||||
val keySecret = current.keySecret.trim()
|
||||
val webhookSecret = current.webhookSecret.trim()
|
||||
val salt256 = current.salt256.trim()
|
||||
val keyIdTest = current.keyIdTest.trim()
|
||||
val keySecretTest = current.keySecretTest.trim()
|
||||
val webhookSecretTest = current.webhookSecretTest.trim()
|
||||
|
||||
if (!current.configured) {
|
||||
if (keyId.isBlank()) {
|
||||
_state.update { it.copy(error = "Key ID is required") }
|
||||
return
|
||||
}
|
||||
if (current.useSalt256 && salt256.isBlank() && !current.hasSalt256) {
|
||||
_state.update { it.copy(error = "Secondary secret is required") }
|
||||
return
|
||||
}
|
||||
if (!current.useSalt256 && keySecret.isBlank() && !current.hasKeySecret) {
|
||||
_state.update { it.copy(error = "Key secret is required") }
|
||||
return
|
||||
val needsTest = current.isTest
|
||||
if (needsTest) {
|
||||
if (keyIdTest.isBlank() && !current.hasKeyIdTest) {
|
||||
_state.update { it.copy(error = "Test Key ID is required") }
|
||||
return
|
||||
}
|
||||
if (keySecretTest.isBlank() && !current.hasKeySecretTest) {
|
||||
_state.update { it.copy(error = "Test Key Secret is required") }
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (keyId.isBlank() && !current.hasKeyId) {
|
||||
_state.update { it.copy(error = "Key ID is required") }
|
||||
return
|
||||
}
|
||||
if (keySecret.isBlank() && !current.hasKeySecret) {
|
||||
_state.update { it.copy(error = "Key Secret is required") }
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val hasKeyIdInput = keyId.isNotBlank()
|
||||
@@ -70,6 +84,12 @@ class RazorpaySettingsViewModel : ViewModel() {
|
||||
_state.update { it.copy(error = "Key ID and Key Secret must be provided together") }
|
||||
return
|
||||
}
|
||||
val hasKeyIdTestInput = keyIdTest.isNotBlank()
|
||||
val hasKeySecretTestInput = keySecretTest.isNotBlank()
|
||||
if (hasKeyIdTestInput xor hasKeySecretTestInput) {
|
||||
_state.update { it.copy(error = "Test Key ID and Test Key Secret must be provided together") }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
@@ -79,11 +99,13 @@ class RazorpaySettingsViewModel : ViewModel() {
|
||||
val response = api.updateRazorpaySettings(
|
||||
propertyId = propertyId,
|
||||
body = RazorpaySettingsRequest(
|
||||
keyId = keyId,
|
||||
keyId = keyId.ifBlank { null },
|
||||
keySecret = keySecret.ifBlank { null },
|
||||
webhookSecret = webhookSecret.ifBlank { null },
|
||||
isTest = current.isTest,
|
||||
useSalt256 = current.useSalt256
|
||||
keyIdTest = keyIdTest.ifBlank { null },
|
||||
keySecretTest = keySecretTest.ifBlank { null },
|
||||
webhookSecretTest = webhookSecretTest.ifBlank { null },
|
||||
isTest = current.isTest
|
||||
)
|
||||
)
|
||||
if (response.isSuccessful) {
|
||||
@@ -124,15 +146,18 @@ class RazorpaySettingsViewModel : ViewModel() {
|
||||
it.copy(
|
||||
keyId = "",
|
||||
isTest = body.isTest == true,
|
||||
useSalt256 = body.useSalt256 == true,
|
||||
hasKeyId = body.hasKeyId == true,
|
||||
hasKeySecret = body.hasKeySecret == true,
|
||||
hasWebhookSecret = body.hasWebhookSecret == true,
|
||||
hasSalt256 = body.hasSalt256 == true,
|
||||
hasKeyIdTest = body.hasKeyIdTest == true,
|
||||
hasKeySecretTest = body.hasKeySecretTest == true,
|
||||
hasWebhookSecretTest = body.hasWebhookSecretTest == true,
|
||||
configured = body.configured == true,
|
||||
keySecret = "",
|
||||
webhookSecret = "",
|
||||
salt256 = "",
|
||||
keyIdTest = "",
|
||||
keySecretTest = "",
|
||||
webhookSecretTest = "",
|
||||
isLoading = false,
|
||||
isSaving = false,
|
||||
error = null
|
||||
|
||||
Reference in New Issue
Block a user