diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 73c6e5a..a1a4af0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -72,6 +72,11 @@
android:exported="false"
android:label="Pay Without Account"
android:theme="@style/Theme.GridFlow.NoActionBar" />
+
= 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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sh/sar/gridflow/data/Models.kt b/app/src/main/java/sh/sar/gridflow/data/Models.kt
index 0187f21..ecc479a 100644
--- a/app/src/main/java/sh/sar/gridflow/data/Models.kt
+++ b/app/src/main/java/sh/sar/gridflow/data/Models.kt
@@ -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
+)
+
+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
diff --git a/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt
index 2822cf8..a86568e 100644
--- a/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt
+++ b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt
@@ -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> = 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::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 = 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 = 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 = 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 {
diff --git a/app/src/main/java/sh/sar/gridflow/ui/home/HomeFragment.kt b/app/src/main/java/sh/sar/gridflow/ui/home/HomeFragment.kt
index 3b295ca..22c0a4b 100644
--- a/app/src/main/java/sh/sar/gridflow/ui/home/HomeFragment.kt
+++ b/app/src/main/java/sh/sar/gridflow/ui/home/HomeFragment.kt
@@ -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)
diff --git a/app/src/main/java/sh/sar/gridflow/ui/home/HomeViewModel.kt b/app/src/main/java/sh/sar/gridflow/ui/home/HomeViewModel.kt
index 1578497..063a564 100644
--- a/app/src/main/java/sh/sar/gridflow/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/sh/sar/gridflow/ui/home/HomeViewModel.kt
@@ -34,6 +34,12 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val _isLoadingBills = MutableLiveData()
val isLoadingBills: LiveData = _isLoadingBills
+ private val _totalOutstandingAmount = MutableLiveData()
+ val totalOutstandingAmount: LiveData = _totalOutstandingAmount
+
+ private val _hasPendingBills = MutableLiveData()
+ val hasPendingBills: LiveData = _hasPendingBills
+
// Subscriptions
private val _subscriptions = MutableLiveData>()
val subscriptions: LiveData> = _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 -> {
diff --git a/app/src/main/java/sh/sar/gridflow/ui/payment/BillSummaryAdapter.kt b/app/src/main/java/sh/sar/gridflow/ui/payment/BillSummaryAdapter.kt
new file mode 100644
index 0000000..cbd6015
--- /dev/null
+++ b/app/src/main/java/sh/sar/gridflow/ui/payment/BillSummaryAdapter.kt
@@ -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 = emptyList()
+) : RecyclerView.Adapter() {
+
+ 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) {
+ bills = newBills
+ notifyDataSetChanged()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sh/sar/gridflow/ui/payment/PaymentConfirmationDialog.kt b/app/src/main/java/sh/sar/gridflow/ui/payment/PaymentConfirmationDialog.kt
new file mode 100644
index 0000000..8d0ff0a
--- /dev/null
+++ b/app/src/main/java/sh/sar/gridflow/ui/payment/PaymentConfirmationDialog.kt
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sh/sar/gridflow/ui/payment/PaymentMethodAdapter.kt b/app/src/main/java/sh/sar/gridflow/ui/payment/PaymentMethodAdapter.kt
new file mode 100644
index 0000000..cec5e98
--- /dev/null
+++ b/app/src/main/java/sh/sar/gridflow/ui/payment/PaymentMethodAdapter.kt
@@ -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 = emptyList(),
+ private val onMethodClick: (PaymentGateway) -> Unit
+) : RecyclerView.Adapter() {
+
+ 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) {
+ paymentMethods = newMethods
+ notifyDataSetChanged()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/cancel_button_background.xml b/app/src/main/res/drawable/cancel_button_background.xml
new file mode 100644
index 0000000..e624800
--- /dev/null
+++ b/app/src/main/res/drawable/cancel_button_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/continue_button_background.xml b/app/src/main/res/drawable/continue_button_background.xml
new file mode 100644
index 0000000..159e2aa
--- /dev/null
+++ b/app/src/main/res/drawable/continue_button_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dialog_background.xml b/app/src/main/res/drawable/dialog_background.xml
new file mode 100644
index 0000000..d59f90e
--- /dev/null
+++ b/app/src/main/res/drawable/dialog_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/payment_logo_background.xml b/app/src/main/res/drawable/payment_logo_background.xml
new file mode 100644
index 0000000..ced1674
--- /dev/null
+++ b/app/src/main/res/drawable/payment_logo_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/red_button_background.xml b/app/src/main/res/drawable/red_button_background.xml
new file mode 100644
index 0000000..f935c96
--- /dev/null
+++ b/app/src/main/res/drawable/red_button_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_payment_review.xml b/app/src/main/res/layout/activity_payment_review.xml
new file mode 100644
index 0000000..a3bc7d9
--- /dev/null
+++ b/app/src/main/res/layout/activity_payment_review.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_payment_confirmation.xml b/app/src/main/res/layout/dialog_payment_confirmation.xml
new file mode 100644
index 0000000..7c73344
--- /dev/null
+++ b/app/src/main/res/layout/dialog_payment_confirmation.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 9843e68..c4d064d 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -36,29 +36,68 @@
app:cardElevation="2dp">
+ android:background="#E8F5E8">
-
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_bill_summary.xml b/app/src/main/res/layout/item_bill_summary.xml
new file mode 100644
index 0000000..fde2888
--- /dev/null
+++ b/app/src/main/res/layout/item_bill_summary.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_payment_method.xml b/app/src/main/res/layout/item_payment_method.xml
new file mode 100644
index 0000000..3de23f5
--- /dev/null
+++ b/app/src/main/res/layout/item_payment_method.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file