fix dashboard bills display and add payment support

This commit is contained in:
2025-07-26 14:27:49 +05:00
parent f62f867d56
commit d39bd62c6d
19 changed files with 1121 additions and 22 deletions

View File

@@ -72,6 +72,11 @@
android:exported="false"
android:label="Pay Without Account"
android:theme="@style/Theme.GridFlow.NoActionBar" />
<activity
android:name=".PaymentReviewActivity"
android:exported="false"
android:label="Payment Review"
android:theme="@style/Theme.GridFlow.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"

View File

@@ -0,0 +1,250 @@
package sh.sar.gridflow
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch
import sh.sar.gridflow.data.OutstandingBill
import sh.sar.gridflow.data.PaymentDetails
import sh.sar.gridflow.data.PaymentGateway
import sh.sar.gridflow.data.PaymentItem
import sh.sar.gridflow.data.PaymentRequest
import sh.sar.gridflow.databinding.ActivityPaymentReviewBinding
import sh.sar.gridflow.network.ApiResult
import sh.sar.gridflow.network.FenakaApiService
import sh.sar.gridflow.ui.payment.BillSummaryAdapter
import sh.sar.gridflow.ui.payment.PaymentConfirmationDialog
import sh.sar.gridflow.ui.payment.PaymentMethodAdapter
import sh.sar.gridflow.utils.SecureStorage
class PaymentReviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityPaymentReviewBinding
private lateinit var secureStorage: SecureStorage
private lateinit var apiService: FenakaApiService
private lateinit var billSummaryAdapter: BillSummaryAdapter
private lateinit var paymentMethodAdapter: PaymentMethodAdapter
private var outstandingBills: List<OutstandingBill> = emptyList()
companion object {
private const val TAG = "PaymentReviewActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Force system theme (follows device dark mode setting)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
binding = ActivityPaymentReviewBinding.inflate(layoutInflater)
setContentView(binding.root)
secureStorage = SecureStorage(this)
apiService = FenakaApiService()
setupViews()
loadData()
}
private fun setupViews() {
// Setup bills RecyclerView
billSummaryAdapter = BillSummaryAdapter()
binding.recyclerBills.apply {
layoutManager = LinearLayoutManager(this@PaymentReviewActivity)
adapter = billSummaryAdapter
}
// Setup payment methods RecyclerView
paymentMethodAdapter = PaymentMethodAdapter { paymentMethod ->
onPaymentMethodSelected(paymentMethod)
}
binding.recyclerPaymentMethods.apply {
layoutManager = LinearLayoutManager(this@PaymentReviewActivity)
adapter = paymentMethodAdapter
}
}
private fun loadData() {
val cookie = secureStorage.getCookie()
if (cookie == null) {
Toast.makeText(this, "Authentication required", Toast.LENGTH_SHORT).show()
finish()
return
}
showLoading(true)
lifecycleScope.launch {
// Load outstanding bills first
when (val billsResult = apiService.getOutstandingBills("connect.sid=$cookie")) {
is ApiResult.Success -> {
outstandingBills = billsResult.data
updateBillsUI()
// Then load payment methods
when (val gatewaysResult = apiService.getPaymentGateways("connect.sid=$cookie")) {
is ApiResult.Success -> {
paymentMethodAdapter.updatePaymentMethods(gatewaysResult.data)
showLoading(false)
}
is ApiResult.Error -> {
Log.e(TAG, "Failed to load payment gateways: ${gatewaysResult.message}")
showLoading(false)
Toast.makeText(this@PaymentReviewActivity, "Failed to load payment methods", Toast.LENGTH_SHORT).show()
}
}
}
is ApiResult.Error -> {
Log.e(TAG, "Failed to load outstanding bills: ${billsResult.message}")
showLoading(false)
Toast.makeText(this@PaymentReviewActivity, "Failed to load bills", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun updateBillsUI() {
billSummaryAdapter.updateBills(outstandingBills)
// Calculate total bills count
val totalBillCount = outstandingBills.sumOf { bill ->
try {
bill.outstandingBillCount.toInt()
} catch (e: NumberFormatException) {
0
}
}
// Calculate total amount
val totalAmount = outstandingBills.sumOf { bill ->
try {
bill.outstandingAmount.toDouble()
} catch (e: NumberFormatException) {
0.0
}
}
binding.textBillCount.text = "You are paying a total of $totalBillCount bills"
binding.textTotalAmount.text = "MVR ${String.format("%.2f", totalAmount)}"
}
private fun showLoading(show: Boolean) {
binding.layoutLoading.visibility = if (show) View.VISIBLE else View.GONE
binding.recyclerPaymentMethods.visibility = if (show) View.GONE else View.VISIBLE
}
private fun onPaymentMethodSelected(paymentMethod: PaymentGateway) {
Log.d(TAG, "Payment method selected: ${paymentMethod.name} (${paymentMethod.code})")
// Show payment confirmation dialog
PaymentConfirmationDialog.show(
context = this,
paymentGateway = paymentMethod,
onContinue = { dialog ->
if (paymentMethod.code == "bml") {
processBmlPayment(dialog)
} else {
Toast.makeText(this, "Not yet implemented.. use BML", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
},
onCancel = {
Log.d(TAG, "Payment cancelled by user")
}
)
}
private fun processBmlPayment(dialog: PaymentConfirmationDialog) {
val cookie = secureStorage.getCookie()
if (cookie == null) {
Toast.makeText(this, "Authentication required", Toast.LENGTH_SHORT).show()
dialog.dismiss()
return
}
Log.d(TAG, "Processing BML payment...")
dialog.showLoading(true)
lifecycleScope.launch {
try {
// Create payment items from outstanding bills
val paymentItems = outstandingBills.map { bill ->
PaymentItem(
id = bill.subscriptionId,
type = "subscription",
amount = bill.outstandingAmount.toDoubleOrNull() ?: 0.0
)
}
val paymentRequest = PaymentRequest(
gateway = "bml",
details = PaymentDetails(items = paymentItems)
)
// Step 1: Initiate payment
when (val paymentResult = apiService.initiatePayment(paymentRequest, "connect.sid=$cookie")) {
is ApiResult.Success -> {
val paymentResponse = paymentResult.data
Log.d(TAG, "Payment initiated successfully. ID: ${paymentResponse.id}")
// Calculate total amount
val totalAmount = paymentItems.sumOf { it.amount }
// Step 2: Get redirect URL
when (val redirectResult = apiService.getPaymentRedirectUrl(
gateway = "bml",
reference = paymentResponse.id,
amount = totalAmount,
cookie = "connect.sid=$cookie"
)) {
is ApiResult.Success -> {
val redirectUrl = redirectResult.data
Log.d(TAG, "Got redirect URL: $redirectUrl")
// Open browser with redirect URL
openBrowser(redirectUrl)
dialog.dismiss()
// Take user back (finish activity)
finish()
}
is ApiResult.Error -> {
Log.e(TAG, "Failed to get redirect URL: ${redirectResult.message}")
dialog.showLoading(false)
Toast.makeText(this@PaymentReviewActivity, "Failed to get payment URL", Toast.LENGTH_SHORT).show()
}
}
}
is ApiResult.Error -> {
Log.e(TAG, "Failed to initiate payment: ${paymentResult.message}")
dialog.showLoading(false)
Toast.makeText(this@PaymentReviewActivity, "Failed to initiate payment", Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
Log.e(TAG, "Error processing BML payment", e)
dialog.showLoading(false)
Toast.makeText(this@PaymentReviewActivity, "Payment processing error", Toast.LENGTH_SHORT).show()
}
}
}
private fun openBrowser(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
Log.d(TAG, "Opened browser with URL: $url")
} catch (e: Exception) {
Log.e(TAG, "Failed to open browser", e)
Toast.makeText(this, "Failed to open browser", Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -133,11 +133,48 @@ data class MeterReading(
// Outstanding bills data model
data class OutstandingBill(
val id: Long,
val billNumber: String,
val billAmount: String,
val dueDate: String,
val branchName: String,
val subscriptionId: Long,
val serviceId: Int
val subscriptionStatus: String,
val subscriptionNumber: String,
val outstandingBillCount: String,
val billAmount: String,
val paidAmount: String,
val outstandingAmount: String
)
// Payment gateway data models
data class PaymentGateway(
val code: String,
val name: String,
val image: String
)
// Payment request/response models
data class PaymentItem(
val id: Long,
val type: String,
val amount: Double
)
data class PaymentDetails(
val items: List<PaymentItem>
)
data class PaymentRequest(
val gateway: String,
val details: PaymentDetails
)
data class PaymentResponse(
val id: Long,
val customerId: Long,
val gateway: String,
val details: PaymentDetails,
val status: String,
val updatedAt: String,
val createdAt: String,
val deletedAt: String?
)
// Band rates data models

View File

@@ -18,6 +18,9 @@ import sh.sar.gridflow.data.ForgotPasswordResponse
import sh.sar.gridflow.data.LoginRequest
import sh.sar.gridflow.data.LoginResponse
import sh.sar.gridflow.data.OutstandingBill
import sh.sar.gridflow.data.PaymentGateway
import sh.sar.gridflow.data.PaymentRequest
import sh.sar.gridflow.data.PaymentResponse
import sh.sar.gridflow.data.SignupRequest
import sh.sar.gridflow.data.SignupResponse
import sh.sar.gridflow.data.UsageReading
@@ -335,6 +338,44 @@ class FenakaApiService {
}
}
suspend fun getPaymentGateways(cookie: String): ApiResult<List<PaymentGateway>> = withContext(Dispatchers.IO) {
Log.d(TAG, "Fetching payment gateways")
val request = Request.Builder()
.url("$BASE_URL/api/gateway-list/")
.get()
.header("Authorization", "Bearer $BEARER_TOKEN")
.header("Cookie", cookie)
.header("Host", "api.fenaka.mv")
.header("User-Agent", "Dart/3.3 (dart:io)")
.build()
try {
val response = client.newCall(request).execute()
Log.d(TAG, "Payment gateways response code: ${response.code}")
when (response.code) {
200 -> {
val responseBody = response.body?.string() ?: "[]"
Log.d(TAG, "Payment gateways response: $responseBody")
val gateways = gson.fromJson(responseBody, Array<PaymentGateway>::class.java).toList()
Log.d(TAG, "Found ${gateways.size} payment gateways")
ApiResult.Success(gateways, null)
}
else -> {
Log.d(TAG, "Failed to fetch payment gateways: ${response.code}")
ApiResult.Error("Failed to fetch payment gateways", response.code)
}
}
} catch (e: IOException) {
Log.e(TAG, "Network error during payment gateways fetch", e)
ApiResult.Error("Network error: ${e.message}", -1)
} catch (e: Exception) {
Log.e(TAG, "Unexpected error during payment gateways fetch", e)
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
suspend fun getBandRates(cookie: String): ApiResult<BandRatesResponse> = withContext(Dispatchers.IO) {
Log.d(TAG, "Fetching band rates")
@@ -507,6 +548,98 @@ class FenakaApiService {
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
suspend fun initiatePayment(paymentRequest: PaymentRequest, cookie: String): ApiResult<PaymentResponse> = withContext(Dispatchers.IO) {
Log.d(TAG, "Initiating payment for gateway: ${paymentRequest.gateway}")
val requestBody = gson.toJson(paymentRequest).toRequestBody(JSON_MEDIA_TYPE.toMediaType())
val request = Request.Builder()
.url("$BASE_URL/api/payments")
.post(requestBody)
.header("Authorization", "Bearer $BEARER_TOKEN")
.header("Content-Type", "application/json")
.header("Cookie", cookie)
.header("Host", "api.fenaka.mv")
.header("User-Agent", "Dart/3.3 (dart:io)")
.build()
try {
val response = client.newCall(request).execute()
Log.d(TAG, "Payment initiation response code: ${response.code}")
when (response.code) {
200, 201 -> {
val responseBody = response.body?.string() ?: "{}"
Log.d(TAG, "Payment initiation response: $responseBody")
val paymentResponse = gson.fromJson(responseBody, PaymentResponse::class.java)
ApiResult.Success(paymentResponse, null)
}
else -> {
Log.d(TAG, "Failed to initiate payment: ${response.code}")
ApiResult.Error("Failed to initiate payment", response.code)
}
}
} catch (e: IOException) {
Log.e(TAG, "Network error during payment initiation", e)
ApiResult.Error("Network error: ${e.message}", -1)
} catch (e: Exception) {
Log.e(TAG, "Unexpected error during payment initiation", e)
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
suspend fun getPaymentRedirectUrl(gateway: String, reference: Long, amount: Double, cookie: String): ApiResult<String> = withContext(Dispatchers.IO) {
Log.d(TAG, "Getting payment redirect URL for gateway: $gateway, reference: $reference")
val request = Request.Builder()
.url("$BASE_URL/pay/home?gateway=$gateway&reference=$reference&amount=$amount")
.get()
.header("Authorization", "Bearer $BEARER_TOKEN")
.header("Cookie", cookie)
.header("Host", "api.fenaka.mv")
.header("User-Agent", "Dart/3.3 (dart:io)")
.build()
try {
val response = client.newCall(request).execute()
Log.d(TAG, "Payment redirect response code: ${response.code}")
when (response.code) {
200 -> {
val responseBody = response.body?.string() ?: ""
Log.d(TAG, "Payment redirect HTML length: ${responseBody.length}")
// Parse HTML to extract redirect URL from JavaScript
val redirectUrl = extractRedirectUrlFromHtml(responseBody)
if (redirectUrl != null) {
Log.d(TAG, "Extracted redirect URL: $redirectUrl")
ApiResult.Success(redirectUrl, null)
} else {
Log.e(TAG, "Failed to extract redirect URL from HTML")
ApiResult.Error("Failed to extract redirect URL", -1)
}
}
else -> {
Log.d(TAG, "Failed to get payment redirect: ${response.code}")
ApiResult.Error("Failed to get payment redirect", response.code)
}
}
} catch (e: IOException) {
Log.e(TAG, "Network error during payment redirect fetch", e)
ApiResult.Error("Network error: ${e.message}", -1)
} catch (e: Exception) {
Log.e(TAG, "Unexpected error during payment redirect fetch", e)
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
private fun extractRedirectUrlFromHtml(html: String): String? {
// Extract URL from JavaScript: document.location = 'URL';
val regex = Regex("document\\.location\\s*=\\s*['\"]([^'\"]+)['\"]")
val matchResult = regex.find(html)
return matchResult?.groupValues?.get(1)
}
}
sealed class ApiResult<T> {

View File

@@ -51,6 +51,13 @@ class HomeFragment : Fragment() {
layoutManager = LinearLayoutManager(requireContext())
adapter = subscriptionsAdapter
}
// Setup Pay now button click listener
binding.btnPayNow.setOnClickListener {
// Navigate to payment review activity
val intent = android.content.Intent(requireContext(), sh.sar.gridflow.PaymentReviewActivity::class.java)
startActivity(intent)
}
}
private fun setupObservers() {
@@ -71,6 +78,33 @@ class HomeFragment : Fragment() {
}
}
// Outstanding amount
homeViewModel.totalOutstandingAmount.observe(viewLifecycleOwner) { amount ->
if (amount > 0) {
binding.textOutstandingAmount.text = "Outstanding MVR %.2f".format(amount)
binding.textOutstandingAmount.visibility = View.VISIBLE
} else {
binding.textOutstandingAmount.visibility = View.GONE
}
}
// Pending bills styling
homeViewModel.hasPendingBills.observe(viewLifecycleOwner) { hasPendingBills ->
if (hasPendingBills) {
// Red styling for pending bills
binding.layoutBillsContent.setBackgroundColor(android.graphics.Color.parseColor("#FFEBEE"))
binding.textBillsStatus.setTextColor(android.graphics.Color.parseColor("#C62828"))
binding.textOutstandingAmount.setTextColor(android.graphics.Color.parseColor("#C62828"))
binding.btnPayNow.visibility = View.VISIBLE
} else {
// Green styling for no pending bills
binding.layoutBillsContent.setBackgroundColor(android.graphics.Color.parseColor("#E8F5E8"))
binding.textBillsStatus.setTextColor(android.graphics.Color.parseColor("#2E7D32"))
binding.textOutstandingAmount.setTextColor(android.graphics.Color.parseColor("#2E7D32"))
binding.btnPayNow.visibility = View.GONE
}
}
// Subscriptions
homeViewModel.subscriptions.observe(viewLifecycleOwner) { subscriptions ->
subscriptionsAdapter.updateSubscriptions(subscriptions)

View File

@@ -34,6 +34,12 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val _isLoadingBills = MutableLiveData<Boolean>()
val isLoadingBills: LiveData<Boolean> = _isLoadingBills
private val _totalOutstandingAmount = MutableLiveData<Double>()
val totalOutstandingAmount: LiveData<Double> = _totalOutstandingAmount
private val _hasPendingBills = MutableLiveData<Boolean>()
val hasPendingBills: LiveData<Boolean> = _hasPendingBills
// Subscriptions
private val _subscriptions = MutableLiveData<List<CustomerSubscription>>()
val subscriptions: LiveData<List<CustomerSubscription>> = _subscriptions
@@ -97,8 +103,32 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
_isLoadingBills.value = false
if (result.data.isEmpty()) {
_billsStatus.value = "You don't have any pending bills left"
_hasPendingBills.value = false
_totalOutstandingAmount.value = 0.0
} else {
_billsStatus.value = "You have ${result.data.size} pending bills"
// Calculate total outstanding amount
val totalAmount = result.data.sumOf { bill ->
try {
bill.outstandingAmount.toDouble()
} catch (e: NumberFormatException) {
Log.w(TAG, "Invalid outstanding amount: ${bill.outstandingAmount}")
0.0
}
}
// Calculate total pending bill count
val totalBillCount = result.data.sumOf { bill ->
try {
bill.outstandingBillCount.toInt()
} catch (e: NumberFormatException) {
Log.w(TAG, "Invalid bill count: ${bill.outstandingBillCount}")
0
}
}
_billsStatus.value = "You have $totalBillCount pending bills"
_hasPendingBills.value = true
_totalOutstandingAmount.value = totalAmount
}
}
is ApiResult.Error -> {

View File

@@ -0,0 +1,35 @@
package sh.sar.gridflow.ui.payment
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import sh.sar.gridflow.data.OutstandingBill
import sh.sar.gridflow.databinding.ItemBillSummaryBinding
class BillSummaryAdapter(
private var bills: List<OutstandingBill> = emptyList()
) : RecyclerView.Adapter<BillSummaryAdapter.BillViewHolder>() {
inner class BillViewHolder(private val binding: ItemBillSummaryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(bill: OutstandingBill) {
binding.textSubscriptionNumber.text = bill.subscriptionNumber
binding.textDueAmount.text = "MVR ${String.format("%.2f", bill.outstandingAmount.toDoubleOrNull() ?: 0.0)}"
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BillViewHolder {
val binding = ItemBillSummaryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BillViewHolder(binding)
}
override fun onBindViewHolder(holder: BillViewHolder, position: Int) {
holder.bind(bills[position])
}
override fun getItemCount(): Int = bills.size
fun updateBills(newBills: List<OutstandingBill>) {
bills = newBills
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,94 @@
package sh.sar.gridflow.ui.payment
import android.content.Context
import android.graphics.BitmapFactory
import android.util.Base64
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import sh.sar.gridflow.data.PaymentGateway
import sh.sar.gridflow.databinding.DialogPaymentConfirmationBinding
class PaymentConfirmationDialog private constructor(
private val context: Context,
private val paymentGateway: PaymentGateway,
private val onContinue: (dialog: PaymentConfirmationDialog) -> Unit,
private val onCancel: () -> Unit
) {
companion object {
private const val TAG = "PaymentConfirmationDialog"
fun show(
context: Context,
paymentGateway: PaymentGateway,
onContinue: (dialog: PaymentConfirmationDialog) -> Unit,
onCancel: () -> Unit
): PaymentConfirmationDialog {
val dialog = PaymentConfirmationDialog(context, paymentGateway, onContinue, onCancel)
dialog.show()
return dialog
}
}
private val binding = DialogPaymentConfirmationBinding.inflate(LayoutInflater.from(context))
private val alertDialog: AlertDialog
init {
// Set gateway name
binding.textGatewayName.text = paymentGateway.name
// Decode and set gateway logo
if (paymentGateway.image.isNotEmpty()) {
try {
val imageBytes = Base64.decode(paymentGateway.image, Base64.DEFAULT)
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
binding.imageGatewayLogo.setImageBitmap(bitmap)
} catch (e: Exception) {
Log.e(TAG, "Failed to decode base64 image for ${paymentGateway.name}", e)
// Keep default background if image fails to decode
}
}
alertDialog = MaterialAlertDialogBuilder(context)
.setView(binding.root)
.setCancelable(true)
.create()
// Set button listeners
binding.btnContinue.setOnClickListener {
onContinue(this)
}
binding.btnCancel.setOnClickListener {
onCancel()
dismiss()
}
alertDialog.setOnCancelListener {
onCancel()
}
}
fun show() {
alertDialog.show()
}
fun dismiss() {
alertDialog.dismiss()
}
fun showLoading(show: Boolean) {
if (show) {
binding.layoutLoading.visibility = View.VISIBLE
binding.layoutButtons.visibility = View.GONE
alertDialog.setCancelable(false)
} else {
binding.layoutLoading.visibility = View.GONE
binding.layoutButtons.visibility = View.VISIBLE
alertDialog.setCancelable(true)
}
}
}

View File

@@ -0,0 +1,58 @@
package sh.sar.gridflow.ui.payment
import android.graphics.BitmapFactory
import android.util.Base64
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import sh.sar.gridflow.data.PaymentGateway
import sh.sar.gridflow.databinding.ItemPaymentMethodBinding
class PaymentMethodAdapter(
private var paymentMethods: List<PaymentGateway> = emptyList(),
private val onMethodClick: (PaymentGateway) -> Unit
) : RecyclerView.Adapter<PaymentMethodAdapter.PaymentMethodViewHolder>() {
companion object {
private const val TAG = "PaymentMethodAdapter"
}
inner class PaymentMethodViewHolder(private val binding: ItemPaymentMethodBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(paymentMethod: PaymentGateway) {
binding.textPaymentName.text = paymentMethod.name
// Decode base64 image if available
if (paymentMethod.image.isNotEmpty()) {
try {
val imageBytes = Base64.decode(paymentMethod.image, Base64.DEFAULT)
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
binding.imagePaymentLogo.setImageBitmap(bitmap)
} catch (e: Exception) {
Log.e(TAG, "Failed to decode base64 image for ${paymentMethod.name}", e)
// Keep default background if image fails to decode
}
}
binding.root.setOnClickListener {
onMethodClick(paymentMethod)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentMethodViewHolder {
val binding = ItemPaymentMethodBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PaymentMethodViewHolder(binding)
}
override fun onBindViewHolder(holder: PaymentMethodViewHolder, position: Int) {
holder.bind(paymentMethods[position])
}
override fun getItemCount(): Int = paymentMethods.size
fun updatePaymentMethods(newMethods: List<PaymentGateway>) {
paymentMethods = newMethods
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="?android:attr/textColorSecondary" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#2196F3" />
<corners android:radius="8dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?android:attr/colorBackground" />
<corners android:radius="12dp" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#D32F2F" />
<corners android:radius="8dp" />
</shape>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:background="?android:attr/colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Header -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Review your payment"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center"
android:layout_marginBottom="32dp" />
<!-- Bills List -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_bills"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
tools:listitem="@layout/item_bill_summary" />
<!-- Bill Count Summary -->
<TextView
android:id="@+id/text_bill_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="16dp"
tools:text="You are paying a total of 2 bills" />
<!-- Total Amount Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Total"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/text_total_amount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
tools:text="MVR 3,578.30" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Payment Methods Section -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Select a payment method"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="16dp" />
<!-- Payment Methods RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_payment_methods"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_payment_method" />
<!-- Loading State -->
<LinearLayout
android:id="@+id/layout_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp"
android:visibility="gone">
<ProgressBar
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Loading payment methods..."
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:minWidth="300dp">
<!-- Header -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Proceed to Payment Gateway"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center"
android:layout_marginBottom="20dp" />
<!-- Gateway Logo and Name -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="20dp">
<ImageView
android:id="@+id/image_gateway_logo"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:scaleType="fitCenter"
android:background="@drawable/payment_logo_background"
tools:src="@drawable/ic_placeholder" />
<TextView
android:id="@+id/text_gateway_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
tools:text="Bank of Maldives" />
</LinearLayout>
<!-- Instructions -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Your browser will open to complete the payment. Please finish the payment process and return to this app."
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="24dp"
android:lineSpacingExtra="2dp" />
<!-- Loading State -->
<LinearLayout
android:id="@+id/layout_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:visibility="gone"
android:layout_marginBottom="16dp">
<ProgressBar
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Loading payment gateway..."
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
<!-- Buttons -->
<LinearLayout
android:id="@+id/layout_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancel"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:background="@drawable/cancel_button_background"
android:layout_marginEnd="12dp"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:minWidth="80dp" />
<Button
android:id="@+id/btn_continue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Continue"
android:textColor="@android:color/white"
android:textSize="14sp"
android:background="@drawable/continue_button_background"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:minWidth="80dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -36,29 +36,68 @@
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/layout_bills_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:orientation="vertical"
android:padding="16dp"
android:background="#E8F5E8"
android:gravity="center_vertical">
android:background="#E8F5E8">
<TextView
android:id="@+id/text_bills_status"
android:layout_width="0dp"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="#2E7D32"
android:textStyle="bold"
tools:text="You don't have any pending bills left" />
android:orientation="horizontal"
android:gravity="center_vertical">
<ProgressBar
android:id="@+id/progress_bills"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"
android:indeterminateTint="#2E7D32" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text_bills_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#2E7D32"
android:textStyle="bold"
tools:text="You don't have any pending bills left" />
<TextView
android:id="@+id/text_outstanding_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textColor="#2E7D32"
android:visibility="gone"
tools:text="Outstanding MVR 3578.30" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bills"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"
android:indeterminateTint="#2E7D32" />
<Button
android:id="@+id/btn_pay_now"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginStart="12dp"
android:text="Pay now"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="bold"
android:background="@drawable/red_button_background"
android:paddingHorizontal="16dp"
android:visibility="gone"
android:minWidth="80dp" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Subscription no:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/text_subscription_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
tools:text="4995" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Due amount:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/text_due_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
tools:text="MVR 1,945.36" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/image_payment_logo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="16dp"
android:scaleType="fitCenter"
android:background="@drawable/payment_logo_background"
tools:src="@drawable/ic_placeholder" />
<TextView
android:id="@+id/text_payment_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
tools:text="Bank of Maldives" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>