chats: business payments, new bubbles, pinning, Scan to Pay

- Card / Scan to Pay payments show as business chats (from BML
  "Payment Completed" alerts and "Purchase" history), kept apart from
  people; static merchant QRs are kept so a business can be paid again
  from its chat, with a card picker
- Scan to Pay button on the chat list; returns and re-checks after paying
- Compact bubbles with ticks (alert seen / booked in history), receipt
  button, day totals, monthly summary, long-press actions
- Personal accounts confirm with just the dialog over the chat; business
  profiles keep the sheet for the OTP step
- From chip shows account number and balance; MVR -> USD account is
  blocked, USD -> MVR warns about conversion
- Long-press a chat to pin it to the top

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQUrmJjrypeoPsubpzuezC
This commit is contained in:
2026-09-26 04:10:06 +05:00
co-authored by Claude Opus 5.5
parent da5cca79c6
commit 83ca37eade
19 changed files with 1089 additions and 252 deletions
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="?attr/colorSurfaceContainerHigh" />
<stroke android:width="1dp" android:color="?attr/colorOutlineVariant" />
<corners android:radius="10dp" />
<stroke android:width="1dp" android:color="?attr/colorOutline" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="14dp" />
<solid android:color="?attr/colorSurfaceContainerHigh" />
</shape>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSurfaceVariant"
android:pathData="M16,12V4h1V2H7v2h1v8l-2,2v2h5.2v6h1.6v-6H18v-2l-2,-2z" />
</vector>
+12
View File
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:pathData="M4,9l1.5,-5h13L20,9M4,9v11h16V9M4,9h16M9,20v-6h6v6" />
</vector>
+150 -47
View File
@@ -10,7 +10,8 @@
android:id="@+id/toAccountBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:background="?attr/colorSecondaryContainer"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
@@ -52,7 +53,7 @@
android:id="@+id/tvToCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_chat_date"
android:background="@drawable/bg_chat_chip"
android:paddingHorizontal="10dp"
android:paddingVertical="3dp"
android:textAppearance="?attr/textAppearanceLabelLarge"
@@ -101,6 +102,88 @@
</LinearLayout>
<LinearLayout
android:id="@+id/summaryCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="8dp"
android:background="@drawable/bg_chat_summary"
android:orientation="horizontal"
android:paddingHorizontal="14dp"
android:paddingVertical="10dp"
android:visibility="gone">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvSumLabel1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvSumValue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/sumColumn2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvSumLabel2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvSumValue2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/sumColumn3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvSumLabel3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvSumValue3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMessages"
android:layout_width="match_parent"
@@ -135,76 +218,78 @@
android:paddingBottom="8dp">
<TextView
android:id="@+id/tvCurrencyWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginStart="4dp"
android:text="@string/chat_from_account"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorError"
android:visibility="gone" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardFromAccount"
<com.google.android.material.button.MaterialButton
android:id="@+id/btnFrom"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingEnd="12dp">
<include
android:id="@+id/fromAccountRow"
layout="@layout/item_account_dropdown"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:rotation="90"
android:src="@drawable/ic_arrow_right"
android:importantForAccessibility="no" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
android:gravity="start|center_vertical"
android:ellipsize="middle"
android:maxLines="1"
android:textColor="?attr/colorOnSecondaryContainer"
app:icon="@drawable/ic_arrow_right"
app:iconGravity="end"
app:iconSize="16dp"
app:iconTint="?attr/colorOnSecondaryContainer" />
<LinearLayout
android:id="@+id/amountRow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAmount"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_weight="1.2"
android:hint="@string/chat_amount"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="28dp"
app:boxCornerRadiusTopEnd="28dp"
app:boxCornerRadiusBottomEnd="28dp"
app:boxCornerRadiusBottomStart="28dp"
app:boxCornerRadiusBottomEnd="28dp">
app:boxCornerRadiusTopEnd="28dp"
app:boxCornerRadiusTopStart="28dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="numberDecimal"
android:maxLines="1"
android:imeOptions="actionSend" />
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilNote"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:hint="@string/chat_note"
app:boxCornerRadiusBottomEnd="28dp"
app:boxCornerRadiusBottomStart="28dp"
app:boxCornerRadiusTopEnd="28dp"
app:boxCornerRadiusTopStart="28dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etNote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionSend"
android:inputType="textCapSentences"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
@@ -224,4 +309,22 @@
</LinearLayout>
<!-- Hosts the Transfer page off-screen when only its confirm dialog is wanted. -->
<FrameLayout
android:id="@+id/hiddenTransferHost"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPayAgain"
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="12dp"
android:text="@string/chat_pay_again"
android:visibility="gone"
app:cornerRadius="28dp" />
</LinearLayout>
+16 -1
View File
@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -64,3 +68,14 @@
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabScanPay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_margin="16dp"
android:contentDescription="@string/chat_scan_to_pay"
app:srcCompat="@drawable/ic_qr_scan" />
</FrameLayout>
+13 -1
View File
@@ -55,8 +55,20 @@
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@id/ivAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivPin"
app:layout_constraintTop_toBottomOf="@id/tvName"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageView
android:id="@+id/ivPin"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="8dp"
android:contentDescription="@string/chat_pinned"
android:src="@drawable/ic_pin"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvPreview"
app:layout_constraintBottom_toBottomOf="@id/tvPreview" />
</androidx.constraintlayout.widget.ConstraintLayout>
+59 -50
View File
@@ -6,23 +6,24 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="4dp">
android:paddingVertical="3dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardBubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="200dp"
app:cardCornerRadius="18dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="200dp"
android:minWidth="150dp"
android:orientation="vertical"
android:padding="14dp">
android:paddingStart="14dp"
android:paddingTop="10dp"
android:paddingEnd="14dp"
android:paddingBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
@@ -31,70 +32,78 @@
android:orientation="horizontal">
<TextView
android:id="@+id/tvLabel"
android:layout_width="0dp"
android:id="@+id/tvAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="12dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:maxLines="1"
android:ellipsize="end" />
android:maxWidth="240dp"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textStyle="bold" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@+id/btnReceipt"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="6dp"
android:padding="6dp"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="-8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/chat_view_receipt"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_receipt"
android:visibility="gone" />
<TextView
android:id="@+id/tvBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp"
android:paddingVertical="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall" />
</LinearLayout>
<TextView
android:id="@+id/tvAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceHeadlineSmall" />
<TextView
android:id="@+id/tvAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:alpha="0.8"
android:textAppearance="?attr/textAppearanceBodySmall" />
<TextView
android:id="@+id/tvNote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceBodyMedium" />
android:layout_marginTop="2dp"
android:maxWidth="240dp"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tvAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="150dp"
android:layout_marginEnd="8dp"
android:alpha="0.8"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceLabelSmall" />
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.8"
android:textAppearance="?attr/textAppearanceLabelSmall" />
<TextView
android:id="@+id/tvTicks"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textAppearance="?attr/textAppearanceLabelMedium" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/tvMeta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginHorizontal="6dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
+1 -2
View File
@@ -10,10 +10,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="@drawable/bg_chat_date"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurface" />
android:textColor="?attr/colorOnSurfaceVariant" />
</FrameLayout>
+1
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="action_open_contacts" type="id" />
<item name="action_pin_chat" type="id" />
</resources>
+33 -9
View File
@@ -410,28 +410,52 @@
<string name="chats_no_results">No chats found</string>
<string name="chat_preview_sent">You sent %s</string>
<string name="chat_preview_received">Received %s</string>
<string name="chat_from_label">From %s</string>
<string name="chat_received_label">Received</string>
<string name="chat_sent_label">Sent</string>
<string name="chat_into_label">To %s</string>
<string name="chat_debit">Debit</string>
<string name="chat_credit">Credit</string>
<string name="chat_sent_meta">✓ Sent · %s</string>
<string name="chat_preview_paid">Paid %s</string>
<string name="chat_today">Today</string>
<string name="chat_yesterday">Yesterday</string>
<string name="chat_from_account">From account</string>
<string name="chat_pick_account">Send from</string>
<string name="chat_pick_card">Pay with</string>
<string name="chat_from_chip">%1$s · %2$s · %3$s</string>
<string name="chat_currency_warning">⚠ Sending USD to an MVR account. It will be converted at BML\'s rate and can\'t be reversed.</string>
<string name="chat_currency_blocked">MVR can\'t be sent to a USD account. Pick a USD account to send from.</string>
<string name="chat_cant_send_mvr_to_usd">Can\'t send MVR to a USD account</string>
<string name="chat_needs_usd_source">Needs a USD account to send from</string>
<string name="chat_transfer_sent">Transfer sent</string>
<string name="chat_payment_sent">Payment sent</string>
<string name="chat_view_receipt">Receipt</string>
<string name="chat_not_in_contacts">Not in your contacts</string>
<string name="chat_save_contact">Save contact</string>
<string name="chat_send">Send</string>
<string name="chat_to">To</string>
<string name="chat_pick_recipient_account">Send to</string>
<string name="chat_from_to_label">From %1$s → %2$s</string>
<string name="chat_receipt_missing">Receipt no longer available</string>
<string name="chat_amount">Amount</string>
<string name="chat_amount_invalid">Enter an amount</string>
<string name="chat_account_subtitle">Account %s</string>
<string name="chat_cannot_send">Account number not known for this chat. Save this person as a contact, or send once from Transfer, to send from here.</string>
<string name="chat_into_short">into %s</string>
<string name="chat_account_to">%1$s → %2$s</string>
<string name="chat_tick_seen">Seen in BML alert</string>
<string name="chat_tick_booked">Booked in BML history</string>
<string name="chat_note">Note</string>
<string name="chat_pay_again">Pay again with Scan to Pay</string>
<string name="chat_pay_business">Pay %s</string>
<string name="chat_scan_to_pay">Scan to Pay</string>
<string name="chat_merchant_subtitle">Business · Scan to Pay &amp; card</string>
<string name="chat_sum_sent">%s · sent</string>
<string name="chat_sum_received">received</string>
<string name="chat_sum_net">net</string>
<string name="chat_sum_spent">Spent here in %s</string>
<string name="chat_sum_payments">payments</string>
<string name="chat_send_again">Send again · %s</string>
<string name="chat_view_receipt_full">View receipt</string>
<string name="chat_copy_reference">Copy reference</string>
<string name="chat_reference_copied">Reference copied</string>
<string name="chat_pinned">Pinned</string>
<string name="chat_pin">Pin</string>
<string name="chat_unpin">Unpin</string>
<string name="chat_selected_one">1 selected</string>
<string name="chat_pinned_msg">%s pinned</string>
<string name="chat_unpinned_msg">%s unpinned</string>
<string name="chat_undo">Undo</string>
</resources>