6 Commits

Author SHA1 Message Date
shihaam 255f43db24 release v1.0.22
Auto Tag on Version Change / check-version (push) Successful in 6s
Build and Release APK / build (push) Failing after 19m22s
2026-06-27 19:44:36 +05:00
shihaam 01cae559cf update card image mappgin
Auto Tag on Version Change / check-version (push) Failing after 11m59s
2026-06-27 19:43:03 +05:00
shihaam 015919a4ac disable paymvQR for mfaisa accounts 2026-06-27 19:42:52 +05:00
shihaam 93a7c8bbde fix bug that flashes bottom bar before share sheet shows up
Auto Tag on Version Change / check-version (push) Failing after 12m1s
2026-06-27 19:22:59 +05:00
shihaam 8f4672f269 fix Transfer slip share from the share button #42 2026-06-27 19:20:24 +05:00
shihaam 00e6b40ee0 update docs
Auto Tag on Version Change / check-version (push) Failing after 14m24s
2026-06-27 18:35:38 +05:00
11 changed files with 285 additions and 55 deletions
+2 -2
View File
@@ -21,8 +21,8 @@ android {
applicationId = "sh.sar.basedbank"
minSdk = 26
targetSdk = 36
versionCode = 22
versionName = "1.0.21"
versionCode = 23
versionName = "1.0.22"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -83,6 +83,7 @@ class PayMvQrFragment : Fragment() {
val eligible = accounts.filter {
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN" &&
it.bank != "MIB" && // TODO: MIB does not support PayMV QR
it.bank != "MFAISA" && // TODO: M-Faisa PayMV QR not implemented yet
!(it.bank == "BML" && it.currencyName.contains("USD", ignoreCase = true)) // TODO: BML USD not supported by MMA
}
val adapter = QrAccountAdapter(requireContext(), eligible)
@@ -11,17 +11,13 @@ import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.Base64
import android.view.LayoutInflater
import android.view.PixelCopy
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
@@ -159,9 +155,6 @@ class TransferReceiptFragment : Fragment() {
}
})
view.findViewById<MaterialButton>(R.id.btnDone).setOnClickListener {
parentFragmentManager.popBackStack()
}
view.findViewById<MaterialButton>(R.id.btnShare).setOnClickListener {
shareReceipt()
}
@@ -377,21 +370,19 @@ class TransferReceiptFragment : Fragment() {
// ── Helpers ───────────────────────────────────────────────────────────────
/**
* Captures the receipt card using PixelCopy, which correctly handles
* hardware-accelerated views (avoids the black-square problem with view.draw()).
* Draws the receipt card to an offscreen bitmap at its natural (unscaled)
* dimensions, so the captured image isn't affected by the on-screen scale
* applied to fit small viewports and doesn't pick up overlapping siblings.
*/
private fun captureReceiptBitmap(callback: (Bitmap?) -> Unit) {
val view = _receiptCard ?: run { callback(null); return }
if (view.width == 0 || view.height == 0) { callback(null); return }
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val location = IntArray(2)
view.getLocationInWindow(location)
val srcRect = Rect(location[0], location[1], location[0] + view.width, location[1] + view.height)
PixelCopy.request(requireActivity().window, srcRect, bitmap, { result ->
callback(if (result == PixelCopy.SUCCESS) bitmap else null)
}, Handler(Looper.getMainLooper()))
val canvas = Canvas(bitmap)
canvas.drawColor(Color.WHITE)
view.draw(canvas)
callback(bitmap)
}
private fun formatBmlTimestamp(raw: String): String {
@@ -494,13 +485,9 @@ class TransferReceiptFragment : Fragment() {
(activity as? HomeActivity)?.setBottomNavVisible(false)
}
override fun onPause() {
super.onPause()
(activity as? HomeActivity)?.setBottomNavVisible(true)
}
override fun onDestroyView() {
super.onDestroyView()
(activity as? HomeActivity)?.setBottomNavVisible(true)
_receiptCard = null
pendingToAvatarBitmap = null
}
@@ -36,9 +36,9 @@ object BmlCardParser {
"C8902", "C8907", "C8909", "C8912", "C8992", "C8996", "C8997", "C8982", "C8983" -> "cards/bml/master_islamic.png"
"C8101" -> "cards/bml/master_masveriyaa.png"
"C8102" -> "cards/bml/master_odiveriyaa.png"
"C8010", "C8011" -> "cards/bml/master_platinum.png"
"C8010", "C8011", "C8033" -> "cards/bml/master_platinum.png"
"C8040", "C8044" -> "cards/bml/master_world.png"
"C8030", "C8033" -> "cards/bml/master_business_debit.png"
"C8030" -> "cards/bml/master_business_debit.png"
"C8901", "C8991", "C8980", "C8981" -> "cards/bml/master_passport.png"
"C1090", "C1130", "C1033", "C1133" -> "cards/bml/visa_corporate.png"
"C8905", "C8995" -> "cards/bml/visa_credit.png"
@@ -246,18 +246,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="4dp"
android:layout_marginStart="4dp"
android:text="Save"
app:icon="@drawable/ic_save" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="Done" />
</LinearLayout>
</LinearLayout>
@@ -376,18 +376,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="4dp"
android:layout_marginStart="4dp"
android:text="Save"
app:icon="@drawable/ic_save" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="Done" />
</LinearLayout>
</LinearLayout>
@@ -273,18 +273,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="4dp"
android:layout_marginStart="4dp"
android:text="Save"
app:icon="@drawable/ic_save" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="Done" />
</LinearLayout>
</LinearLayout>
+2 -2
View File
@@ -190,9 +190,9 @@ Known asset mappings:
|---|---|---|
| `C8201`, `C8001`, `C8009` | Mastercard Prepaid | `master_prepaid` |
| `C8205`, `C8005`, `C8008` | Mastercard Prepaid Travel | `master_prepaid_travel` |
| `C8010`, `C8011` | Mastercard Platinum | `master_platinum` |
| `C8010`, `C8011`, `C8033` | Mastercard Platinum | `master_platinum` |
| `C8020`, `C8022` | Mastercard Gold | `master_gold` |
| `C8030`, `C8033` | Mastercard Business Debit | `master_business_debit` |
| `C8030` | Mastercard Business Debit | `master_business_debit` |
| `C8040`, `C8044` | Mastercard World | `master_world` |
| `C8101` | Mastercard Masveriyaa | `master_masveriyaa` |
| `C8102` | Mastercard Odiveriyaa | `master_odiveriyaa` |
+1 -1
View File
@@ -279,4 +279,4 @@ python tmp/mfaisa_transfer.py <myMsisdn> <myMpin> <recipientMsisdn>
---
> **← Back to** [Transaction History](03-history.md) | [README](README.md)
> **← Back to** [Transaction History](03-history.md) | [README](README.md) | **Next →** [QR Merchant Payment](05-qr-pay.md)
+265
View File
@@ -0,0 +1,265 @@
# QR Merchant Payment ("Smart Pay")
Pay an Ooredoo M-Faisa merchant by scanning their QR. The QR encodes only a numeric `qrCodeId` (e.g. `1594103440350`) — no URL, no EMV TLV envelope. The flow is three calls and **does not require OTP** (`2FARequired=NONE`).
> **Currency / pocket constraints:** captures cover MVR→MVR purchases from the user's EMONEY pocket only. The `transactionCurrency` field is taken from the QR lookup; we have not seen a non-MVR variant.
---
## Flow
```
Client Server
| |
| POST /QRCodeUtility/fetchQRCodeById | ← user scanned a QR
| formData = { qrCodeId, tenantCode } |
|------------------------------------------------>|
| [{ success, response:[{ commercialName, |
| customerId, mobileNumber, currencyCode, |
| txnAmount, status, ... }] }] |
|<------------------------------------------------|
| |
| (show merchant, accept amount + remarks)
| |
| POST /initiateNewBuy |
| formData = { merchantId, mobileNumber, |
| sourceDetails, transactionAmount, |
| transactionType:"PURCHASE", … } |
|------------------------------------------------>|
| [{ 2FARequired:"NONE", |
| authenticationType:"NONE", |
| success:true, |
| response:[{ responseObject:{ referenceId, |
| chargeDetails, … } }] }] |
|<------------------------------------------------|
| |
| (no OTP — go straight to confirm)
| |
| POST /confirmNewBuy |
| formData = { referenceId } |
| transactionAuthDetails = "null" ← literal |
|------------------------------------------------>|
| [{ success:true, |
| message:"Payment Completed Successfully", |
| response:[{ responseObject:{ isCompleted, |
| balanceInquiryDTO, ... } }] }] |
|<------------------------------------------------|
```
All three endpoints carry the standard anti-replay pair (`rndValue` + `csValue`) derived from each request's `formData` JSON — see [01-encryption.md → rndValue / csValue](01-encryption.md#anti-replay-envelope-rndvalue--csvalue).
Unlike the transfer flow, **every endpoint here returns its envelope as a JSON array** `[{...}]` for both success and error.
---
## Step 1: `QRCodeUtility/fetchQRCodeById` — resolve merchant
### Endpoint
```
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/QRCodeUtility/fetchQRCodeById
```
### Request
**Content-Type:** `application/x-www-form-urlencoded`
| Field | Value |
|---|---|
| `role` | **`R01`** (the other two endpoints use `RETAIL_SUBSCRIBER` — this one does not) |
| `channel` | `C03` |
| `loginExchangeKey` | From login |
| `rndValue` / `csValue` | [Standard anti-replay](01-encryption.md#anti-replay-envelope-rndvalue--csvalue) |
| `formData` | JSON below ([html-safe `=` escaping](01-encryption.md#html-safe-gson--escape)) |
```json
{
"qrCodeId": "<numeric id from QR>",
"tenantCode": "ooredoo"
}
```
### Response — happy path
```json
[
{
"success": true,
"message": "QRCode fetched Successfully.",
"response": [
{
"mobileNumber": "9609569506", /* merchant's '960' + msisdn */
"customerId": "72518", /* used as merchantId in step 2 */
"commercialName": "Family Room", /* merchant display name */
"qrCodeId": "1594103440350",
"qrImageString": "<base64 PNG>", /* unused */
"accountNumber": null,
"txnAmount": null, /* static QR; dynamic QRs put a number here */
"currencyCode": "MVR",
"status": "Active",
"role": "AGENT",
"tenantCode": "ooredoo",
"...": "..."
}
]
}
]
```
`accountNumber` / `txnAmount` are JSON `null` for **static QRs** (the user chooses the amount). For **dynamic QRs** the server returns the fixed amount in `txnAmount` and the client should lock the amount field.
The `mobileNumber` field already includes the `960` country prefix.
### Response — QR not found / inactive
```json
[{ "success": false, "message": "QRCode not found." }]
```
The client also rejects entries with `status != "Active"`.
---
## Step 2: `initiateNewBuy` — initiate purchase (no OTP triggered)
### Endpoint
```
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/initiateNewBuy
```
### Request
**Content-Type:** `application/x-www-form-urlencoded`
| Field | Value |
|---|---|
| `role` | `RETAIL_SUBSCRIBER` |
| `channel` | `C03` (top-level differs from inner `formData.channel`, which is `SubscriberApp`) |
| `loginExchangeKey` | From login |
| `rndValue` / `csValue` | Standard anti-replay (derived from the `formData` below) |
| `formData` | JSON below |
```json
{
"channel": "SubscriberApp",
"commodityType": "WALLET",
"description": "<remarks>", /* free text, may be empty */
"merchantId": "<customerId from step 1>",
"mobileNumber": "<merchant '960' msisdn from step 1>",
"sourceDetails": {
"MDNId": "960<myMsisdn>", /* PLAINTEXT '960' + my phone */
"actorRoleType": "RETAIL_SUBSCRIBER",
"pocketId": "<my source pocket id>" /* EMONEY pocket from login */
},
"transactionAmount": "<amount>", /* string, e.g. "7.56" */
"transactionCurrency": "MVR",
"transactionType": "PURCHASE"
}
```
Unlike the transfer flow's `initiateFTRequest`, this endpoint does **not** take an `identifier` header field or `tPin`.
### Response — happy path
```json
[
{
"2FARequired": "NONE", /* the key difference */
"authenticationType": "NONE",
"success": true,
"message": "Purchase Initiated Successfully",
"response": [
{
"requestObject": { "...": "..." },
"responseObject": {
"referenceId": "685011023630",
"transactionAmount": { "amount": 7.56, "currencyCode": "MVR" },
"netAmount": { "amount": 7.56, "currencyCode": "MVR" },
"chargeDetailsDTO": { "totalFeesInTenantCurrency": { "amount": 0.0, "...": "..." }, "...": "..." },
"isCompleted": false,
"authenticationType":"NONE",
"...": "..."
}
}
]
}
]
```
Cache `referenceId` for step 3. No SMS is sent — proceed straight to confirm.
> **Defensive check:** the Thijooree client throws if it ever sees `2FARequired != "NONE"` on this endpoint so a no-op confirm can't silently complete. If Ooredoo ever turns 2FA on for QR pay, you'll see a clear error instead of a partial transaction.
---
## Step 3: `confirmNewBuy` — settle purchase
### Endpoint
```
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/confirmNewBuy
```
### Request
**Content-Type:** `application/x-www-form-urlencoded`
| Field | Value |
|---|---|
| `role` | `RETAIL_SUBSCRIBER` |
| `channel` | `C03` |
| `loginExchangeKey` | From login |
| `rndValue` / `csValue` | Anti-replay derived from `formData` below |
| `formData` | `{"referenceId": "<from step 2>"}` |
| `transactionAuthDetails` | **literal string `"null"`** (not a JSON null, not an empty string — the captured request sends the four-character string `null`) |
### Response — happy path
```json
[
{
"success": true,
"message": "Payment Completed Successfully",
"response": [
{
"responseObject": {
"isCompleted": true,
"balanceInquiryDTO": {
"currencyCode": "MVR",
"pocketAmount": 92.85,
"pocketId": "<source pocket id>",
"pocketBalanceMap": { "...": "..." }
},
"status": { "replyCode": 0.0 },
"...": "..."
}
}
]
}
]
```
### Session expiry
Same envelope as elsewhere — `attributeValue: "SESSION_EXPIRED"` with HTTP 200; the client throws `MfaisaSessionExpiredException`. See [03-history.md → Session expiry](03-history.md#session-expiry).
---
## Optional: `save/smart-pay-recipient` — bookkeeping
After a successful confirm the official Ooredoo app saves the merchant to a server-side "recent recipients" list:
```
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/save/smart-pay-recipient
```
This is **not** required for the payment itself — the transfer is final once `confirmNewBuy` succeeds. Thijooree skips it; the merchant is kept in the local picker `RecentsCache` under an `mfaisaqr:<qrCodeId>` synthetic accountNumber (mirroring the BML QR `bmlqr:` scheme).
---
&nbsp;
---
> **← Back to** [Transfer Money](04-transfer.md) | [README](README.md)
+1
View File
@@ -88,6 +88,7 @@ Client Server
| 2 | [Login](02-login.md) | Subscriber lookup + mPIN login |
| 3 | [Transaction History](03-history.md) | Paginated history per session |
| 4 | [Transfer Money](04-transfer.md) | Three-step wallet-to-wallet send: recipient lookup → initiate (server SMSes OTP) → confirm |
| 5 | [QR Merchant Payment](05-qr-pay.md) | Three-step "smart pay" scan-to-merchant: QR lookup → initiate → confirm. **No OTP** (`2FARequired=NONE`) |
---