# Integration Guide This README explains how to wire the two new crates (`robius-ussd`, `robius-fingerprinting`) and the Pay feature patch (`nigig-mpesa-patches`) into the existing `nigig-mpesa` Makepad app. --- ## 1. Workspace layout After integration, the workspace looks like: ``` your-workspace/ ├── robius-sms/ ← existing (template) ├── robius-ussd/ ← NEW (copy from download/) ├── robius-fingerprinting/ ← NEW (copy from download/) ├── makepad/ ├── robius/ │ ├── crates/ │ │ ├── location/ │ │ ├── directories/ │ │ └── open/ ├── nigig-core/ ├── nigig-uikit/ └── nigig-mpesa/ └── src/ └── pages/ └── pay/ ← NEW (copy from download/nigig-mpesa-patches/) ``` --- ## 2. Copy the two new crates ```bash cp -r /home/z/my-project/download/robius-ussd / cp -r /home/z/my-project/download/robius-fingerprinting / ``` ## 3. Copy the Pay feature into nigig-mpesa ```bash cp -r /home/z/my-project/download/nigig-mpesa-patches/src/pages/pay \ /nigig-mpesa/src/pages/ ``` ## 4. Wire `nigig-mpesa/Cargo.toml` Add to `[dependencies]`: ```toml robius-ussd = { path = "../../robius-ussd", features = ["serde"] } robius-fingerprinting = { path = "../../robius-fingerprinting" } ``` **Important**: the `serde` feature on `robius-ussd` is REQUIRED — it enables `Serialize`/`Deserialize` on `TransactionKind`, which `PendingTransaction` needs for its JSON persistence. Without it you'll get "trait bound `TransactionKind: Serialize` not satisfied" errors. The existing deps `robius-sms`, `nigig-core`, `nigig-uikit`, `makepad-widgets`, `chrono`, `serde`, `serde_json` are already declared. ## 5. Wire `nigig-mpesa/src/lib.rs` In the existing `lib.rs`, register the pay module's `script_mod`: ```rust // existing line: pub mod pages; // existing line: pages::script_mod(vm); // Add inside the existing script_mod function: pages::pay::script_mod(vm); ``` And add a module declaration in `src/pages/mod.rs`: ```rust pub mod pay; // inside pages::script_mod(vm): pay::script_mod(vm); ``` ## 6. Wire the FAB + PaySheet into `MpesaTrackerScreen` The existing `MpesaTrackerScreen` already has a `flow: Overlay` root with `main_content`, `transaction_detail_modal`, `modal_overlay`, and `camera_widget` children. Add two more children at the end: ```makepad // In src/pages/transactions/mod.rs inside the script_mod! block: mod.widgets.MpesaTrackerScreen = #(MpesaTrackerScreen::register_widget(vm)) { ..mod.widgets.SolidView width: Fill, height: Fill flow: Overlay show_bg: true draw_bg.color: #xF8FAFC main_content := SolidView { /* …existing… */ } transaction_detail_modal := SolidView { /* …existing… */ } modal_overlay := SolidView { /* …existing… */ } camera_widget := mod.widgets.CameraWidget {} // ── NEW: pay feature ── pay_fab := mod.widgets.PayFab { margin: Inset{right: 16, bottom: 16} align: Align{x: 1.0, y: 1.0} // bottom-right anchor } pay_sheet := mod.widgets.PaySheet { visible: false } } ``` The `mod.widgets.PayFab` and `mod.widgets.PaySheet` templates are defined in `src/pages/pay/pay_sheet.live` — paste that file's contents into your `script_mod!` block (Makepad 2.0 currently cannot load `.live` files across crate boundaries, so the DSL must live inline). ## 7. Wire the event handler in `MpesaTrackerScreen::handle_event` The `PaySheet` widget self-manages most of its event flow. The parent screen only needs to react to FAB taps and final outcome actions. **Add this import at the top of `src/pages/transactions/mod.rs`:** ```rust use crate::pages::pay::{PaySheet, PaySheetAction, PaySheetTab}; use nigig_uikit::pull_up_sheet::PullUpSheetWidgetExt; // required for .pull_up_sheet() ``` Then in `MpesaTrackerScreen::handle_event`, inside the `if let Event::Actions(actions)` arm: if self.view.button(cx, ids!(pay_fab)).clicked(&actions) { self.view.set_visible(cx, true); self.view.pull_up_sheet(cx, ids!(pay_sheet.sheet)).expand_full(cx); } // Listen for PaySheet outcome actions. // NOTE: `cast::()` returns `T` directly (uses Default for failed casts), // NOT `Option`. So use `match` not `if let Some(...)`. for action in &actions { let a = action.as_widget_action().cast::(); match &a { PaySheetAction::Closed => { self.view.set_visible(cx, false); // hide pay_sheet overlay self.scan_sms(cx); // refresh transaction list } PaySheetAction::Verified(_, _) => { self.scan_sms(cx); // pull in the new verified transaction } PaySheetAction::Failed(_, _) => { self.scan_sms(cx); // refresh in case a partial record exists } _ => {} } } ``` The full `PaySheetAction` enum is in `src/pages/pay/pay_sheet.rs`. ## 8. AndroidManifest additions Merge these into the host app's `AndroidManifest.xml`: ```xml ``` And create `app/src/main/res/xml/ussd_accessibility_config.xml`: ```xml ``` ## 9. Initialisation In the Makepad app's `AppMain::handle_event`, on `Event::Construct`, call: ```rust fn handle_event(&mut self, cx: &mut Cx, event: &Event) { match event { Event::Construct => { let _ = robius_ussd::init(); let _ = robius_fingerprinting::init(); } _ => {} } // …existing event dispatch… } ``` Both `init()` calls are idempotent and safe to call from any thread (they internally use `robius_android_env::with_activity`). ## 10. End-to-end runtime flow When the user taps the green "Pay" FAB: 1. `PaySheet::open(cx)` runs — `PullUpSheet::expand_full(cx)` slides the bottom sheet up. 2. User picks the **Payment** tab, chooses a kind (Send Money / Pochi / Till / Paybill / Withdraw), enters amount + recipient + PIN. 3. As they type the amount, `refresh_band_preview` calls `robius_ussd::mpesa_band_for_kind_amount(kind, amount)` and updates the `band_preview_label` and `fee_value_label`. 4. User taps **"Pay via M-Pesa"** (green CTA). 5. `PaySheet::start_payment_flow`: - Validates the request. - **Writes a `PendingTransaction` to `pending_transactions.json`** (offline-first). - Calls `robius_fingerprinting::authenticate` with `AuthPrompt::for_transaction`. 6. The Android system shows the BiometricPrompt. 7. On `AuthResult::Succeeded`: - `PaySheet::on_biometric_succeeded` calls `robius_ussd::begin_transaction`. - The Java `UssdAccessibilityService` is invoked when the `*334#` dialog appears. - It drives the M-Pesa menu state machine (1→1, 1→3, 6→2, 6→1, 2→1) and types the user's inputs. 8. On `UssdSessionEvent::ResultText(text)`: - The text is parsed by the existing `mpesa_parser::parse`. - `TransactionFlow::on_sms_received` marks the pending record `Verified` and upserts into `MpesaTransactionStore`. - The transaction appears in the Transactions tab on the next SMS scan. 9. On `UssdSessionEvent::SessionEnded{Failed}` or verification-window expiry: - The pending record is marked `Failed`. - The failure reason is shown in the `status_label`. ## 11. M-Pesa fee bands (the user's specific ask) The Safaricom tariff table lives in `robius-ussd/src/mpesa_bands.rs`. It is the **Jan 2024 revision**. There are six tables: | Constant | Used for | |-----------------------------------|-------------------------| | `MPESA_SEND_REGISTERED` | Send Money → registered | | `MPESA_SEND_UNREGISTERED` | Send Money → unregistered | | `MPESA_POCHI` | Pochi la Biashara | | `MPESA_TILL` | Lipa na M-Pesa (Till) | | `MPESA_PAYBILL` | Pay Bill | | `MPESA_WITHDRAW_AGENT` | Withdraw at Agent | **Update this file when Safaricom revises the tariff.** The `mpesa_band_for_kind_amount` function is the public API consumed by the Pay sheet's `band_preview_label`. ## 12. Transaction cost as Expense The existing `MpesaTransaction` struct already has a `cost: Option` field parsed from M-Pesa SMS ("Transaction Cost, KSh X.XX"). This patch surfaces it via: - The Pay sheet's `band_preview_label` shows the *estimated* cost BEFORE the transaction (from the band table). - After verification, the *actual* cost from the SMS is stored on the `MpesaTransaction.cost` field. - The pending-store tracks `estimated_fee` vs `actual_fee` for discrepancy detection. Future work (out of scope for this patch): add a "Fees paid" summary card to the Expenses tab that aggregates `tx.cost` across all verified transactions for a date range. The `MpesaTransactionStore::totals` function already iterates all transactions; a new `total_fees()` method is a 5-line addition. ## 13. File summary | Path | Purpose | |---------------------------------------------------|-----------------------------------------------| | `robius-ussd/Cargo.toml` | Crate manifest | | `robius-ussd/README.md` | Crate docs | | `robius-ussd/src/lib.rs` | Public API (TransactionKind, UssdTransactionRequest, etc.) | | `robius-ussd/src/error.rs` | Error enum | | `robius-ussd/src/mpesa_bands.rs` | Safaricom fee band table | | `robius-ussd/src/build.rs` | javac + d8 → classes.dex pipeline | | `robius-ussd/src/sys/mod.rs` | cfg_if platform dispatch | | `robius-ussd/src/sys/android/mod.rs` | Re-exports + From | | `robius-ussd/src/sys/android/permissions.rs` | CALL_PHONE / POST_NOTIFICATIONS runtime perms | | `robius-ussd/src/sys/android/dialer.rs` | tel:*334# via ACTION_CALL | | `robius-ussd/src/sys/android/session.rs` | SharedPreferences-backed state machine | | `robius-ussd/src/sys/android/accessibility.rs` | In-memory DEX + register_native_methods | | `robius-ussd/src/sys/android/events.rs` | Java→Rust event channel | | `robius-ussd/src/sys/android/UssdAccessibilityService.java` | Java AccessibilityService | | `robius-ussd/src/examples/simple_ussd.rs` | Usage example | | `robius-fingerprinting/Cargo.toml` | Crate manifest | | `robius-fingerprinting/README.md` | Crate docs | | `robius-fingerprinting/src/lib.rs` | Public API (AuthPrompt, AuthResult, etc.) | | `robius-fingerprinting/src/error.rs` | Error enum | | `robius-fingerprinting/src/build.rs` | javac + d8 → classes.dex pipeline | | `robius-fingerprinting/src/sys/mod.rs` | cfg_if platform dispatch | | `robius-fingerprinting/src/sys/android/mod.rs` | Re-exports + From | | `robius-fingerprinting/src/sys/android/permissions.rs` | USE_BIOMETRIC / USE_FINGERPRINT perms | | `robius-fingerprinting/src/sys/android/prompt.rs` | BiometricPrompt presentation + JNI bridge | | `robius-fingerprinting/src/sys/android/events.rs` | Java→Rust event channel | | `robius-fingerprinting/src/sys/android/BiometricCallback.java` | Java BiometricPrompt callback | | `robius-fingerprinting/src/examples/simple_fingerprint.rs` | Usage example | | `nigig-mpesa-patches/src/pages/pay/mod.rs` | Module registry + script_mod | | `nigig-mpesa-patches/src/pages/pay/mpesa_bands_bridge.rs` | Re-exports + format_band_preview | | `nigig-mpesa-patches/src/pages/pay/pay_sheet.rs` | PaySheet widget (FAB + bottom sheet + tabs) | | `nigig-mpesa-patches/src/pages/pay/pay_sheet.live`| Makepad live DSL templates | | `nigig-mpesa-patches/src/pages/pay/pending_store.rs` | Offline-first PendingTransactionStore | | `nigig-mpesa-patches/src/pages/pay/transaction_flow.rs` | Offline-first state machine | | `nigig-mpesa-patches/src/pages/pay/fingerprint_gate.rs` | Makepad action wrapper | ## 14. Testing The `robius-ussd` crate has unit tests for the band table: ```bash cd robius-ussd cargo test ``` Expected output: - `send_money_low_band_is_free` ✓ - `send_money_mid_band` ✓ - `send_money_high_band_caps_at_105` ✓ - `till_is_always_free_for_customer` ✓ - `withdraw_agent_minimum_is_50` ✓ - `band_lookup_returns_band` ✓ The `robius-fingerprinting` crate has no unit tests (it's a JNI bridge — the only meaningful test is on a real Android device). The `dummy_success` / `dummy_failure` helpers let the UI be developed on desktop. The `pending_store` has a lifecycle test that runs without an Android device. ## 15. Known limitations / future work 1. **AccessibilityService enablement** must be done manually by the user — Android does not allow programmatic enablement. The crate detects this via `is_accessibility_enabled()` and the UI should prompt the user to open Settings. 2. **M-Pesa PIN entry**: in production, replace `pin_input` with a `SecureTextInput` and never log the PIN. The PIN is currently held in memory only for the duration of the USSD session and written to `SharedPreferences` by the Java side — it should be moved to `EncryptedSharedPreferences` (matching the PesaMirror `SecurePrefs` pattern). 3. **Multi-session**: only one transaction can be in flight at a time (the `current` field on `TransactionFlow`). This is intentional — M-Pesa itself does not support concurrent USSD sessions. 4. **Tariff updates**: when Safaricom revises the tariff, update `robius-ussd/src/mpesa_bands.rs` only — the consuming app picks up the change automatically. 5. **Biometric crypto object**: the current implementation initialises the CryptoObject with a fixed `"robius-ussd-txn"` plaintext. For real transaction signing, derive a per-transaction cipher from the `PendingTransaction.id` so a stolen prompt screenshot cannot be replayed.