razorpay: add payment link support

This commit is contained in:
androidlover5842
2026-02-01 14:37:05 +05:30
parent 8f62459d5e
commit 4c31a20af4
5 changed files with 103 additions and 38 deletions

View File

@@ -15,7 +15,7 @@ import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
import com.android.trisolarispms.data.api.model.BookingDetailsResponse import com.android.trisolarispms.data.api.model.BookingDetailsResponse
import com.android.trisolarispms.data.api.model.RoomStayDto import com.android.trisolarispms.data.api.model.RoomStayDto
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
import com.android.trisolarispms.data.api.model.RazorpayQrListItemDto import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
import com.android.trisolarispms.data.api.model.RazorpayQrRequest import com.android.trisolarispms.data.api.model.RazorpayQrRequest
import com.android.trisolarispms.data.api.model.RazorpayQrResponse import com.android.trisolarispms.data.api.model.RazorpayQrResponse
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
@@ -139,17 +139,17 @@ interface BookingApi {
@Path("paymentId") paymentId: String @Path("paymentId") paymentId: String
): Response<Unit> ): Response<Unit>
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr") @GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/requests")
suspend fun listRazorpayQrs( suspend fun listRazorpayRequests(
@Path("propertyId") propertyId: String, @Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String @Path("bookingId") bookingId: String
): Response<List<RazorpayQrListItemDto>> ): Response<List<RazorpayRequestListItemDto>>
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/close") @POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/close")
suspend fun closeRazorpayQr( suspend fun closeRazorpayRequest(
@Path("propertyId") propertyId: String, @Path("propertyId") propertyId: String,
@Path("bookingId") bookingId: String, @Path("bookingId") bookingId: String,
@Path("qrId") qrId: String @Body body: com.android.trisolarispms.data.api.model.RazorpayCloseRequest
): Response<Unit> ): Response<Unit>
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events") @GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events")

View File

