Skip to content

Compose Frontend

mero-compose is an optional Jetpack Compose layer that ships alongside the core SDK — a MeroClient view-model exposing a StateFlow<MeroAuthState>, a MeroProvider/useMero pair for passing it down the tree, and ready-made LoginSheet / ConnectButton composables. It’s the analog of mero-react’s MeroProvider/useMero + LoginModal in its Cloud mode: sign-in is “Continue with Calimero” and nothing else — no node URL, username or password. See Authentication for what happens underneath.

Add the optional dependency (see install):

implementation("com.calimero.mero:mero-compose:0.1.0")

Build a MeroClient, provide it with MeroProvider, then read it anywhere below with useMero():

import com.calimero.mero.compose.*
@Composable
fun App(context: Context) {
// Production factory: device keys + session in EncryptedSharedPreferences
val client = remember { MeroClient.create(context) }
LaunchedEffect(Unit) { client.restore() } // reconnect a saved session
MeroProvider(client) {
val c = useMero() // the client, anywhere in the tree
val state by c.state.collectAsStateWithLifecycle()
if (state.isAuthenticated) HomeScreen() else LoginSheet(callbackUrl = "myapp://calimero-enrol")
}
}
// The Activity that owns the callback intent filter forwards the wallet's redirect:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.toString()?.let { lifecycleScope.launch { client.handleEnrolmentCallback(it) } }
}
MeroClientStateFlow<MeroAuthState> · isAuthenticatedMeroProvider → useMero()collectAsStateWithLifecycle()CompositionLocalfalseLoginSheet()Continue with Calimero→ client.signInWithCloud(…)trueyour Home screenclient.relay · client.meroConnectButton → signOut()

A plain (non-composable) class wrapping a CloudAccount, exposing one observable state flow. MeroAuthState carries isAuthenticated, isLoading, account, device, relayUrl, sessionReady (a Bearer session on the relay exists), relayNote, cancelled and error, plus signedInWithoutRelay.

class MeroClient(
val account: CloudAccount,
tokenStore: TokenStore = MemoryTokenStore(),
launchWallet: (Context, String) -> Unit = WalletLauncher::launch, // a Custom Tab
) {
val state: StateFlow<MeroAuthState>
val relay: RelayClient? // warranted writes, reads by query
val mero: Mero? // admin / events on the relay (Bearer)
fun signInWithCloud(context: Context, callbackUrl: String)
suspend fun handleEnrolmentCallback(url: String): Boolean
suspend fun restore()
suspend fun joinWithInvitation(namespaceId: String, invitation: SignedGroupOpenInvitation): JoinResult
suspend fun signOut(forgetDevice: Boolean = false)
companion object {
fun create(context: Context, cloudBaseUrl: String = …, walletUrl: String = …): MeroClient
}
}

handleEnrolmentCallback verifies the certificate against this device’s own keys and the state it sent before storing anything, so a crafted deep link cannot sign the app in to someone else’s account. A declined approval sets cancelled, not error.

Both default their client to useMero():

Composable Purpose
MeroProvider(client) { … } provides the MeroClient via a CompositionLocal
useMero() reads the provided client (throws if no provider above)
LoginSheet(callbackUrl, onAuthenticated, colors) “Continue with Calimero” → client.signInWithCloud(...)
ConnectButton(callbackUrl, onConnect) “Continue with Calimero” when signed out, “Sign out” when signed in
LoginSheet(
callbackUrl = "myapp://calimero-enrol",
onAuthenticated = { /* navigate home */ },
)
ConnectButton(callbackUrl = "myapp://calimero-enrol")

LoginSheet uses the light Calimero tokens by default; pass LoginSheetColors(...) to restyle it.