remove that crap payu
This commit is contained in:
@@ -36,8 +36,8 @@ import com.android.trisolarispms.ui.roomimage.ImageTagsScreen
|
|||||||
import com.android.trisolarispms.ui.roomimage.AddImageTagScreen
|
import com.android.trisolarispms.ui.roomimage.AddImageTagScreen
|
||||||
import com.android.trisolarispms.ui.roomimage.EditImageTagScreen
|
import com.android.trisolarispms.ui.roomimage.EditImageTagScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
|
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
|
||||||
import com.android.trisolarispms.ui.payu.PayuSettingsScreen
|
import com.android.trisolarispms.ui.razorpay.RazorpaySettingsScreen
|
||||||
import com.android.trisolarispms.ui.payu.PayuQrScreen
|
import com.android.trisolarispms.ui.razorpay.RazorpayQrScreen
|
||||||
import com.android.trisolarispms.ui.roomtype.AddAmenityScreen
|
import com.android.trisolarispms.ui.roomtype.AddAmenityScreen
|
||||||
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
|
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
|
||||||
import com.android.trisolarispms.ui.roomtype.AmenitiesScreen
|
import com.android.trisolarispms.ui.roomtype.AmenitiesScreen
|
||||||
@@ -86,7 +86,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
it == "ADMIN" || it == "MANAGER" || it == "STAFF"
|
it == "ADMIN" || it == "MANAGER" || it == "STAFF"
|
||||||
} == true
|
} == true
|
||||||
}
|
}
|
||||||
val canManagePayuSettings: (String) -> Boolean = { propertyId ->
|
val canManageRazorpaySettings: (String) -> Boolean = { propertyId ->
|
||||||
state.isSuperAdmin || state.propertyRoles[propertyId]?.any {
|
state.isSuperAdmin || state.propertyRoles[propertyId]?.any {
|
||||||
it == "ADMIN" || it == "MANAGER"
|
it == "ADMIN" || it == "MANAGER"
|
||||||
} == true
|
} == true
|
||||||
@@ -125,11 +125,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
currentRoute.roomTypeId
|
currentRoute.roomTypeId
|
||||||
)
|
)
|
||||||
is AppRoute.PayuSettings -> route.value = AppRoute.ActiveRoomStays(
|
is AppRoute.RazorpaySettings -> route.value = AppRoute.ActiveRoomStays(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
selectedPropertyName.value ?: "Property"
|
selectedPropertyName.value ?: "Property"
|
||||||
)
|
)
|
||||||
is AppRoute.PayuQr -> route.value = AppRoute.BookingDetailsTabs(
|
is AppRoute.RazorpayQr -> route.value = AppRoute.BookingDetailsTabs(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
currentRoute.bookingId,
|
currentRoute.bookingId,
|
||||||
null
|
null
|
||||||
@@ -278,8 +278,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
onBack = { route.value = AppRoute.Home },
|
onBack = { route.value = AppRoute.Home },
|
||||||
onViewRooms = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
onViewRooms = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
||||||
onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
|
onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
|
||||||
showPayuSettings = canManagePayuSettings(currentRoute.propertyId),
|
showRazorpaySettings = canManageRazorpaySettings(currentRoute.propertyId),
|
||||||
onPayuSettings = { route.value = AppRoute.PayuSettings(currentRoute.propertyId) },
|
onRazorpaySettings = { route.value = AppRoute.RazorpaySettings(currentRoute.propertyId) },
|
||||||
onManageRoomStay = { booking ->
|
onManageRoomStay = { booking ->
|
||||||
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
|
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
|
||||||
?: booking.expectedCheckInAt.orEmpty()
|
?: booking.expectedCheckInAt.orEmpty()
|
||||||
@@ -308,7 +308,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
is AppRoute.PayuSettings -> PayuSettingsScreen(
|
is AppRoute.RazorpaySettings -> RazorpaySettingsScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
onBack = {
|
onBack = {
|
||||||
route.value = AppRoute.ActiveRoomStays(
|
route.value = AppRoute.ActiveRoomStays(
|
||||||
@@ -317,7 +317,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
is AppRoute.PayuQr -> PayuQrScreen(
|
is AppRoute.RazorpayQr -> RazorpayQrScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
bookingId = currentRoute.bookingId,
|
bookingId = currentRoute.bookingId,
|
||||||
pendingAmount = currentRoute.pendingAmount,
|
pendingAmount = currentRoute.pendingAmount,
|
||||||
@@ -470,8 +470,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
guestId
|
guestId
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onOpenPayuQr = { pendingAmount, guestPhone ->
|
onOpenRazorpayQr = { pendingAmount, guestPhone ->
|
||||||
route.value = AppRoute.PayuQr(
|
route.value = AppRoute.RazorpayQr(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
bookingId = currentRoute.bookingId,
|
bookingId = currentRoute.bookingId,
|
||||||
pendingAmount = pendingAmount,
|
pendingAmount = pendingAmount,
|
||||||
@@ -484,12 +484,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
bookingId = currentRoute.bookingId
|
bookingId = currentRoute.bookingId
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
canManageDocuments = canManagePayuSettings(currentRoute.propertyId)
|
canManageDocuments = canManageRazorpaySettings(currentRoute.propertyId)
|
||||||
)
|
)
|
||||||
is AppRoute.BookingPayments -> BookingPaymentsScreen(
|
is AppRoute.BookingPayments -> BookingPaymentsScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
bookingId = currentRoute.bookingId,
|
bookingId = currentRoute.bookingId,
|
||||||
canAddCash = canManagePayuSettings(currentRoute.propertyId),
|
canAddCash = canManageRazorpaySettings(currentRoute.propertyId),
|
||||||
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
|
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
|
||||||
onBack = {
|
onBack = {
|
||||||
route.value = AppRoute.BookingDetailsTabs(
|
route.value = AppRoute.BookingDetailsTabs(
|
||||||
|
|||||||
@@ -16,5 +16,4 @@ interface ApiService :
|
|||||||
InboundEmailApi,
|
InboundEmailApi,
|
||||||
AmenityApi,
|
AmenityApi,
|
||||||
RatePlanApi,
|
RatePlanApi,
|
||||||
PayuSettingsApi,
|
RazorpaySettingsApi
|
||||||
PayuPaymentLinkSettingsApi
|
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
|||||||
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
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.PayuQrRequest
|
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
|
||||||
import com.android.trisolarispms.data.api.model.PayuQrResponse
|
import com.android.trisolarispms.data.api.model.RazorpayQrListItemDto
|
||||||
import com.android.trisolarispms.data.api.model.PayuLinkRequest
|
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
|
||||||
import com.android.trisolarispms.data.api.model.PayuLinkResponse
|
import com.android.trisolarispms.data.api.model.RazorpayQrResponse
|
||||||
|
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.PaymentDto
|
||||||
import com.android.trisolarispms.data.api.model.PaymentCreateRequest
|
import com.android.trisolarispms.data.api.model.PaymentCreateRequest
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
@@ -103,19 +105,19 @@ interface BookingApi {
|
|||||||
@Body body: BookingRoomStayCreateRequest
|
@Body body: BookingRoomStayCreateRequest
|
||||||
): Response<RoomStayDto>
|
): Response<RoomStayDto>
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/payments/payu/qr")
|
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr")
|
||||||
suspend fun generatePayuQr(
|
suspend fun generateRazorpayQr(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Body body: PayuQrRequest
|
@Body body: RazorpayQrRequest
|
||||||
): Response<PayuQrResponse>
|
): Response<RazorpayQrResponse>
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/payments/payu/link")
|
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/payment-link")
|
||||||
suspend fun generatePayuLink(
|
suspend fun generateRazorpayPaymentLink(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Body body: PayuLinkRequest
|
@Body body: RazorpayPaymentLinkRequest
|
||||||
): Response<PayuLinkResponse>
|
): Response<RazorpayPaymentLinkResponse>
|
||||||
|
|
||||||
@GET("properties/{propertyId}/bookings/{bookingId}/payments")
|
@GET("properties/{propertyId}/bookings/{bookingId}/payments")
|
||||||
suspend fun listPayments(
|
suspend fun listPayments(
|
||||||
@@ -136,4 +138,24 @@ interface BookingApi {
|
|||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Path("paymentId") paymentId: String
|
@Path("paymentId") paymentId: String
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr")
|
||||||
|
suspend fun listRazorpayQrs(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String
|
||||||
|
): Response<List<RazorpayQrListItemDto>>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/close")
|
||||||
|
suspend fun closeRazorpayQr(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Path("qrId") qrId: String
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
@GET("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events")
|
||||||
|
suspend fun listRazorpayQrEvents(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Path("qrId") qrId: String
|
||||||
|
): Response<List<RazorpayQrEventDto>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.android.trisolarispms.data.api
|
|
||||||
|
|
||||||
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsRequest
|
|
||||||
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsResponse
|
|
||||||
import retrofit2.Response
|
|
||||||
import retrofit2.http.Body
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.PUT
|
|
||||||
import retrofit2.http.Path
|
|
||||||
|
|
||||||
interface PayuPaymentLinkSettingsApi {
|
|
||||||
@GET("properties/{propertyId}/payu-payment-link-settings")
|
|
||||||
suspend fun getPayuPaymentLinkSettings(
|
|
||||||
@Path("propertyId") propertyId: String
|
|
||||||
): Response<PayuPaymentLinkSettingsResponse>
|
|
||||||
|
|
||||||
@PUT("properties/{propertyId}/payu-payment-link-settings")
|
|
||||||
suspend fun updatePayuPaymentLinkSettings(
|
|
||||||
@Path("propertyId") propertyId: String,
|
|
||||||
@Body body: PayuPaymentLinkSettingsRequest
|
|
||||||
): Response<PayuPaymentLinkSettingsResponse>
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.android.trisolarispms.data.api
|
|
||||||
|
|
||||||
import com.android.trisolarispms.data.api.model.PayuSettingsRequest
|
|
||||||
import com.android.trisolarispms.data.api.model.PayuSettingsResponse
|
|
||||||
import retrofit2.Response
|
|
||||||
import retrofit2.http.Body
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.PUT
|
|
||||||
import retrofit2.http.Path
|
|
||||||
|
|
||||||
interface PayuSettingsApi {
|
|
||||||
@GET("properties/{propertyId}/payu-settings")
|
|
||||||
suspend fun getPayuSettings(
|
|
||||||
@Path("propertyId") propertyId: String
|
|
||||||
): Response<PayuSettingsResponse>
|
|
||||||
|
|
||||||
@PUT("properties/{propertyId}/payu-settings")
|
|
||||||
suspend fun updatePayuSettings(
|
|
||||||
@Path("propertyId") propertyId: String,
|
|
||||||
@Body body: PayuSettingsRequest
|
|
||||||
): Response<PayuSettingsResponse>
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.android.trisolarispms.data.api
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpaySettingsRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpaySettingsResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.PUT
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface RazorpaySettingsApi {
|
||||||
|
@GET("properties/{propertyId}/razorpay-settings")
|
||||||
|
suspend fun getRazorpaySettings(
|
||||||
|
@Path("propertyId") propertyId: String
|
||||||
|
): Response<RazorpaySettingsResponse>
|
||||||
|
|
||||||
|
@PUT("properties/{propertyId}/razorpay-settings")
|
||||||
|
suspend fun updateRazorpaySettings(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Body body: RazorpaySettingsRequest
|
||||||
|
): Response<RazorpaySettingsResponse>
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.android.trisolarispms.data.api.model
|
|
||||||
|
|
||||||
data class PayuPaymentLinkSettingsRequest(
|
|
||||||
val merchantId: String,
|
|
||||||
val clientId: String,
|
|
||||||
val clientSecret: String,
|
|
||||||
val isTest: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PayuPaymentLinkSettingsResponse(
|
|
||||||
val propertyId: String? = null,
|
|
||||||
val configured: Boolean? = null,
|
|
||||||
val merchantId: String? = null,
|
|
||||||
val isTest: Boolean? = null,
|
|
||||||
val hasClientId: Boolean? = null,
|
|
||||||
val hasClientSecret: Boolean? = null,
|
|
||||||
val hasAccessToken: Boolean? = null
|
|
||||||
)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.android.trisolarispms.data.api.model
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
|
|
||||||
data class PayuQrRequest(
|
|
||||||
val amount: Long,
|
|
||||||
val deviceInfo: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PayuQrResponse(
|
|
||||||
val txnid: String? = null,
|
|
||||||
val amount: Long? = null,
|
|
||||||
val currency: String? = null,
|
|
||||||
val payuResponse: JsonElement? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PayuLinkRequest(
|
|
||||||
val amount: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PayuLinkResponse(
|
|
||||||
val amount: Long? = null,
|
|
||||||
val currency: String? = null,
|
|
||||||
val paymentLink: String? = null,
|
|
||||||
val payuResponse: JsonElement? = null
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
|
||||||
|
data class RazorpayQrRequest(
|
||||||
|
val amount: Long,
|
||||||
|
val deviceInfo: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RazorpayQrResponse(
|
||||||
|
val qrId: String? = null,
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val imageUrl: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RazorpayPaymentLinkRequest(
|
||||||
|
val amount: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RazorpayPaymentLinkResponse(
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val paymentLink: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RazorpayQrEventDto(
|
||||||
|
val event: String? = null,
|
||||||
|
val qrId: String? = null,
|
||||||
|
val status: String? = null,
|
||||||
|
val receivedAt: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RazorpayQrListItemDto(
|
||||||
|
val qrId: String? = null,
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val status: String? = null,
|
||||||
|
val imageUrl: String? = null,
|
||||||
|
val expiryAt: String? = null,
|
||||||
|
val createdAt: String? = null
|
||||||
|
)
|
||||||
@@ -2,20 +2,21 @@ package com.android.trisolarispms.data.api.model
|
|||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class PayuSettingsRequest(
|
data class RazorpaySettingsRequest(
|
||||||
val merchantKey: String,
|
val keyId: String,
|
||||||
val salt32: String? = null,
|
val keySecret: String? = null,
|
||||||
val salt256: String? = null,
|
val webhookSecret: String? = null,
|
||||||
@SerializedName("isTest") val isTest: Boolean,
|
@SerializedName("isTest") val isTest: Boolean,
|
||||||
val useSalt256: Boolean
|
val useSalt256: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PayuSettingsResponse(
|
data class RazorpaySettingsResponse(
|
||||||
val propertyId: String? = null,
|
val propertyId: String? = null,
|
||||||
val configured: Boolean? = null,
|
val configured: Boolean? = null,
|
||||||
val merchantKey: String? = null,
|
|
||||||
@SerializedName("test") val isTest: Boolean? = null,
|
@SerializedName("test") val isTest: Boolean? = null,
|
||||||
val useSalt256: Boolean? = null,
|
val useSalt256: Boolean? = null,
|
||||||
val hasSalt32: Boolean? = null,
|
val hasKeyId: Boolean? = null,
|
||||||
|
val hasKeySecret: Boolean? = null,
|
||||||
|
val hasWebhookSecret: Boolean? = null,
|
||||||
val hasSalt256: Boolean? = null
|
val hasSalt256: Boolean? = null
|
||||||
)
|
)
|
||||||
@@ -67,8 +67,8 @@ sealed interface AppRoute {
|
|||||||
val ratePlanId: String,
|
val ratePlanId: String,
|
||||||
val ratePlanCode: String
|
val ratePlanCode: String
|
||||||
) : AppRoute
|
) : AppRoute
|
||||||
data class PayuSettings(val propertyId: String) : AppRoute
|
data class RazorpaySettings(val propertyId: String) : AppRoute
|
||||||
data class PayuQr(
|
data class RazorpayQr(
|
||||||
val propertyId: String,
|
val propertyId: String,
|
||||||
val bookingId: String,
|
val bookingId: String,
|
||||||
val pendingAmount: Long?,
|
val pendingAmount: Long?,
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.android.trisolarispms.ui.payu
|
|
||||||
|
|
||||||
data class PayuPaymentLinkSettingsState(
|
|
||||||
val merchantId: String = "",
|
|
||||||
val clientId: String = "",
|
|
||||||
val clientSecret: String = "",
|
|
||||||
val isTest: Boolean = false,
|
|
||||||
val configured: Boolean = false,
|
|
||||||
val hasClientId: Boolean = false,
|
|
||||||
val hasClientSecret: Boolean = false,
|
|
||||||
val hasAccessToken: Boolean = false,
|
|
||||||
val isLoading: Boolean = false,
|
|
||||||
val isSaving: Boolean = false,
|
|
||||||
val error: String? = null,
|
|
||||||
val message: String? = null
|
|
||||||
)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package com.android.trisolarispms.ui.payu
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.android.trisolarispms.data.api.ApiClient
|
|
||||||
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsRequest
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class PayuPaymentLinkSettingsViewModel : ViewModel() {
|
|
||||||
private val _state = MutableStateFlow(PayuPaymentLinkSettingsState())
|
|
||||||
val state: StateFlow<PayuPaymentLinkSettingsState> = _state
|
|
||||||
|
|
||||||
fun load(propertyId: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
|
||||||
try {
|
|
||||||
val api = ApiClient.create()
|
|
||||||
val response = api.getPayuPaymentLinkSettings(propertyId)
|
|
||||||
val body = response.body()
|
|
||||||
if (response.isSuccessful && body != null) {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
merchantId = body.merchantId.orEmpty(),
|
|
||||||
isTest = body.isTest == true,
|
|
||||||
configured = body.configured == true,
|
|
||||||
hasClientId = body.hasClientId == true,
|
|
||||||
hasClientSecret = body.hasClientSecret == true,
|
|
||||||
hasAccessToken = body.hasAccessToken == true,
|
|
||||||
clientId = "",
|
|
||||||
clientSecret = "",
|
|
||||||
isLoading = false,
|
|
||||||
error = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = "Load failed: ${response.code()}",
|
|
||||||
message = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = e.localizedMessage ?: "Load failed",
|
|
||||||
message = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMerchantIdChange(value: String) {
|
|
||||||
_state.update { it.copy(merchantId = value, error = null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onClientIdChange(value: String) {
|
|
||||||
_state.update { it.copy(clientId = value, error = null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onClientSecretChange(value: String) {
|
|
||||||
_state.update { it.copy(clientSecret = value, error = null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onIsTestChange(value: Boolean) {
|
|
||||||
_state.update { it.copy(isTest = value, error = null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save(propertyId: String) {
|
|
||||||
val current = state.value
|
|
||||||
val merchantId = current.merchantId.trim()
|
|
||||||
val clientId = current.clientId.trim()
|
|
||||||
val clientSecret = current.clientSecret.trim()
|
|
||||||
if (merchantId.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
|
|
||||||
_state.update { it.copy(error = "All fields are required") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update { it.copy(isSaving = true, error = null, message = null) }
|
|
||||||
try {
|
|
||||||
val api = ApiClient.create()
|
|
||||||
val response = api.updatePayuPaymentLinkSettings(
|
|
||||||
propertyId = propertyId,
|
|
||||||
body = PayuPaymentLinkSettingsRequest(
|
|
||||||
merchantId = merchantId,
|
|
||||||
clientId = clientId,
|
|
||||||
clientSecret = clientSecret,
|
|
||||||
isTest = current.isTest
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
_state.update { it.copy(isSaving = false, message = "Saved", error = null) }
|
|
||||||
load(propertyId)
|
|
||||||
} else {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isSaving = false,
|
|
||||||
error = "Save failed: ${response.code()}",
|
|
||||||
message = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isSaving = false,
|
|
||||||
error = e.localizedMessage ?: "Save failed",
|
|
||||||
message = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
package com.android.trisolarispms.ui.payu
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.google.zxing.BarcodeFormat
|
|
||||||
import com.google.zxing.qrcode.QRCodeWriter
|
|
||||||
import com.google.zxing.common.BitMatrix
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
fun PayuQrScreen(
|
|
||||||
propertyId: String,
|
|
||||||
bookingId: String,
|
|
||||||
pendingAmount: Long?,
|
|
||||||
guestPhone: String?,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
viewModel: PayuQrViewModel = viewModel()
|
|
||||||
) {
|
|
||||||
val state by viewModel.state.collectAsState()
|
|
||||||
val clipboard = LocalClipboardManager.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose { viewModel.reset() }
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(pendingAmount) {
|
|
||||||
viewModel.setInitialAmount(pendingAmount)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text("PayU QR") },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBack) {
|
|
||||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
.padding(24.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
verticalArrangement = Arrangement.Top
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.amountInput,
|
|
||||||
onValueChange = viewModel::onAmountChange,
|
|
||||||
label = { Text("Amount") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
if (!guestPhone.isNullOrBlank()) {
|
|
||||||
viewModel.generateLink(propertyId, bookingId) { link ->
|
|
||||||
val uri = Uri.parse("smsto:${guestPhone}")
|
|
||||||
val intent = Intent(Intent.ACTION_SENDTO, uri)
|
|
||||||
.putExtra("sms_body", "Pay using this link: $link")
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = !state.isLoading && !guestPhone.isNullOrBlank(),
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text("Share Link")
|
|
||||||
}
|
|
||||||
Button(
|
|
||||||
onClick = { viewModel.generate(propertyId, bookingId) },
|
|
||||||
enabled = !state.isLoading,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
if (state.isLoading) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(16.dp)
|
|
||||||
.padding(end = 8.dp),
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
Text("Generating...")
|
|
||||||
} else {
|
|
||||||
Text("Generate QR")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.error?.let {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.qrString.isNullOrBlank()) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
val qrBitmap = remember(state.qrString) { state.qrString?.let { createQrBitmap(it) } }
|
|
||||||
qrBitmap?.let {
|
|
||||||
androidx.compose.foundation.Image(
|
|
||||||
bitmap = it.asImageBitmap(),
|
|
||||||
contentDescription = "PayU QR",
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(240.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Amount: ${state.amount ?: "-"} ${state.currency ?: ""}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
clipboard.setText(AnnotatedString(state.qrString.orEmpty()))
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Copy QR string")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createQrBitmap(content: String, size: Int = 600): Bitmap {
|
|
||||||
val matrix: BitMatrix = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, size, size)
|
|
||||||
val width = matrix.width
|
|
||||||
val height = matrix.height
|
|
||||||
val pixels = IntArray(width * height)
|
|
||||||
for (y in 0 until height) {
|
|
||||||
val offset = y * width
|
|
||||||
for (x in 0 until width) {
|
|
||||||
pixels[offset + x] = if (matrix.get(x, y)) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
|
|
||||||
setPixels(pixels, 0, width, 0, 0, width, height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.android.trisolarispms.ui.payu
|
|
||||||
|
|
||||||
data class PayuQrState(
|
|
||||||
val deviceInfo: String = "",
|
|
||||||
val amountInput: String = "",
|
|
||||||
val isLoading: Boolean = false,
|
|
||||||
val error: String? = null,
|
|
||||||
val txnId: String? = null,
|
|
||||||
val amount: Long? = null,
|
|
||||||
val currency: String? = null,
|
|
||||||
val qrString: String? = null,
|
|
||||||
val payuResponseRaw: String? = null,
|
|
||||||
val paymentLink: String? = null
|
|
||||||
)
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
package com.android.trisolarispms.ui.payu
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.android.trisolarispms.data.api.ApiClient
|
|
||||||
import com.android.trisolarispms.data.api.model.PayuLinkRequest
|
|
||||||
import com.android.trisolarispms.data.api.model.PayuQrRequest
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class PayuQrViewModel : ViewModel() {
|
|
||||||
private val gson = Gson()
|
|
||||||
private val _state = MutableStateFlow(
|
|
||||||
PayuQrState(deviceInfo = buildDeviceInfo())
|
|
||||||
)
|
|
||||||
val state: StateFlow<PayuQrState> = _state
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
_state.value = PayuQrState(deviceInfo = buildDeviceInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAmountChange(value: String) {
|
|
||||||
val digits = value.filter { it.isDigit() }
|
|
||||||
_state.update { it.copy(amountInput = digits, error = null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setInitialAmount(amount: Long?) {
|
|
||||||
if (amount == null || amount <= 0) return
|
|
||||||
_state.update { current ->
|
|
||||||
if (current.amountInput.isBlank()) {
|
|
||||||
current.copy(amountInput = amount.toString())
|
|
||||||
} else {
|
|
||||||
current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generate(propertyId: String, bookingId: String) {
|
|
||||||
val current = state.value
|
|
||||||
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
|
|
||||||
if (amount == null) {
|
|
||||||
_state.update { it.copy(error = "Amount is required") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
|
||||||
try {
|
|
||||||
val api = ApiClient.create()
|
|
||||||
val response = api.generatePayuQr(
|
|
||||||
propertyId = propertyId,
|
|
||||||
bookingId = bookingId,
|
|
||||||
body = PayuQrRequest(
|
|
||||||
amount = amount,
|
|
||||||
deviceInfo = current.deviceInfo
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val body = response.body()
|
|
||||||
if (response.isSuccessful && body != null) {
|
|
||||||
val (qrString, rawPayload) = extractQrString(body.payuResponse)
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
txnId = body.txnid,
|
|
||||||
amount = body.amount,
|
|
||||||
currency = body.currency,
|
|
||||||
qrString = qrString,
|
|
||||||
payuResponseRaw = rawPayload,
|
|
||||||
error = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = "QR request failed: ${response.code()}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = e.localizedMessage ?: "QR request failed"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateLink(propertyId: String, bookingId: String, onReady: (String) -> Unit) {
|
|
||||||
val current = state.value
|
|
||||||
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
|
|
||||||
if (amount == null) {
|
|
||||||
_state.update { it.copy(error = "Amount is required") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
|
||||||
try {
|
|
||||||
val api = ApiClient.create()
|
|
||||||
val response = api.generatePayuLink(
|
|
||||||
propertyId = propertyId,
|
|
||||||
bookingId = bookingId,
|
|
||||||
body = PayuLinkRequest(amount = amount)
|
|
||||||
)
|
|
||||||
val body = response.body()
|
|
||||||
if (response.isSuccessful && body?.paymentLink != null) {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
paymentLink = body.paymentLink,
|
|
||||||
amount = body.amount,
|
|
||||||
currency = body.currency,
|
|
||||||
error = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onReady(body.paymentLink)
|
|
||||||
} else {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = "Link request failed: ${response.code()}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = e.localizedMessage ?: "Link request failed"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractQrString(payuResponse: JsonElement?): Pair<String?, String?> {
|
|
||||||
if (payuResponse == null) return null to null
|
|
||||||
val raw = gson.toJson(payuResponse)
|
|
||||||
val root = when {
|
|
||||||
payuResponse.isJsonObject -> payuResponse.asJsonObject
|
|
||||||
payuResponse.isJsonPrimitive && payuResponse.asJsonPrimitive.isString -> {
|
|
||||||
runCatching {
|
|
||||||
gson.fromJson(payuResponse.asString, JsonObject::class.java)
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
val qrString = root?.getAsJsonObject("result")?.get("qrString")?.asString
|
|
||||||
return qrString to raw
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildDeviceInfo(): String {
|
|
||||||
val release = android.os.Build.VERSION.RELEASE
|
|
||||||
val model = android.os.Build.MODEL
|
|
||||||
return "Android $release; $model; PMS"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,437 +0,0 @@
|
|||||||
package com.android.trisolarispms.ui.payu
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.TabRow
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
fun PayuSettingsScreen(
|
|
||||||
propertyId: String,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
viewModel: PayuSettingsViewModel = viewModel(),
|
|
||||||
linkViewModel: PayuPaymentLinkSettingsViewModel = viewModel()
|
|
||||||
) {
|
|
||||||
val state by viewModel.state.collectAsState()
|
|
||||||
val linkState by linkViewModel.state.collectAsState()
|
|
||||||
val clipboard = LocalClipboardManager.current
|
|
||||||
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 })
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(propertyId) {
|
|
||||||
viewModel.load(propertyId)
|
|
||||||
linkViewModel.load(propertyId)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text("Payu Settings") },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBack) {
|
|
||||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
TabRow(selectedTabIndex = pagerState.currentPage) {
|
|
||||||
Tab(
|
|
||||||
selected = pagerState.currentPage == 0,
|
|
||||||
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
|
|
||||||
text = { Text("QR") }
|
|
||||||
)
|
|
||||||
Tab(
|
|
||||||
selected = pagerState.currentPage == 1,
|
|
||||||
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
|
|
||||||
text = { Text("Payment Links") }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalPager(
|
|
||||||
state = pagerState,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) { page ->
|
|
||||||
when (page) {
|
|
||||||
0 -> PayuQrSettingsTab(
|
|
||||||
propertyId = propertyId,
|
|
||||||
state = state,
|
|
||||||
onMerchantKeyChange = viewModel::onMerchantKeyChange,
|
|
||||||
onIsTestChange = viewModel::onIsTestChange,
|
|
||||||
onUseSalt256Change = viewModel::onUseSalt256Change,
|
|
||||||
onSalt32Change = viewModel::onSalt32Change,
|
|
||||||
onSalt256Change = viewModel::onSalt256Change,
|
|
||||||
onSave = { viewModel.save(propertyId) },
|
|
||||||
clipboard = clipboard
|
|
||||||
)
|
|
||||||
1 -> PayuPaymentLinksTab(
|
|
||||||
propertyId = propertyId,
|
|
||||||
state = linkState,
|
|
||||||
onMerchantIdChange = linkViewModel::onMerchantIdChange,
|
|
||||||
onClientIdChange = linkViewModel::onClientIdChange,
|
|
||||||
onClientSecretChange = linkViewModel::onClientSecretChange,
|
|
||||||
onIsTestChange = linkViewModel::onIsTestChange,
|
|
||||||
onSave = { linkViewModel.save(propertyId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PayuQrSettingsTab(
|
|
||||||
propertyId: String,
|
|
||||||
state: PayuSettingsState,
|
|
||||||
onMerchantKeyChange: (String) -> Unit,
|
|
||||||
onIsTestChange: (Boolean) -> Unit,
|
|
||||||
onUseSalt256Change: (Boolean) -> Unit,
|
|
||||||
onSalt32Change: (String) -> Unit,
|
|
||||||
onSalt256Change: (String) -> Unit,
|
|
||||||
onSave: () -> Unit,
|
|
||||||
clipboard: ClipboardManager
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
verticalArrangement = Arrangement.Top
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Payu Settings",
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "More payment settings will be added later. For now, configure PayU.",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Success/failure URLs are derived by the server using this property ID.",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
if (state.isLoading) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = if (state.configured) "Status: Configured" else "Status: Not configured",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
state.message?.let {
|
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.primary)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
state.error?.let {
|
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.merchantKey,
|
|
||||||
onValueChange = onMerchantKeyChange,
|
|
||||||
label = { Text("Merchant key") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(text = "Use PayU test URL")
|
|
||||||
Switch(
|
|
||||||
checked = state.isTest,
|
|
||||||
onCheckedChange = onIsTestChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
Text(
|
|
||||||
text = if (state.isTest) {
|
|
||||||
"Base URL: https://test.payu.in/_payment"
|
|
||||||
} else {
|
|
||||||
"Base URL: https://secure.payu.in/_payment"
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(text = "Use salt256")
|
|
||||||
Switch(
|
|
||||||
checked = state.useSalt256,
|
|
||||||
onCheckedChange = onUseSalt256Change
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
if (state.useSalt256) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.salt256,
|
|
||||||
onValueChange = onSalt256Change,
|
|
||||||
label = { Text("Salt256") },
|
|
||||||
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.salt32,
|
|
||||||
onValueChange = onSalt32Change,
|
|
||||||
label = { Text("Salt32") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
if (state.hasSalt32) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Salt32 saved",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = onSave,
|
|
||||||
enabled = !state.isSaving,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
if (state.isSaving) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(16.dp)
|
|
||||||
.padding(end = 8.dp),
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
Text("Saving...")
|
|
||||||
} else {
|
|
||||||
Text("Save")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = "https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/success",
|
|
||||||
onValueChange = {},
|
|
||||||
label = { Text("Success URL") },
|
|
||||||
readOnly = true,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = "https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/failure",
|
|
||||||
onValueChange = {},
|
|
||||||
label = { Text("Failure URL") },
|
|
||||||
readOnly = true,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
clipboard.setText(
|
|
||||||
AnnotatedString(
|
|
||||||
"https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/success"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Copy success URL")
|
|
||||||
}
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
clipboard.setText(
|
|
||||||
AnnotatedString(
|
|
||||||
"https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/failure"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Copy failure URL")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PayuPaymentLinksTab(
|
|
||||||
propertyId: String,
|
|
||||||
state: PayuPaymentLinkSettingsState,
|
|
||||||
onMerchantIdChange: (String) -> Unit,
|
|
||||||
onClientIdChange: (String) -> Unit,
|
|
||||||
onClientSecretChange: (String) -> Unit,
|
|
||||||
onIsTestChange: (Boolean) -> Unit,
|
|
||||||
onSave: () -> Unit
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
verticalArrangement = Arrangement.Top
|
|
||||||
) {
|
|
||||||
if (state.isLoading) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = if (state.configured) "Status: Configured" else "Status: Not configured",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
if (state.hasClientId) {
|
|
||||||
Text(text = "Client ID saved", style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
if (state.hasClientSecret) {
|
|
||||||
Text(text = "Client Secret saved", style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
if (state.hasAccessToken) {
|
|
||||||
Text(text = "Access token available", style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Text(
|
|
||||||
text = "Payment Link Settings",
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
state.message?.let {
|
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.primary)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
state.error?.let {
|
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.merchantId,
|
|
||||||
onValueChange = onMerchantIdChange,
|
|
||||||
label = { Text("Merchant ID") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.clientId,
|
|
||||||
onValueChange = onClientIdChange,
|
|
||||||
label = { Text("Client ID") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.clientSecret,
|
|
||||||
onValueChange = onClientSecretChange,
|
|
||||||
label = { Text("Client Secret") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(text = "Use PayU test environment")
|
|
||||||
Switch(
|
|
||||||
checked = state.isTest,
|
|
||||||
onCheckedChange = onIsTestChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = onSave,
|
|
||||||
enabled = !state.isSaving,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
if (state.isSaving) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(16.dp)
|
|
||||||
.padding(end = 8.dp),
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
Text("Saving...")
|
|
||||||
} else {
|
|
||||||
Text("Save")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
package com.android.trisolarispms.ui.razorpay
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import coil.compose.SubcomposeAsyncImage
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import com.android.trisolarispms.BuildConfig
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun RazorpayQrScreen(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
pendingAmount: Long?,
|
||||||
|
guestPhone: String?,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: RazorpayQrViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose { viewModel.reset() }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(pendingAmount) {
|
||||||
|
viewModel.setInitialAmount(pendingAmount)
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (BuildConfig.DEBUG && state.amountInput.isBlank()) {
|
||||||
|
viewModel.onAmountChange("10")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(propertyId, bookingId) {
|
||||||
|
viewModel.loadQrList(propertyId, bookingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isViewingQr = state.isClosed || !state.imageUrl.isNullOrBlank()
|
||||||
|
BackHandler(enabled = isViewingQr) {
|
||||||
|
viewModel.exitQrView()
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Razorpay QR") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (isViewingQr) {
|
||||||
|
viewModel.exitQrView()
|
||||||
|
} else {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
if (state.isClosed) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "QR closed",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (!state.imageUrl.isNullOrBlank()) {
|
||||||
|
LaunchedEffect(state.qrId) {
|
||||||
|
viewModel.startStreamForCurrentQr(propertyId, bookingId)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
model = state.imageUrl,
|
||||||
|
contentDescription = "Razorpay QR",
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
loading = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
strokeWidth = 3.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error = {
|
||||||
|
Text(
|
||||||
|
text = "Failed to load QR image",
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.amountInput,
|
||||||
|
onValueChange = viewModel::onAmountChange,
|
||||||
|
label = { Text("Amount") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.generate(propertyId, bookingId) },
|
||||||
|
enabled = !state.isLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(18.dp)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(strokeWidth = 2.dp)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Generating…")
|
||||||
|
} else {
|
||||||
|
Text("Generate QR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.error?.let {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.qrList.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(text = "QR List", style = MaterialTheme.typography.titleMedium)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
state.qrList.forEachIndexed { index, item ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { viewModel.openQrFromList(item) },
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "QR ${index + 1}",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = "${item.amount ?: "-"} ${item.currency.orEmpty()}",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
val qrId = item.qrId
|
||||||
|
if (!qrId.isNullOrBlank()) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.closeQr(propertyId, bookingId, qrId)
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = "Close QR",
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.android.trisolarispms.ui.razorpay
|
||||||
|
|
||||||
|
data class RazorpayQrState(
|
||||||
|
val deviceInfo: String = "",
|
||||||
|
val amountInput: String = "",
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val qrId: String? = null,
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val imageUrl: String? = null,
|
||||||
|
val isClosed: Boolean = false,
|
||||||
|
val paymentLink: String? = null,
|
||||||
|
val qrList: List<com.android.trisolarispms.data.api.model.RazorpayQrListItemDto> = emptyList()
|
||||||
|
)
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package com.android.trisolarispms.ui.razorpay
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.ApiConstants
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayQrListItemDto
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayPaymentLinkRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.sse.EventSource
|
||||||
|
import okhttp3.sse.EventSourceListener
|
||||||
|
import okhttp3.sse.EventSources
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class RazorpayQrViewModel : ViewModel() {
|
||||||
|
private val gson = Gson()
|
||||||
|
private val _state = MutableStateFlow(
|
||||||
|
RazorpayQrState(deviceInfo = buildDeviceInfo())
|
||||||
|
)
|
||||||
|
val state: StateFlow<RazorpayQrState> = _state
|
||||||
|
private var qrEventSource: EventSource? = null
|
||||||
|
private var lastQrId: String? = null
|
||||||
|
private var qrPollJob: Job? = null
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
stopQrEventStream()
|
||||||
|
stopQrEventPolling()
|
||||||
|
_state.value = RazorpayQrState(deviceInfo = buildDeviceInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exitQrView() {
|
||||||
|
stopQrEventPolling()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
qrId = null,
|
||||||
|
imageUrl = null,
|
||||||
|
isClosed = false,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAmountChange(value: String) {
|
||||||
|
val digits = value.filter { it.isDigit() }
|
||||||
|
_state.update { it.copy(amountInput = digits, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInitialAmount(amount: Long?) {
|
||||||
|
if (amount == null || amount <= 0) return
|
||||||
|
_state.update { current ->
|
||||||
|
if (current.amountInput.isBlank()) {
|
||||||
|
current.copy(amountInput = amount.toString())
|
||||||
|
} else {
|
||||||
|
current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generate(propertyId: String, bookingId: String) {
|
||||||
|
val current = state.value
|
||||||
|
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
|
||||||
|
if (amount == null) {
|
||||||
|
_state.update { it.copy(error = "Amount is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.generateRazorpayQr(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = RazorpayQrRequest(
|
||||||
|
amount = amount,
|
||||||
|
deviceInfo = current.deviceInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
qrId = body.qrId,
|
||||||
|
amount = body.amount,
|
||||||
|
currency = body.currency,
|
||||||
|
imageUrl = body.imageUrl,
|
||||||
|
isClosed = false,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadQrList(propertyId, bookingId)
|
||||||
|
startQrEventStream(propertyId, bookingId, body.qrId)
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "QR request failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "QR request failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startQrEventStream(propertyId: String, bookingId: String, qrId: String?) {
|
||||||
|
if (qrId.isNullOrBlank()) return
|
||||||
|
if (lastQrId == qrId && qrEventSource != null) return
|
||||||
|
stopQrEventStream()
|
||||||
|
lastQrId = qrId
|
||||||
|
val client = ApiClient.createOkHttpClient(readTimeoutSeconds = 0)
|
||||||
|
val url = "${ApiConstants.BASE_URL}properties/$propertyId/bookings/$bookingId/payments/razorpay/qr/$qrId/events/stream"
|
||||||
|
val request = Request.Builder().url(url).get().build()
|
||||||
|
qrEventSource = EventSources.createFactory(client).newEventSource(
|
||||||
|
request,
|
||||||
|
object : EventSourceListener() {
|
||||||
|
override fun onEvent(
|
||||||
|
eventSource: EventSource,
|
||||||
|
id: String?,
|
||||||
|
type: String?,
|
||||||
|
data: String
|
||||||
|
) {
|
||||||
|
if (data.isBlank() || type == "ping") return
|
||||||
|
val event = runCatching {
|
||||||
|
gson.fromJson(data, RazorpayQrEventDto::class.java)
|
||||||
|
}.getOrNull() ?: return
|
||||||
|
if (isClosedStatus(event.status)) {
|
||||||
|
_state.update { it.copy(isClosed = true, imageUrl = null) }
|
||||||
|
stopQrEventStream()
|
||||||
|
stopQrEventPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
eventSource: EventSource,
|
||||||
|
t: Throwable?,
|
||||||
|
response: okhttp3.Response?
|
||||||
|
) {
|
||||||
|
stopQrEventStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosed(eventSource: EventSource) {
|
||||||
|
stopQrEventStream()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
startQrEventPolling(propertyId, bookingId, qrId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopQrEventStream() {
|
||||||
|
qrEventSource?.cancel()
|
||||||
|
qrEventSource = null
|
||||||
|
lastQrId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startQrEventPolling(propertyId: String, bookingId: String, qrId: String) {
|
||||||
|
if (qrPollJob?.isActive == true) return
|
||||||
|
qrPollJob = viewModelScope.launch {
|
||||||
|
while (true) {
|
||||||
|
val currentQrId = state.value.qrId
|
||||||
|
if (currentQrId.isNullOrBlank() || currentQrId != qrId || state.value.isClosed) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val response = ApiClient.create().listRazorpayQrEvents(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId
|
||||||
|
)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
if (body.any { isClosedStatus(it.status) }) {
|
||||||
|
_state.update { it.copy(isClosed = true, imageUrl = null) }
|
||||||
|
stopQrEventStream()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// ignore polling errors
|
||||||
|
}
|
||||||
|
delay(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopQrEventPolling() {
|
||||||
|
qrPollJob?.cancel()
|
||||||
|
qrPollJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isClosedStatus(status: String?): Boolean {
|
||||||
|
return when (status?.lowercase()) {
|
||||||
|
"credited", "closed", "expired" -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadQrList(propertyId: String, bookingId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.create().listRazorpayQrs(propertyId, bookingId)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
val filtered = body.filterNot { item ->
|
||||||
|
when (item.status?.lowercase()) {
|
||||||
|
"closed", "expired", "credited" -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_state.update { it.copy(qrList = filtered) }
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// ignore list load errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeQr(propertyId: String, bookingId: String, qrId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response = ApiClient.create().closeRazorpayQr(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
qrId = qrId
|
||||||
|
)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(qrList = current.qrList.filterNot { it.qrId == qrId })
|
||||||
|
}
|
||||||
|
loadQrList(propertyId, bookingId)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// ignore close errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openQrFromList(item: RazorpayQrListItemDto) {
|
||||||
|
val status = item.status?.lowercase()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
qrId = item.qrId,
|
||||||
|
amount = item.amount,
|
||||||
|
currency = item.currency,
|
||||||
|
imageUrl = item.imageUrl,
|
||||||
|
isClosed = status == "closed" || status == "expired" || status == "credited",
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startStreamForCurrentQr(propertyId: String, bookingId: String) {
|
||||||
|
startQrEventStream(propertyId, bookingId, state.value.qrId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateLink(propertyId: String, bookingId: String, onReady: (String) -> Unit) {
|
||||||
|
val current = state.value
|
||||||
|
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
|
||||||
|
if (amount == null) {
|
||||||
|
_state.update { it.copy(error = "Amount is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.generateRazorpayPaymentLink(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = RazorpayPaymentLinkRequest(amount = amount)
|
||||||
|
)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body?.paymentLink != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
paymentLink = body.paymentLink,
|
||||||
|
amount = body.amount,
|
||||||
|
currency = body.currency,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onReady(body.paymentLink)
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Link request failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "Link request failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDeviceInfo(): String {
|
||||||
|
val release = android.os.Build.VERSION.RELEASE
|
||||||
|
val model = android.os.Build.MODEL
|
||||||
|
return "Android $release; $model; PMS"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
stopQrEventStream()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
package com.android.trisolarispms.ui.razorpay
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun RazorpaySettingsScreen(
|
||||||
|
propertyId: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: RazorpaySettingsViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
|
||||||
|
LaunchedEffect(propertyId) {
|
||||||
|
viewModel.load(propertyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Razorpay Settings") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
|
RazorpaySettingsTab(
|
||||||
|
propertyId = propertyId,
|
||||||
|
state = state,
|
||||||
|
onKeyIdChange = viewModel::onMerchantKeyChange,
|
||||||
|
onKeySecretChange = viewModel::onSalt32Change,
|
||||||
|
onWebhookSecretChange = viewModel::onWebhookSecretChange,
|
||||||
|
onIsTestChange = viewModel::onIsTestChange,
|
||||||
|
onUseSalt256Change = viewModel::onUseSalt256Change,
|
||||||
|
onSalt256Change = viewModel::onSalt256Change,
|
||||||
|
onSave = { viewModel.save(propertyId) },
|
||||||
|
clipboard = clipboard
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RazorpaySettingsTab(
|
||||||
|
propertyId: String,
|
||||||
|
state: RazorpaySettingsState,
|
||||||
|
onKeyIdChange: (String) -> Unit,
|
||||||
|
onKeySecretChange: (String) -> Unit,
|
||||||
|
onWebhookSecretChange: (String) -> Unit,
|
||||||
|
onIsTestChange: (Boolean) -> Unit,
|
||||||
|
onUseSalt256Change: (Boolean) -> Unit,
|
||||||
|
onSalt256Change: (String) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
clipboard: ClipboardManager
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Razorpay Settings",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "More payment settings will be added later. For now, configure Razorpay.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Webhook URL is derived by the server using this property ID.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (state.configured) "Status: Configured" else "Status: Not configured",
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
state.message?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
state.error?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
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))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = "Use test environment")
|
||||||
|
Switch(
|
||||||
|
checked = state.isTest,
|
||||||
|
onCheckedChange = onIsTestChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (state.isSaving) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Text("Saving...")
|
||||||
|
} else {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = "https://api.hoteltrisolaris.in/properties/$propertyId/razorpay/webhook",
|
||||||
|
onValueChange = {},
|
||||||
|
label = { Text("Webhook URL") },
|
||||||
|
readOnly = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
clipboard.setText(
|
||||||
|
AnnotatedString(
|
||||||
|
"https://api.hoteltrisolaris.in/properties/$propertyId/razorpay/webhook"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Copy webhook URL")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.android.trisolarispms.ui.payu
|
package com.android.trisolarispms.ui.razorpay
|
||||||
|
|
||||||
data class PayuSettingsState(
|
data class RazorpaySettingsState(
|
||||||
val merchantKey: String = "",
|
val keyId: String = "",
|
||||||
val salt32: String = "",
|
val keySecret: String = "",
|
||||||
|
val webhookSecret: String = "",
|
||||||
val salt256: String = "",
|
val salt256: String = "",
|
||||||
val isTest: Boolean = false,
|
val isTest: Boolean = false,
|
||||||
val useSalt256: Boolean = false,
|
val useSalt256: Boolean = false,
|
||||||
val hasSalt32: Boolean = false,
|
val hasKeyId: Boolean = false,
|
||||||
|
val hasKeySecret: Boolean = false,
|
||||||
|
val hasWebhookSecret: Boolean = false,
|
||||||
val hasSalt256: Boolean = false,
|
val hasSalt256: Boolean = false,
|
||||||
val configured: Boolean = false,
|
val configured: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
package com.android.trisolarispms.ui.payu
|
package com.android.trisolarispms.ui.razorpay
|
||||||
|
|
||||||
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.PayuSettingsRequest
|
import com.android.trisolarispms.data.api.model.RazorpaySettingsRequest
|
||||||
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
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PayuSettingsViewModel : ViewModel() {
|
class RazorpaySettingsViewModel : ViewModel() {
|
||||||
private val _state = MutableStateFlow(PayuSettingsState())
|
private val _state = MutableStateFlow(RazorpaySettingsState())
|
||||||
val state: StateFlow<PayuSettingsState> = _state
|
val state: StateFlow<RazorpaySettingsState> = _state
|
||||||
|
|
||||||
fun load(propertyId: String) {
|
fun load(propertyId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -20,11 +20,15 @@ class PayuSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onMerchantKeyChange(value: String) {
|
fun onMerchantKeyChange(value: String) {
|
||||||
_state.update { it.copy(merchantKey = value, error = null) }
|
_state.update { it.copy(keyId = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSalt32Change(value: String) {
|
fun onSalt32Change(value: String) {
|
||||||
_state.update { it.copy(salt32 = value, error = null) }
|
_state.update { it.copy(keySecret = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onWebhookSecretChange(value: String) {
|
||||||
|
_state.update { it.copy(webhookSecret = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSalt256Change(value: String) {
|
fun onSalt256Change(value: String) {
|
||||||
@@ -41,33 +45,43 @@ class PayuSettingsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun save(propertyId: String) {
|
fun save(propertyId: String) {
|
||||||
val current = state.value
|
val current = state.value
|
||||||
val merchantKey = current.merchantKey.trim()
|
val keyId = current.keyId.trim()
|
||||||
val salt32 = current.salt32.trim()
|
val keySecret = current.keySecret.trim()
|
||||||
|
val webhookSecret = current.webhookSecret.trim()
|
||||||
val salt256 = current.salt256.trim()
|
val salt256 = current.salt256.trim()
|
||||||
|
|
||||||
if (merchantKey.isBlank()) {
|
if (!current.configured) {
|
||||||
_state.update { it.copy(error = "Merchant key is required") }
|
if (keyId.isBlank()) {
|
||||||
return
|
_state.update { it.copy(error = "Key ID is required") }
|
||||||
}
|
return
|
||||||
if (current.useSalt256 && salt256.isBlank() && !current.hasSalt256) {
|
}
|
||||||
_state.update { it.copy(error = "Salt256 is required") }
|
if (current.useSalt256 && salt256.isBlank() && !current.hasSalt256) {
|
||||||
return
|
_state.update { it.copy(error = "Secondary secret is required") }
|
||||||
}
|
return
|
||||||
if (!current.useSalt256 && salt32.isBlank() && !current.hasSalt32) {
|
}
|
||||||
_state.update { it.copy(error = "Salt32 is required") }
|
if (!current.useSalt256 && keySecret.isBlank() && !current.hasKeySecret) {
|
||||||
return
|
_state.update { it.copy(error = "Key secret is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val hasKeyIdInput = keyId.isNotBlank()
|
||||||
|
val hasKeySecretInput = keySecret.isNotBlank()
|
||||||
|
if (hasKeyIdInput xor hasKeySecretInput) {
|
||||||
|
_state.update { it.copy(error = "Key ID and Key Secret must be provided together") }
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isSaving = true, error = null, message = null) }
|
_state.update { it.copy(isSaving = true, error = null, message = null) }
|
||||||
try {
|
try {
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
val response = api.updatePayuSettings(
|
val response = api.updateRazorpaySettings(
|
||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
body = PayuSettingsRequest(
|
body = RazorpaySettingsRequest(
|
||||||
merchantKey = merchantKey,
|
keyId = keyId,
|
||||||
salt32 = salt32.ifBlank { null },
|
keySecret = keySecret.ifBlank { null },
|
||||||
salt256 = salt256.ifBlank { null },
|
webhookSecret = webhookSecret.ifBlank { null },
|
||||||
isTest = current.isTest,
|
isTest = current.isTest,
|
||||||
useSalt256 = current.useSalt256
|
useSalt256 = current.useSalt256
|
||||||
)
|
)
|
||||||
@@ -103,18 +117,21 @@ class PayuSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
val response = api.getPayuSettings(propertyId)
|
val response = api.getRazorpaySettings(propertyId)
|
||||||
val body = response.body()
|
val body = response.body()
|
||||||
if (response.isSuccessful && body != null) {
|
if (response.isSuccessful && body != null) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
merchantKey = body.merchantKey.orEmpty(),
|
keyId = "",
|
||||||
isTest = body.isTest == true,
|
isTest = body.isTest == true,
|
||||||
useSalt256 = body.useSalt256 == true,
|
useSalt256 = body.useSalt256 == true,
|
||||||
hasSalt32 = body.hasSalt32 == true,
|
hasKeyId = body.hasKeyId == true,
|
||||||
|
hasKeySecret = body.hasKeySecret == true,
|
||||||
|
hasWebhookSecret = body.hasWebhookSecret == true,
|
||||||
hasSalt256 = body.hasSalt256 == true,
|
hasSalt256 = body.hasSalt256 == true,
|
||||||
configured = body.configured == true,
|
configured = body.configured == true,
|
||||||
salt32 = "",
|
keySecret = "",
|
||||||
|
webhookSecret = "",
|
||||||
salt256 = "",
|
salt256 = "",
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
@@ -52,8 +52,8 @@ fun ActiveRoomStaysScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onViewRooms: () -> Unit,
|
onViewRooms: () -> Unit,
|
||||||
onCreateBooking: () -> Unit,
|
onCreateBooking: () -> Unit,
|
||||||
showPayuSettings: Boolean,
|
showRazorpaySettings: Boolean,
|
||||||
onPayuSettings: () -> Unit,
|
onRazorpaySettings: () -> Unit,
|
||||||
onManageRoomStay: (BookingListItem) -> Unit,
|
onManageRoomStay: (BookingListItem) -> Unit,
|
||||||
onViewBookingStays: (BookingListItem) -> Unit,
|
onViewBookingStays: (BookingListItem) -> Unit,
|
||||||
onOpenBookingDetails: (BookingListItem) -> Unit,
|
onOpenBookingDetails: (BookingListItem) -> Unit,
|
||||||
@@ -79,9 +79,9 @@ fun ActiveRoomStaysScreen(
|
|||||||
IconButton(onClick = onViewRooms) {
|
IconButton(onClick = onViewRooms) {
|
||||||
Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms")
|
Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms")
|
||||||
}
|
}
|
||||||
if (showPayuSettings) {
|
if (showRazorpaySettings) {
|
||||||
IconButton(onClick = onPayuSettings) {
|
IconButton(onClick = onRazorpaySettings) {
|
||||||
Icon(Icons.Default.Payment, contentDescription = "Payu Settings")
|
Icon(Icons.Default.Payment, contentDescription = "Razorpay Settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ fun BookingDetailsTabsScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onEditCheckout: (String?, String?) -> Unit,
|
onEditCheckout: (String?, String?) -> Unit,
|
||||||
onEditSignature: (String) -> Unit,
|
onEditSignature: (String) -> Unit,
|
||||||
onOpenPayuQr: (Long?, String?) -> Unit,
|
onOpenRazorpayQr: (Long?, String?) -> Unit,
|
||||||
onOpenPayments: () -> Unit,
|
onOpenPayments: () -> Unit,
|
||||||
canManageDocuments: Boolean,
|
canManageDocuments: Boolean,
|
||||||
staysViewModel: BookingRoomStaysViewModel = viewModel(),
|
staysViewModel: BookingRoomStaysViewModel = viewModel(),
|
||||||
@@ -153,7 +153,7 @@ fun BookingDetailsTabsScreen(
|
|||||||
error = detailsState.error,
|
error = detailsState.error,
|
||||||
onEditCheckout = onEditCheckout,
|
onEditCheckout = onEditCheckout,
|
||||||
onEditSignature = onEditSignature,
|
onEditSignature = onEditSignature,
|
||||||
onOpenPayuQr = onOpenPayuQr,
|
onOpenRazorpayQr = onOpenRazorpayQr,
|
||||||
onOpenPayments = onOpenPayments
|
onOpenPayments = onOpenPayments
|
||||||
)
|
)
|
||||||
1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
|
1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
|
||||||
@@ -190,7 +190,7 @@ private fun GuestInfoTabContent(
|
|||||||
error: String?,
|
error: String?,
|
||||||
onEditCheckout: (String?, String?) -> Unit,
|
onEditCheckout: (String?, String?) -> Unit,
|
||||||
onEditSignature: (String) -> Unit,
|
onEditSignature: (String) -> Unit,
|
||||||
onOpenPayuQr: (Long?, String?) -> Unit,
|
onOpenRazorpayQr: (Long?, String?) -> Unit,
|
||||||
onOpenPayments: () -> Unit
|
onOpenPayments: () -> Unit
|
||||||
) {
|
) {
|
||||||
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
||||||
@@ -280,10 +280,10 @@ private fun GuestInfoTabContent(
|
|||||||
title = "Calculations",
|
title = "Calculations",
|
||||||
headerContent = {
|
headerContent = {
|
||||||
if (hasGuestName && hasGuestPhone && hasPending) {
|
if (hasGuestName && hasGuestPhone && hasPending) {
|
||||||
IconButton(onClick = { onOpenPayuQr(details.pending, details.guestPhone) }) {
|
IconButton(onClick = { onOpenRazorpayQr(details.pending, details.guestPhone) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.QrCode,
|
imageVector = Icons.Default.QrCode,
|
||||||
contentDescription = "PayU QR",
|
contentDescription = "Razorpay QR",
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user