From d39bd62c6d84bee7d37f997ca8970520518f4d43 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Sat, 26 Jul 2025 14:27:49 +0500 Subject: [PATCH] fix dashboard bills display and add payment support --- app/src/main/AndroidManifest.xml | 5 + .../sh/sar/gridflow/PaymentReviewActivity.kt | 250 ++++++++++++++++++ .../main/java/sh/sar/gridflow/data/Models.kt | 45 +++- .../sar/gridflow/network/FenakaApiService.kt | 133 ++++++++++ .../sh/sar/gridflow/ui/home/HomeFragment.kt | 34 +++ .../sh/sar/gridflow/ui/home/HomeViewModel.kt | 32 ++- .../gridflow/ui/payment/BillSummaryAdapter.kt | 35 +++ .../ui/payment/PaymentConfirmationDialog.kt | 94 +++++++ .../ui/payment/PaymentMethodAdapter.kt | 58 ++++ .../res/drawable/cancel_button_background.xml | 9 + .../drawable/continue_button_background.xml | 6 + .../main/res/drawable/dialog_background.xml | 6 + .../res/drawable/payment_logo_background.xml | 9 + .../res/drawable/red_button_background.xml | 6 + .../res/layout/activity_payment_review.xml | 123 +++++++++ .../layout/dialog_payment_confirmation.xml | 126 +++++++++ app/src/main/res/layout/fragment_home.xml | 73 +++-- app/src/main/res/layout/item_bill_summary.xml | 58 ++++ .../main/res/layout/item_payment_method.xml | 41 +++ 19 files changed, 1121 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/sh/sar/gridflow/PaymentReviewActivity.kt create mode 100644 app/src/main/java/sh/sar/gridflow/ui/payment/BillSummaryAdapter.kt create mode 100644 app/src/main/java/sh/sar/gridflow/ui/payment/PaymentConfirmationDialog.kt create mode 100644 app/src/main/java/sh/sar/gridflow/ui/payment/PaymentMethodAdapter.kt create mode 100644 app/src/main/res/drawable/cancel_button_background.xml create mode 100644 app/src/main/res/drawable/continue_button_background.xml create mode 100644 app/src/main/res/drawable/dialog_background.xml create mode 100644 app/src/main/res/drawable/payment_logo_background.xml create mode 100644 app/src/main/res/drawable/red_button_background.xml create mode 100644 app/src/main/res/layout/activity_payment_review.xml create mode 100644 app/src/main/res/layout/dialog_payment_confirmation.xml create mode 100644 app/src/main/res/layout/item_bill_summary.xml create mode 100644 app/src/main/res/layout/item_payment_method.xml 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +