Monaf Horany
All work

003

2024 — 2025

Netlastik

A B2B tyre commerce app for dealers, shipped to both the App Store and Google Play.

shipped on
iOS + Android
app stores, both live
2
feature modules
17

Architecture

Netlastik — dealer app and payment flowA dealer opens the Flutter app and chooses which account they are buying for; that choice scopes the catalogue, the pricing and the ledger for the whole session. The app talks to one HTTP API through a shared dio client, which serves both the tyre catalogue and the dealer’s current account. Payment leaves the app entirely: card entry happens in the bank’s own webview, and the order is settled only when the result comes back.1 · account scope2 · pay3 · settledFlutter appiOS · AndroidNetlastik APIHTTP · dioCataloguetyres · pricingCari hesapledger · receiptsBank webviewcard entry
A dealer opens the Flutter app and chooses which account they are buying for; that choice scopes the catalogue, the pricing and the ledger for the whole session. The app talks to one HTTP API through a shared dio client, which serves both the tyre catalogue and the dealer’s current account. Payment leaves the app entirely: card entry happens in the bank’s own webview, and the order is settled only when the result comes back.
  • Client
  • Service
  • Data store
  • External

The problem

Netlastik is a tyre retailer selling to dealers rather than to drivers. The app is published under the client’s own developer accounts on both stores, and was built as the dealer-facing surface for their existing catalogue and accounting.

Dealers do not shop the way consumers do. One person often buys for several accounts, prices depend on which account they are acting for, and the thing they check most is not an order history but a running balance — what they owe, what they have paid, and the receipt proving it. A consumer storefront pattern does not survive contact with any of that.

The approach

A Flutter client built as seventeen feature modules over one shared core: Bloc for state, get_it for wiring, go_router for navigation, and a single dio client every module goes through. The account a dealer selects is session state that scopes everything downstream, so no screen has to remember to filter by it. Payment is handed to the bank’s own webview rather than reimplemented, so card details never reach the app.

Decisions that mattered

  1. 01

    One architecture, repeated seventeen times

    One architecture, repeated. Every feature module has the same shape — Bloc for state, get_it for construction, go_router for entry, dio for transport. Seventeen modules is enough that "how do I build a screen" has to have exactly one answer, or the codebase becomes seventeen dialects.

  2. 02

    Errors are values, not exceptions

    Errors are values, not exceptions. Requests return an Either from dartz, so a failure is something the caller has to destructure before it can reach the UI. A forgotten try/catch fails silently; an unhandled Either does not compile.

  3. 03

    The selected account is session state

    The selected account is session state, not a parameter. A dealer acting for several accounts picks one, and that choice scopes catalogue, pricing and ledger for the whole session. Threading an account id through every call is the version of this that eventually ships a screen showing the wrong balance.

  4. 04

    The current account is a first-class surface

    The current account is a first-class surface. Balance movements, receipts and successful and failed payments each get their own destination rather than being buried in an order history — because for a dealer the ledger, not the catalogue, is the screen they open daily.

  5. 05

    Card entry never happens in the app

    The 3-D Secure flow runs in the bank’s own webview rather than in a card form of mine. Card details never enter the app’s process, so the client never stores, transmits or processes them — the part of a payment integration carrying the most compliance weight is simply absent, and the flow that would have been hardest to get right is the one I did not write. The cost is that the result arrives asynchronously, which is what turns the three return paths below into a real problem rather than a hypothetical one.

Hard problems

  1. 01

    A price is a function of who is asking

    B2B pricing is per-account, so nothing can be cached as "the" price of a product. Every price is a function of who is asking, which rules out most of the obvious catalogue caching and makes the account switch a cache boundary.

  2. 02

    A payment can come back three ways

    A payment that leaves the app can come back three ways: settled, refused, or not at all when the dealer closes the webview mid-flow. Success and failure are separate destinations with their own state, and an abandoned payment resolves to neither rather than silently reading as success.

  3. 03

    Two stores, one branch

    Two stores, two review processes, one codebase. iOS and Android diverge on permissions, webview behaviour and release cadence, and both had to stay shippable from the same branch.

My role

Sole engineer on the client. Architecture, state management, navigation, all seventeen feature modules, the payment hand-off, and both store releases.

Status

Shipped and live on Google Play and the App Store. Built and released between 2024 and 2025; the app has since been handed over and is maintained by others.

Stack

Mobile
FlutterDartBloc / Cubitgo_routerget_itdio
Data
dartzequatableshared_preferences
Frontend
flutter_screenutilflutter_svgcached_network_imageinfinite_scroll_paginationpinputwebview_flutter