payments: ability to refund

This commit is contained in:
androidlover5842
2026-02-01 16:16:44 +05:30
parent 4c31a20af4
commit 3219e40a02
10 changed files with 323 additions and 125 deletions

View File

@@ -491,6 +491,7 @@ class MainActivity : ComponentActivity() {
bookingId = currentRoute.bookingId, bookingId = currentRoute.bookingId,
canAddCash = canManageRazorpaySettings(currentRoute.propertyId), canAddCash = canManageRazorpaySettings(currentRoute.propertyId),
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId), canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
canRefund = canManageRazorpaySettings(currentRoute.propertyId),
onBack = { onBack = {
route.value = AppRoute.BookingDetailsTabs( route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId, currentRoute.propertyId,

View File

@@ -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.RazorpayPaymentLinkResponse
import com.android.trisolarispms.data.api.model.PaymentDto import com.android.trisolarispms.data.api.model.PaymentDto
import com.android.trisolarispms.data.api.model.PaymentCreateRequest 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.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
@@ -132,6 +134,13 @@ interface BookingApi {
@Body body: PaymentCreateRequest @Body body: PaymentCreateRequest
): Response<PaymentDto> ): 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}") @DELETE("properties/{propertyId}/bookings/{bookingId}/payments/{paymentId}")
suspend fun deletePayment( suspend fun deletePayment(
@Path("propertyId") propertyId: String, @Path("propertyId") propertyId: String,

View File

@@ -23,3 +23,17 @@ data class PaymentDto(
data class PaymentCreateRequest( data class PaymentCreateRequest(
val amount: Long 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
)

View File

@@ -1,22 +1,23 @@
package com.android.trisolarispms.data.api.model package com.android.trisolarispms.data.api.model
import com.google.gson.annotations.SerializedName
data class RazorpaySettingsRequest( data class RazorpaySettingsRequest(
val keyId: String, val keyId: String? = null,
val keySecret: String? = null, val keySecret: String? = null,
val webhookSecret: String? = null, val webhookSecret: String? = null,
@SerializedName("isTest") val isTest: Boolean, val keyIdTest: String? = null,
val useSalt256: Boolean val keySecretTest: String? = null,
val webhookSecretTest: String? = null,
val isTest: Boolean
) )
data class RazorpaySettingsResponse( data class RazorpaySettingsResponse(
val propertyId: String? = null, val propertyId: String? = null,
val configured: Boolean? = null, val configured: Boolean? = null,
@SerializedName("test") val isTest: Boolean? = null, @com.google.gson.annotations.SerializedName("test") val isTest: Boolean? = null,
val useSalt256: Boolean? = null,
val hasKeyId: Boolean? = null, val hasKeyId: Boolean? = null,
val hasKeySecret: Boolean? = null, val hasKeySecret: Boolean? = null,
val hasWebhookSecret: Boolean? = null, val hasWebhookSecret: Boolean? = null,
val hasSalt256: Boolean? = null val hasKeyIdTest: Boolean? = null,
val hasKeySecretTest: Boolean? = null,
val hasWebhookSecretTest: Boolean? = null
) )

View File

@@ -25,14 +25,17 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@@ -49,11 +52,15 @@ fun BookingPaymentsScreen(
bookingId: String, bookingId: String,
canAddCash: Boolean, canAddCash: Boolean,
canDeleteCash: Boolean, canDeleteCash: Boolean,
canRefund: Boolean,
onBack: () -> Unit, onBack: () -> Unit,
viewModel: BookingPaymentsViewModel = viewModel() viewModel: BookingPaymentsViewModel = viewModel()
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val amountInput = remember { mutableStateOf("") } val amountInput = remember { mutableStateOf("") }
val refundTarget = remember { mutableStateOf<PaymentDto?>(null) }
val refundAmount = rememberSaveable { mutableStateOf("") }
val refundNotes = rememberSaveable { mutableStateOf("") }
LaunchedEffect(propertyId, bookingId) { LaunchedEffect(propertyId, bookingId) {
viewModel.load(propertyId, bookingId) viewModel.load(propertyId, bookingId)
@@ -108,6 +115,10 @@ fun BookingPaymentsScreen(
Text(text = it, color = MaterialTheme.colorScheme.error) Text(text = it, color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(8.dp)) 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()) { if (!state.isLoading && state.error == null && state.payments.isEmpty()) {
Text(text = "No payments yet") Text(text = "No payments yet")
} }
@@ -121,21 +132,87 @@ fun BookingPaymentsScreen(
PaymentCard( PaymentCard(
payment = payment, payment = payment,
canDeleteCash = canDeleteCash, canDeleteCash = canDeleteCash,
canRefund = canRefund,
onDelete = { paymentId -> onDelete = { paymentId ->
viewModel.deleteCashPayment(propertyId, bookingId, 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 @Composable
private fun PaymentCard( private fun PaymentCard(
payment: PaymentDto, payment: PaymentDto,
canDeleteCash: Boolean, canDeleteCash: Boolean,
onDelete: (String) -> Unit canRefund: Boolean,
onDelete: (String) -> Unit,
onRefund: (PaymentDto) -> Unit
) { ) {
val date = payment.receivedAt?.let { val date = payment.receivedAt?.let {
runCatching { OffsetDateTime.parse(it) }.getOrNull() runCatching { OffsetDateTime.parse(it) }.getOrNull()
@@ -163,6 +240,11 @@ private fun PaymentCard(
payment.currency?.let { append(" $it") } payment.currency?.let { append(" $it") }
} }
Text(text = amountText, style = MaterialTheme.typography.titleMedium) 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()) { if (canDeleteCash && isCash && !payment.id.isNullOrBlank()) {
IconButton(onClick = { onDelete(payment.id) }) { IconButton(onClick = { onDelete(payment.id) }) {
Icon( Icon(

View File

@@ -5,5 +5,6 @@ import com.android.trisolarispms.data.api.model.PaymentDto
data class BookingPaymentsState( data class BookingPaymentsState(
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null, val error: String? = null,
val message: String? = null,
val payments: List<PaymentDto> = emptyList() val payments: List<PaymentDto> = emptyList()
) )

View File

@@ -3,6 +3,7 @@ package com.android.trisolarispms.ui.payment
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -14,7 +15,7 @@ class BookingPaymentsViewModel : ViewModel() {
fun load(propertyId: String, bookingId: String) { fun load(propertyId: String, bookingId: String) {
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null, message = null) }
try { try {
val api = ApiClient.create() val api = ApiClient.create()
val response = api.listPayments(propertyId, bookingId) val response = api.listPayments(propertyId, bookingId)
@@ -24,14 +25,16 @@ class BookingPaymentsViewModel : ViewModel() {
it.copy( it.copy(
isLoading = false, isLoading = false,
payments = body, payments = body,
error = null error = null,
message = null
) )
} }
} else { } else {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
error = "Load failed: ${response.code()}" error = "Load failed: ${response.code()}",
message = null
) )
} }
} }
@@ -39,7 +42,8 @@ class BookingPaymentsViewModel : ViewModel() {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, 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) { fun addCashPayment(propertyId: String, bookingId: String, amount: Long) {
if (amount <= 0) { 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 return
} }
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null, message = null) }
try { try {
val api = ApiClient.create() val api = ApiClient.create()
val response = api.createPayment( val response = api.createPayment(
@@ -66,14 +70,16 @@ class BookingPaymentsViewModel : ViewModel() {
current.copy( current.copy(
isLoading = false, isLoading = false,
payments = listOf(body) + current.payments, payments = listOf(body) + current.payments,
error = null error = null,
message = "Cash payment added"
) )
} }
} else { } else {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
error = "Create failed: ${response.code()}" error = "Create failed: ${response.code()}",
message = null
) )
} }
} }
@@ -81,7 +87,8 @@ class BookingPaymentsViewModel : ViewModel() {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, 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) { fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null, message = null) }
try { try {
val api = ApiClient.create() val api = ApiClient.create()
val response = api.deletePayment( val response = api.deletePayment(
@@ -103,14 +110,16 @@ class BookingPaymentsViewModel : ViewModel() {
current.copy( current.copy(
isLoading = false, isLoading = false,
payments = current.payments.filterNot { it.id == paymentId }, payments = current.payments.filterNot { it.id == paymentId },
error = null error = null,
message = "Cash payment deleted"
) )
} }
} else { } else {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
error = "Delete failed: ${response.code()}" error = "Delete failed: ${response.code()}",
message = null
) )
} }
} }
@@ -118,7 +127,69 @@ class BookingPaymentsViewModel : ViewModel() {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, 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
) )
} }
} }

View File

@@ -74,11 +74,12 @@ fun RazorpaySettingsScreen(
propertyId = propertyId, propertyId = propertyId,
state = state, state = state,
onKeyIdChange = viewModel::onMerchantKeyChange, onKeyIdChange = viewModel::onMerchantKeyChange,
onKeySecretChange = viewModel::onSalt32Change, onKeySecretChange = viewModel::onKeySecretChange,
onWebhookSecretChange = viewModel::onWebhookSecretChange, onWebhookSecretChange = viewModel::onWebhookSecretChange,
onKeyIdTestChange = viewModel::onKeyIdTestChange,
onKeySecretTestChange = viewModel::onKeySecretTestChange,
onWebhookSecretTestChange = viewModel::onWebhookSecretTestChange,
onIsTestChange = viewModel::onIsTestChange, onIsTestChange = viewModel::onIsTestChange,
onUseSalt256Change = viewModel::onUseSalt256Change,
onSalt256Change = viewModel::onSalt256Change,
onSave = { viewModel.save(propertyId) }, onSave = { viewModel.save(propertyId) },
clipboard = clipboard clipboard = clipboard
) )
@@ -93,9 +94,10 @@ private fun RazorpaySettingsTab(
onKeyIdChange: (String) -> Unit, onKeyIdChange: (String) -> Unit,
onKeySecretChange: (String) -> Unit, onKeySecretChange: (String) -> Unit,
onWebhookSecretChange: (String) -> Unit, onWebhookSecretChange: (String) -> Unit,
onKeyIdTestChange: (String) -> Unit,
onKeySecretTestChange: (String) -> Unit,
onWebhookSecretTestChange: (String) -> Unit,
onIsTestChange: (Boolean) -> Unit, onIsTestChange: (Boolean) -> Unit,
onUseSalt256Change: (Boolean) -> Unit,
onSalt256Change: (String) -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
clipboard: ClipboardManager clipboard: ClipboardManager
) { ) {
@@ -132,15 +134,12 @@ private fun RazorpaySettingsTab(
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
if (state.hasKeyId) { if (state.hasKeyId) Text(text = "Live Key ID saved", style = MaterialTheme.typography.bodySmall)
Text(text = "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.hasKeySecret) { if (state.hasKeyIdTest) Text(text = "Test Key ID saved", style = MaterialTheme.typography.bodySmall)
Text(text = "Key Secret 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)
if (state.hasWebhookSecret) {
Text(text = "Webhook Secret saved", style = MaterialTheme.typography.bodySmall)
}
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
state.message?.let { state.message?.let {
@@ -153,22 +152,59 @@ private fun RazorpaySettingsTab(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
OutlinedTextField( Text(text = "Live", style = MaterialTheme.typography.titleSmall)
value = state.keyId, Spacer(modifier = Modifier.height(8.dp))
onValueChange = onKeyIdChange, OutlinedTextField(
label = { Text("Key ID") }, value = state.keyId,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii), onValueChange = onKeyIdChange,
modifier = Modifier.fillMaxWidth() label = { Text("Key ID (Live)") },
) keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
Spacer(modifier = Modifier.height(12.dp)) modifier = Modifier.fillMaxWidth()
OutlinedTextField( )
value = state.webhookSecret, Spacer(modifier = Modifier.height(12.dp))
onValueChange = onWebhookSecretChange, OutlinedTextField(
label = { Text("Webhook Secret") }, value = state.keySecret,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii), onValueChange = onKeySecretChange,
modifier = Modifier.fillMaxWidth() label = { Text("Key Secret (Live)") },
) keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
Spacer(modifier = Modifier.height(12.dp)) 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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -183,53 +219,8 @@ private fun RazorpaySettingsTab(
} }
Spacer(modifier = Modifier.height(12.dp)) 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)) 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( Button(
onClick = onSave, onClick = onSave,
enabled = !state.isSaving, enabled = !state.isSaving,

View File

@@ -4,13 +4,16 @@ data class RazorpaySettingsState(
val keyId: String = "", val keyId: String = "",
val keySecret: String = "", val keySecret: String = "",
val webhookSecret: String = "", val webhookSecret: String = "",
val salt256: String = "", val keyIdTest: String = "",
val keySecretTest: String = "",
val webhookSecretTest: String = "",
val isTest: Boolean = false, val isTest: Boolean = false,
val useSalt256: Boolean = false,
val hasKeyId: Boolean = false, val hasKeyId: Boolean = false,
val hasKeySecret: Boolean = false, val hasKeySecret: Boolean = false,
val hasWebhookSecret: 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 configured: Boolean = false,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val isSaving: Boolean = false, val isSaving: Boolean = false,

View File

@@ -23,7 +23,7 @@ class RazorpaySettingsViewModel : ViewModel() {
_state.update { it.copy(keyId = value, error = null) } _state.update { it.copy(keyId = value, error = null) }
} }
fun onSalt32Change(value: String) { fun onKeySecretChange(value: String) {
_state.update { it.copy(keySecret = value, error = null) } _state.update { it.copy(keySecret = value, error = null) }
} }
@@ -31,37 +31,51 @@ class RazorpaySettingsViewModel : ViewModel() {
_state.update { it.copy(webhookSecret = value, error = null) } _state.update { it.copy(webhookSecret = value, error = null) }
} }
fun onSalt256Change(value: String) { fun onKeyIdTestChange(value: String) {
_state.update { it.copy(salt256 = value, error = null) } _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) { fun onIsTestChange(value: Boolean) {
_state.update { it.copy(isTest = value, error = null) } _state.update { it.copy(isTest = value, error = null) }
} }
fun onUseSalt256Change(value: Boolean) {
_state.update { it.copy(useSalt256 = value, error = null) }
}
fun save(propertyId: String) { fun save(propertyId: String) {
val current = state.value val current = state.value
val keyId = current.keyId.trim() val keyId = current.keyId.trim()
val keySecret = current.keySecret.trim() val keySecret = current.keySecret.trim()
val webhookSecret = current.webhookSecret.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 (!current.configured) {
if (keyId.isBlank()) { val needsTest = current.isTest
_state.update { it.copy(error = "Key ID is required") } if (needsTest) {
return if (keyIdTest.isBlank() && !current.hasKeyIdTest) {
} _state.update { it.copy(error = "Test Key ID is required") }
if (current.useSalt256 && salt256.isBlank() && !current.hasSalt256) { return
_state.update { it.copy(error = "Secondary secret is required") } }
return if (keySecretTest.isBlank() && !current.hasKeySecretTest) {
} _state.update { it.copy(error = "Test Key Secret is required") }
if (!current.useSalt256 && keySecret.isBlank() && !current.hasKeySecret) { return
_state.update { it.copy(error = "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 { } else {
val hasKeyIdInput = keyId.isNotBlank() 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") } _state.update { it.copy(error = "Key ID and Key Secret must be provided together") }
return 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 { viewModelScope.launch {
@@ -79,11 +99,13 @@ class RazorpaySettingsViewModel : ViewModel() {
val response = api.updateRazorpaySettings( val response = api.updateRazorpaySettings(
propertyId = propertyId, propertyId = propertyId,
body = RazorpaySettingsRequest( body = RazorpaySettingsRequest(
keyId = keyId, keyId = keyId.ifBlank { null },
keySecret = keySecret.ifBlank { null }, keySecret = keySecret.ifBlank { null },
webhookSecret = webhookSecret.ifBlank { null }, webhookSecret = webhookSecret.ifBlank { null },
isTest = current.isTest, keyIdTest = keyIdTest.ifBlank { null },
useSalt256 = current.useSalt256 keySecretTest = keySecretTest.ifBlank { null },
webhookSecretTest = webhookSecretTest.ifBlank { null },
isTest = current.isTest
) )
) )
if (response.isSuccessful) { if (response.isSuccessful) {
@@ -124,15 +146,18 @@ class RazorpaySettingsViewModel : ViewModel() {
it.copy( it.copy(
keyId = "", keyId = "",
isTest = body.isTest == true, isTest = body.isTest == true,
useSalt256 = body.useSalt256 == true,
hasKeyId = body.hasKeyId == true, hasKeyId = body.hasKeyId == true,
hasKeySecret = body.hasKeySecret == true, hasKeySecret = body.hasKeySecret == true,
hasWebhookSecret = body.hasWebhookSecret == 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, configured = body.configured == true,
keySecret = "", keySecret = "",
webhookSecret = "", webhookSecret = "",
salt256 = "", keyIdTest = "",
keySecretTest = "",
webhookSecretTest = "",
isLoading = false, isLoading = false,
isSaving = false, isSaving = false,
error = null error = null