@@ -24,6 +24,11 @@ data class RazorpayPaymentLinkResponse(
val paymentLink: String? = null val paymentLink: String? = null
) )
data class RazorpayCloseRequest(
val qrId: String? = null,
val paymentLinkId: String? = null
)
data class RazorpayQrEventDto( data class RazorpayQrEventDto(
val event: String? = null, val event: String? = null,
val qrId: String? = null, val qrId: String? = null,
@@ -31,12 +36,16 @@ data class RazorpayQrEventDto(
val receivedAt: String? = null val receivedAt: String? = null
) )
data class RazorpayQrListItemDto( data class RazorpayRequestListItemDto(
val qrId: String? = null, val type: String? = null,
val requestId: String? = null,
val amount: Long? = null, val amount: Long? = null,
val currency: String? = null, val currency: String? = null,
val status: String? = null, val status: String? = null,
val createdAt: String? = null,
val qrId: String? = null,
val imageUrl: String? = null, val imageUrl: String? = null,
val expiryAt: String? = null, val expiryAt: String? = null,
val createdAt: String? = null val paymentLinkId: String? = null,
val paymentLink: String? = null
) )

View File

@@ -29,6 +29,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TextButton
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
@@ -45,6 +46,9 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import com.android.trisolarispms.BuildConfig import com.android.trisolarispms.BuildConfig
import androidx.compose.ui.platform.LocalContext
import android.content.Intent
import android.net.Uri
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -57,6 +61,7 @@ fun RazorpayQrScreen(
viewModel: RazorpayQrViewModel = viewModel() viewModel: RazorpayQrViewModel = viewModel()
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val context = LocalContext.current
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { viewModel.reset() } onDispose { viewModel.reset() }
@@ -205,6 +210,18 @@ fun RazorpayQrScreen(
Text("Generate QR") Text("Generate QR")
} }
} }
Spacer(modifier = Modifier.height(8.dp))
TextButton(
onClick = { viewModel.generateLink(propertyId, bookingId) },
enabled = !state.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (state.isLoading) {
Text("Generating link…")
} else {
Text("Generate payment link")
}
}
state.error?.let { state.error?.let {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@@ -213,13 +230,19 @@ fun RazorpayQrScreen(
if (state.qrList.isNotEmpty()) { if (state.qrList.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text(text = "QR List", style = MaterialTheme.typography.titleMedium) Text(text = "Requests", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
state.qrList.forEachIndexed { index, item -> state.qrList.forEachIndexed { index, item ->
val status = item.status?.lowercase().orEmpty()
val isInactive = status == "closed" || status == "expired" || status == "credited" || status == "cancelled" || status == "canceled"
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { viewModel.openQrFromList(item) }, .clickable(
enabled = !isInactive &&
item.type?.equals("QR", ignoreCase = true) == true &&
!item.imageUrl.isNullOrBlank()
) { viewModel.openQrFromList(item) },
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant containerColor = MaterialTheme.colorScheme.surfaceVariant
) )
@@ -230,23 +253,58 @@ fun RazorpayQrScreen(
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Column {
text = "QR ${index + 1}", Text(
style = MaterialTheme.typography.labelMedium, text = when (item.type?.uppercase()) {
color = MaterialTheme.colorScheme.onSurfaceVariant "PAYMENT_LINK" -> "Payment Link"
) else -> "QR"
} + " ${index + 1}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
item.status?.let { status ->
Text(
text = status.replaceFirstChar { it.uppercase() },
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
text = "${item.amount ?: "-"} ${item.currency.orEmpty()}", text = "${item.amount ?: "-"} ${item.currency.orEmpty()}",
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
val qrId = item.qrId if (item.type?.equals("PAYMENT_LINK", ignoreCase = true) == true &&
if (!qrId.isNullOrBlank()) { !item.paymentLink.isNullOrBlank() &&
!isInactive
) {
Spacer(modifier = Modifier.width(8.dp))
TextButton(
onClick = {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, item.paymentLink)
if (!guestPhone.isNullOrBlank()) {
putExtra("address", guestPhone)
putExtra(Intent.EXTRA_PHONE_NUMBER, guestPhone)
}
}
context.startActivity(
Intent.createChooser(intent, "Share payment link")
)
}
) {
Text("Share")
}
}
if (!item.qrId.isNullOrBlank() || !item.paymentLinkId.isNullOrBlank()) {
IconButton( IconButton(
onClick = { onClick = {
viewModel.closeQr(propertyId, bookingId, qrId) viewModel.closeRequest(propertyId, bookingId, item)
}, },
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp),
enabled = !isInactive
) { ) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,

View File

@@ -12,5 +12,5 @@ data class RazorpayQrState(
val isClosed: Boolean = false, val isClosed: Boolean = false,
val isCredited: Boolean = false, val isCredited: Boolean = false,
val paymentLink: String? = null, val paymentLink: String? = null,
val qrList: List<com.android.trisolarispms.data.api.model.RazorpayQrListItemDto> = emptyList() val qrList: List<com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto> = emptyList()
) )

View File

@@ -4,8 +4,9 @@ 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.ApiConstants import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.model.RazorpayCloseRequest
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
import com.android.trisolarispms.data.api.model.RazorpayQrListItemDto import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
import com.android.trisolarispms.data.api.model.RazorpayQrRequest import com.android.trisolarispms.data.api.model.RazorpayQrRequest
import com.google.gson.Gson import com.google.gson.Gson
@@ -232,16 +233,10 @@ class RazorpayQrViewModel : ViewModel() {
fun loadQrList(propertyId: String, bookingId: String) { fun loadQrList(propertyId: String, bookingId: String) {
viewModelScope.launch { viewModelScope.launch {
try { try {
val response = ApiClient.create().listRazorpayQrs(propertyId, bookingId) val response = ApiClient.create().listRazorpayRequests(propertyId, bookingId)
val body = response.body() val body = response.body()
if (response.isSuccessful && body != null) { if (response.isSuccessful && body != null) {
val filtered = body.filterNot { item -> _state.update { it.copy(qrList = body) }
when (item.status?.lowercase()) {
"closed", "expired", "credited" -> true
else -> false
}
}
_state.update { it.copy(qrList = filtered) }
} }
} catch (_: Exception) { } catch (_: Exception) {
// ignore list load errors // ignore list load errors
@@ -249,17 +244,20 @@ class RazorpayQrViewModel : ViewModel() {
} }
} }
fun closeQr(propertyId: String, bookingId: String, qrId: String) { fun closeRequest(propertyId: String, bookingId: String, item: RazorpayRequestListItemDto) {
val qrId = item.qrId
val paymentLinkId = item.paymentLinkId
if (qrId.isNullOrBlank() && paymentLinkId.isNullOrBlank()) return
viewModelScope.launch { viewModelScope.launch {
try { try {
val response = ApiClient.create().closeRazorpayQr( val response = ApiClient.create().closeRazorpayRequest(
propertyId = propertyId, propertyId = propertyId,
bookingId = bookingId, bookingId = bookingId,
qrId = qrId body = RazorpayCloseRequest(qrId = qrId, paymentLinkId = paymentLinkId)
) )
if (response.isSuccessful) { if (response.isSuccessful) {
_state.update { current -> _state.update { current ->
current.copy(qrList = current.qrList.filterNot { it.qrId == qrId }) current.copy(qrList = current.qrList.filterNot { it.requestId == item.requestId })
} }
loadQrList(propertyId, bookingId) loadQrList(propertyId, bookingId)
} }
@@ -269,7 +267,7 @@ class RazorpayQrViewModel : ViewModel() {
} }
} }
fun openQrFromList(item: RazorpayQrListItemDto) { fun openQrFromList(item: RazorpayRequestListItemDto) {
val status = item.status?.lowercase() val status = item.status?.lowercase()
_state.update { _state.update {
it.copy( it.copy(
@@ -288,7 +286,7 @@ class RazorpayQrViewModel : ViewModel() {
startQrEventStream(propertyId, bookingId, state.value.qrId) startQrEventStream(propertyId, bookingId, state.value.qrId)
} }
fun generateLink(propertyId: String, bookingId: String, onReady: (String) -> Unit) { fun generateLink(propertyId: String, bookingId: String) {
val current = state.value val current = state.value
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 } val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
if (amount == null) { if (amount == null) {
@@ -315,7 +313,7 @@ class RazorpayQrViewModel : ViewModel() {
error = null error = null
) )
} }
onReady(body.paymentLink) loadQrList(propertyId, bookingId)
} else { } else {
_state.update { _state.update {
it.copy( it.copy(