| Bisq Easy Node (Android) | Bisq Connect (Android) | Bisq Connect (iOS) |
|---|---|---|
|
|
|
TestFlight |
All releases & changelogs on GitHub
Learn how to use Bisq Connect
-
App Architecture Design Choice
- Dumb Views
- UI independently built
- Encourage Rich Domain well-test models
- Presenters guide the orchestra
- Use Cases encapsulate complex workflows
- Repositories key for reactive UI
- Services allow us to have different networking sources
- What about Lifecycle and main view components
- When it’s acceptable to reuse a presenter for my view
- Presenter Lifecycle
This project aims to make Bisq Network accesible in Mobile Platforms following the philosofy of Bisq2 - to make it easier for both, experienced and newcomers, to trade Bitcoin in a decentralized way as well as defending Bisq motto: exchange, decentralized, private & secure.
To achieve this goal, we are building a total of 3 mobile apps that can be divided in 2 categories:
- Bisq Easy Node for Android (Gradle module
:apps:nodeApp), an Android app that runs Bisq2 core and aims to bring a fully featured trading version ofBisq2(also referred to by its main protocol -Bisq Easy) to mobile for full privacy & security.
- Bisq Connect (Gradle module
:apps:clientAppfor Android and theiosClientXcode project for iOS), a thin Bisq client app that can be configured to connect to a trusted Bisq2 node (over Tor or clearnet) to cater for people willing to try Bisq from somebody they really trust (popularily described as "Uncle Jim") who is willing to share their Bisq node with them.
We follow Bisq standard guidelines for contributions, fork + PR, etc. Please refer to Contributor Checklist
We track work via GitHub issues at https://github.com/bisq-network/bisq-mobile/issues. Pick something that interests you or open a new issue for discussion.
For Jetpack Compose best practices in this project, see the Compose guidelines.
This project uses ktlint with Compose Rules to maintain consistent code style across the codebase.
# Check code style
./gradlew ktlintCheck
# Auto-fix style violations
./gradlew ktlintFormatGit hooks are automatically installed when you sync the project. They will:
- Pre-commit: Check ktlint on staged files only (with auto-fix prompt)
- Pre-push: Run full ktlint check + unit tests
To bypass hooks temporarily (not recommended):
git commit --no-verify
git push --no-verifyAll pull requests automatically run ktlint checks in CI. Make sure your code passes locally before pushing:
./gradlew ktlintCheck test.editorconfig: Main ktlint configuration with Compose-specific rulesbuild.gradle.kts: ktlint plugin setup (version 1.7.1)- Compose Rules: Enabled for Compose best practices enforcement
For now follow along to learn how to run this project. If you are a mobile enthusiast and feel driven by Bisq goals, please reach out!
- Java: 21.0.6.fx-zulu JDK (sdkman env file is avail in project root)
- Ruby: v3+ (for iOS Cocoapods 1.15+)
- IDE: We recommend using Android Studio with the Kotlin Multiplatform Mobile (KMP) plugin. For iOS testing you will need XCode installed and updated.
- Get sdkman installed since the project uses JDK
- Open Android Studio with the Kotlin Multiplatform Mobile plugin installed and open the project root folder.
- Wait for the Gradle sync to complete and download the dependencies. This will let you know what's missing in your machine to run the project.
- If you are on a MacOS computer building the iOS app you can go ahead and run
setup_ios.shscript and build the project and run it in your device or emulator. - For Android it can run on any machine, just run the preconfigured run configurations
clientAppand/ornodeAppin Android Studio
- If you are on a MacOS computer building the iOS app you can go ahead and run
Alternatively, you could run ./gradlew clean build first from terminal and then open with Android Studio.
For the androidNode module to build, you need the Bisq2 dependencies. There are two ways to get them:
- Download Bisq2 if you don't have it already
- Bisq Android Node uses Bisq2 core code by design, this dependency will always be against a bisq2 branch OFF A STABLE RELEASE + commits of current bisq-mobile development.
Check the current codebase bisq-core dependency version in the toml file, at the top of the file
bisq-coreproperty will have the version (e.g. "2.1.7"). Now go ahead and checkout the bisq2 dev branch for bisq-mobile which follows the patternfor-mobile-based-on-[VERSION](E.g. if the bisq-core-version="2.1.7" then checkout for-mobile-based-on-2.1.7 -git checkout for-mobile-based-on-2.1.7). You can double check if that branch is from the right release line comparing the initial commit of the branch with the tommlbisq-core-commitvalue :) - Follow Bisq2 root
README.mdsteps to build the project. - Run
./gradlew publishAll// this will install all the jars you need in your local m2 repo
NOTE #1 For bisq-mobile release the bisq-core-commit should point to the exact commit the apps were design to work with
NOTE #2 if you have troubles publishing the jars try ./gradlew cleanAll buildAll publishAll publishAll -- info it's known to always update properly
The CI environment automatically uses our remote Maven repository to get the Bisq2 dependencies. No additional setup is required.
Done! Alternatively if you are interested only in contributing for the xClients you can just build them individually instead of building the whole project.
Node
You just need to run a local bisq seed node from the bisq2 project. By default port 8000 is used
Clients
You need to run the seed node as explained above + the http-api module with the following VM parameters
-Dapplication.appName=bisq2_restApi_clear
-Dapplication.network.supportedTransportTypes.2=CLEAR
-Dapplication.devMode=true
-Dapplication.devModeReputationScore=50000
Default networking setup for the WebSocket (WS) connection can be found in gradle.properties file. You can change there for locally building pointing at the ip you are interested in.
Designs
New feature designs are generated as working Compose @Preview composables placed in:
shared/presentation/src/commonMain/kotlin/.../presentation/design/<feature>/
These are created by an AI design agent that can generate designs from scratch or adapt them from the Bisq2 Desktop codebase. The composables are fully previewable in Android Studio and serve as the reference for implementation. When picking up a GitHub issue that requires UI work, check if designs have been uploaded (look for the designs-uploaded label). If not, request them before starting implementation.
Developers should move design composables into the appropriate production package during implementation. Unused designs are easy to locate and clean up since they all live under the design/ package.
The original Figma designs (legacy reference): https://www.figma.com/design/IPnuicxGKIZXq28gybxOgp/Xchange?node-id=7-759&t=LV9Gx9XgJRvXu5YQ-1
Navigation Implementation
Please refer to this README
- Some Apple M chips have trouble with cocoapods, follow this guide to fix it
- On MacOS: non-homebrew versions of Ruby will cause problems
- On MacOS: If Gradle sync fails with "Gradle not found" error, you may need to install gradle with
homebrewand then rungradle wrapperon the root. Then reopen Android Studio and try syncing again.
Though this can evolve, this is the initial structure of this KMP project:
- shared:domain: Domain module has models (KOJOs) and components that provide them.
- shared:presentation: Contains UI shared code using Kotlin MultiPlatform Compose Implementation forr all the apps, its Presenter's behaviour contracts (interfaces) and default presenter implementations that connects UI with domain.
- iosClient: Xcode project that generates the thin iOS client from sharedUI
- androidClient: (now found in
apps:clientApp) Kotlin Compose Android thin app. This app as well should have most if not all of the code shared with the iosClient. - androidNode: (now found in
apps:nodeApp) Bisq2 Implementation in Android, will contain the dependencies to Java 17 Bisq2 core jars.
This project uses the MVP (Model-View-Presenter) Design Pattern with variations (introducing Use Cases, Repositories, and allowing reuse of presenters under specific conditions) in the following way:
- Dumb Views: Each View defines its desired presenter behaviour. For example, for the
AppViewit would define theAppPresenterinterface. This includes which data it's interested in observing and the commands it needs to trigger from user interactions. - UI independently built: The view reacts to changes in the presenter observed data, and calls the methods it needs to inform the presenter about user actions. In this way each view can be created independently without strictly needing anything else.
- Encourage Rich Domain well-tested models: Same goes for the Models — they can be built (and unit tested) without needing anything else, simple POKOs (Plain Old Kotlin Objects — meaning no external deps). Ideally business logic should go here and the result of executing a business model logic should be put back into the repository for all observers to know.
- Presenters guide the orchestra: When you want to bring interaction to life, create a presenter (or reuse one if the view is small enough) and implement the interface you defined when doing the view (
AppPresenterinterface for example). That presenter will generally modify/observe the models through a repository and/or a service. The most important thing is that mutable/immutable observability should happen here connecting those fields that the view needs with the real data as appropriate case-by-case. - Use Cases encapsulate complex workflows: (
NEW!) When a presenter needs to orchestrate a multi-step process involving several services and repositories, that logic is extracted into a Use Case class. Use cases own their ownStateFlow-based state, coordinate services and repositories in sequence, and expose a cleanexecute()entry point. Presenters observe the use case state and delegate complex operations to it. This keeps presenters lean (focused on UI state mapping) and makes the workflow logic independently testable and reusable across multiple presenters. SeeTrustedNodeSetupUseCasefor a reference implementation. - Repositories key for reactive UI: For the presenter (or use case) to connect to the domain models we use repositories which is basically a storage of data (that abstracts where that data is stored in). The repositories also expose the data in an observable way, so the presenter can satisfy the requested data from the view from the data of the domain model in the ways it see fit. Sometimes it would just be a passthrough. The repositories could also have caching strategy, and persistence. For most of the use cases so far we don't see a strong need for persistence in most of them (with the exception of settings-related repositories) — more on this soon.
- Services allow us to have different networking sources: We are developing 3 apps divided in 2 groups:
nodeandclient. Each group has a very distinct networking setup. We need each type of app build to have only the networking it needs. The proposed separation of concerns not only allows a clean architecture but also allows faster development focus on each complexity separately. We found that for theandroidNodeit makes sense to handle all the domain stuff directly using domain models in the services without connecting to a repository since the bisq-core jars manage all the persistence. You have the option to decide how to connect this in your presenter.
As per original specs single-activity pattern (or single-viewcontroller in iOS) is sufficient for this project. Which means, unless we find a specific use case for it, we'll stick to a single Activity/ViewController for the whole lifecycle of the app.
The app's architecture BasePresenter allows a tree like behaviour where a presenter can be a root with dependent child presenters.
We leverage this by having:
- A
MainPresenterthat acts as root in each and all of the apps - The rest of the presenters require the main presenter as construction parameter to be notified about lifecycle events.
It's ok to reuse an existing presenter for your view if:
- Your view is a very small part of a bigger view that renders together (commonly called
Screen) and you can't foresee reusal for it - Your view is a very small part of a bigger view and even if its reused the presenter required implementation is minimal
To reuse an existing presenter you would have to make it extend your view defined presenter interface and do the right Koin bind on its Koin repository definition.
Then you can inject it in the @Composable function using koinInject().
Presenters are wired to Compose via lifecycle helpers. There are two lifecycle modes — choose based on whether the presenter should survive back-stack navigation.
Enter screen → onViewAttached() → coroutines start
Leave screen → onViewUnattaching() → scope disposed, coroutines cancelled
Re-enter → new presenter instance (factory) → onViewAttached() → fresh start
Use for: splash, onboarding, settings, dialog presenters, screens that should always start fresh.
Enter screen (first) → onViewAttached() → coroutines start
Leave screen (to back stack) → onViewHidden() → scope ALIVE, coroutines continue
Re-enter (from back stack) → onViewRevealed() → scope still alive, no re-subscription
Leave screen (popped) → onViewUnattaching() → scope disposed (via ViewModel.onCleared)
Use for: wizard steps (create/take offer), tab screens with expensive data loading, any screen where going back should preserve state, screens that should survive configuration changes (rotation, dark mode).
How it works: the presenter is stored inside a ViewModel scoped to the NavBackStackEntry. The ViewModel is an internal container — the presenter pattern, DI, and testing remain unchanged.
Bonus — Android config changes survival: because the presenter lives inside a ViewModel, it automatically survives Activity recreation triggered by configuration changes (rotation, dark mode toggle, language switch). During a config change the lifecycle is onViewHidden() → Activity recreated → onViewRevealed() — onViewUnattaching() is NOT called, so the scope and all in-flight coroutines persist. Screens using RememberPresenterLifecycle do NOT get this benefit — they restart from scratch on config changes.
// Default: scope disposed on navigation
@Composable
fun SettingsScreen() {
val presenter: SettingsPresenter = koinInject()
RememberPresenterLifecycle(presenter)
}
// Back-stack aware (recommended for most cases): scope survives while on back stack
// Presenter is created once inside a ViewModel — no wasted instances on recomposition
@Composable
fun DashboardScreen() {
val presenter = RememberPresenterLifecycleBackStackAware<DashboardPresenter>()
}Offer flow step presenters (create offer, take offer) extend OfferFlowPresenter instead of BasePresenter directly. This provides navigateToOfferbookTab() for closing the wizard flow. The shared data coordinators (CreateOfferCoordinator, TakeOfferCoordinator) are not presenters — they are Koin singletons that hold mutable wizard state across steps.
The project implements two distinct push notification strategies, each with different trade-offs between decentralization and reliability.
Both Android apps (Bisq Easy Node and Bisq Connect) use a fully decentralized, peer-to-peer foreground service approach. The OpenTradesNotificationService runs as an Android foreground service, maintaining a persistent notification while monitoring trade state changes and new chat messages in real time over the existing WebSocket/P2P connection.
How it works:
- On app start, a foreground service is launched immediately (before heavy initialization) to satisfy Android's strict timing requirements
- The service observes trade flows and chat messages via
TradesServiceFacadeandOffersServiceFacade - When the app moves to the background, the foreground service keeps the connection alive and delivers local notifications for trade events (state changes, new messages, new trades taken)
- No external servers are involved — notifications are generated entirely on-device from live P2P data
Trade-off: This approach is fully private and decentralized — no third-party servers ever see your trade activity. However, if the app is fully killed (e.g. the user swipes it away, or the device restarts), the foreground service stops and no notifications will be delivered until the user opens the app again. Android's OS-level restrictions on background execution make this an inherent limitation of the decentralized approach.
iOS does not allow apps to maintain persistent background connections — the OS suspends apps within seconds of backgrounding and terminates WebSocket connections. A decentralized monitoring approach like Android's is not feasible on iOS.
To provide reliable push notifications on iOS, Bisq Connect uses Apple Push Notification service (APNs) with end-to-end encryption, following a model similar to Signal and ProtonMail. This was the most privacy-preserving solution that still delivers a top-class notification experience.
How it works:
- The iOS device generates a device-specific encryption key pair and stores the private key locally
- During registration, the device shares its public key and APNs device token with the trusted Bisq2 node
- When a trade event occurs, the Bisq2 node encrypts the notification payload using the device's public key
- The encrypted payload is forwarded to a bisq-relay server (bisq-relay project) which passes it to APNs
- APNs routes the notification to the iOS device
- The app decrypts the payload locally using its private key
What is protected:
- The notification content (trade details, messages) is end-to-end encrypted — neither the relay server nor Apple can read it
- Only the generic notification metadata and timing/frequency are visible to Apple (this is unavoidable with APNs)
Trade-off: This delivers reliable, always-on push notifications even when the app is fully killed. The cost is the introduction of centralized infrastructure: a 24/7 bisq-relay server and Apple's APNs servers sit in the notification path. While neither can see the notification content thanks to E2E encryption, they do participate in the delivery chain, which is a departure from Bisq's fully decentralized philosophy for this specific platform.
For more details on the iOS implementation design, see issue #895.
- Native Performance
- Allows us to focus on the "easiest" platform first for the Node (Because of Apple restrictions on Tor and networking in general). Althought unexpected, if situation changes in the future we could cater for an iOS Node.
- Flexibility without the security/privacy concerns of its competitors
- (Node)JVM language allows us to port much of the optimised Bisq code already existing in the Desktop apps
- Kotlin Compose UI allows us to share UI code easily across the 3 apps.
If you are interested in seeing the POCs related to the R&D before this project kicked-off please refer to this branch

