diff --git a/atala-prism-sdk/build.gradle.kts b/atala-prism-sdk/build.gradle.kts index bcba6169b..32f29b0d4 100644 --- a/atala-prism-sdk/build.gradle.kts +++ b/atala-prism-sdk/build.gradle.kts @@ -106,6 +106,7 @@ kotlin { implementation("io.ktor:ktor-client-content-negotiation:2.3.4") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.4") implementation("io.ktor:ktor-client-logging:2.3.4") + implementation("io.ktor:ktor-websockets:2.3.4") implementation("io.iohk.atala.prism.didcomm:didpeer:$didpeerVersion") @@ -135,12 +136,15 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") implementation("io.ktor:ktor-client-mock:2.3.4") implementation("junit:junit:4.13.2") + implementation("org.mockito:mockito-core:4.4.0") + implementation("org.mockito.kotlin:mockito-kotlin:4.0.0") } } val jvmMain by getting { dependencies { implementation("io.ktor:ktor-client-okhttp:2.3.4") implementation("app.cash.sqldelight:sqlite-driver:2.0.1") + implementation("io.ktor:ktor-client-java:2.3.4") } } val jvmTest by getting @@ -149,6 +153,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") implementation("io.ktor:ktor-client-okhttp:2.3.4") implementation("app.cash.sqldelight:android-driver:2.0.1") + implementation("io.ktor:ktor-client-android:2.3.4") } } val androidInstrumentedTest by getting { diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt index 496d75275..88d60befd 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManager.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:import-ordering") + package io.iohk.atala.prism.walletsdk.prismagent import io.iohk.atala.prism.walletsdk.domain.buildingblocks.Castor @@ -9,8 +11,14 @@ import io.iohk.atala.prism.walletsdk.domain.models.Message import io.iohk.atala.prism.walletsdk.prismagent.connectionsmanager.ConnectionsManager import io.iohk.atala.prism.walletsdk.prismagent.connectionsmanager.DIDCommConnection import io.iohk.atala.prism.walletsdk.prismagent.mediation.MediationHandler +import java.time.Duration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlin.jvm.Throws /** @@ -27,9 +35,99 @@ class ConnectionManager( private val castor: Castor, private val pluto: Pluto, internal val mediationHandler: MediationHandler, - private var pairings: MutableList + private var pairings: MutableList, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) ) : ConnectionsManager, DIDCommConnection { + var fetchingMessagesJob: Job? = null + + /** + * Starts the process of fetching messages at a regular interval. + * + * @param requestInterval The time interval (in seconds) between message fetch requests. + * Defaults to 5 seconds if not specified. + */ + @JvmOverloads + fun startFetchingMessages(requestInterval: Int = 5) { + // Check if the job for fetching messages is already running + if (fetchingMessagesJob == null) { + // Launch a coroutine in the provided scope + fetchingMessagesJob = scope.launch { + // Retrieve the current mediator DID + val currentMediatorDID = mediationHandler.mediatorDID + // Resolve the DID document for the mediator + val mediatorDidDoc = castor.resolveDID(currentMediatorDID.toString()) + var serviceEndpoint: String? = null + + // Loop through the services in the DID document to find a WebSocket endpoint + mediatorDidDoc.services.forEach { + if (it.serviceEndpoint.uri.contains("wss://") || it.serviceEndpoint.uri.contains("ws://")) { + serviceEndpoint = it.serviceEndpoint.uri + return@forEach // Exit loop once the WebSocket endpoint is found + } + } + + // If a WebSocket service endpoint is found + serviceEndpoint?.let { serviceEndpointUrl -> + // Listen for unread messages on the WebSocket endpoint + mediationHandler.listenUnreadMessages( + serviceEndpointUrl + ) { arrayMessages -> + // Process the received messages + val messagesIds = mutableListOf() + val messages = mutableListOf() + arrayMessages.map { pair -> + messagesIds.add(pair.first) + messages.add(pair.second) + } + // If there are any messages, mark them as read and store them + scope.launch { + if (messagesIds.isNotEmpty()) { + mediationHandler.registerMessagesAsRead( + messagesIds.toTypedArray() + ) + pluto.storeMessages(messages) + } + } + } + } + + // Fallback mechanism if no WebSocket service endpoint is available + if (serviceEndpoint == null) { + while (true) { + // Continuously await and process new messages + awaitMessages().collect { array -> + val messagesIds = mutableListOf() + val messages = mutableListOf() + array.map { pair -> + messagesIds.add(pair.first) + messages.add(pair.second) + } + if (messagesIds.isNotEmpty()) { + mediationHandler.registerMessagesAsRead( + messagesIds.toTypedArray() + ) + pluto.storeMessages(messages) + } + } + // Wait for the specified request interval before fetching new messages + delay(Duration.ofSeconds(requestInterval.toLong()).toMillis()) + } + } + } + + // Start the coroutine if it's not already active + fetchingMessagesJob?.let { + if (it.isActive) return + it.start() + } + } + } + + fun stopConnection() { + fetchingMessagesJob?.cancel() + } + /** * Suspends the current coroutine and boots the registered mediator associated with the mediator handler. * If no mediator is available, a [PrismAgentError.NoMediatorAvailableError] is thrown. diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt index d7e497cd6..bd3427f50 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/PrismAgent.kt @@ -67,11 +67,8 @@ import io.ktor.http.HttpMethod import io.ktor.http.Url import io.ktor.serialization.kotlinx.json.json import java.net.UnknownHostException -import java.time.Duration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first @@ -116,7 +113,6 @@ class PrismAgent { val pluto: Pluto val mercury: Mercury val pollux: Pollux - var fetchingMessagesJob: Job? = null val flowState = MutableSharedFlow() private val prismAgentScope: CoroutineScope = CoroutineScope(Dispatchers.Default) @@ -298,7 +294,6 @@ class PrismAgent { } logger.info(message = "Stoping agent") state = State.STOPPING - fetchingMessagesJob?.cancel() state = State.STOPPED logger.info(message = "Agent not running") } @@ -724,32 +719,7 @@ class PrismAgent { */ @JvmOverloads fun startFetchingMessages(requestInterval: Int = 5) { - if (fetchingMessagesJob == null) { - logger.info(message = "Start streaming new unread messages") - fetchingMessagesJob = prismAgentScope.launch { - while (true) { - connectionManager.awaitMessages().collect { array -> - val messagesIds = mutableListOf() - val messages = mutableListOf() - array.map { pair -> - messagesIds.add(pair.first) - messages.add(pair.second) - } - if (messagesIds.isNotEmpty()) { - connectionManager.mediationHandler.registerMessagesAsRead( - messagesIds.toTypedArray() - ) - pluto.storeMessages(messages) - } - } - delay(Duration.ofSeconds(requestInterval.toLong()).toMillis()) - } - } - } - fetchingMessagesJob?.let { - if (it.isActive) return - it.start() - } + connectionManager.startFetchingMessages(requestInterval) } /** @@ -757,7 +727,7 @@ class PrismAgent { */ fun stopFetchingMessages() { logger.info(message = "Stop streaming new unread messages") - fetchingMessagesJob?.cancel() + connectionManager.stopConnection() } /** diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/mediation/BasicMediatorHandler.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/mediation/BasicMediatorHandler.kt index 958f02543..7c54d2df6 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/mediation/BasicMediatorHandler.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/mediation/BasicMediatorHandler.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:import-ordering") + package io.iohk.atala.prism.walletsdk.prismagent.mediation import io.iohk.atala.prism.walletsdk.domain.buildingblocks.Mercury @@ -7,16 +9,24 @@ import io.iohk.atala.prism.walletsdk.domain.models.Mediator import io.iohk.atala.prism.walletsdk.domain.models.Message import io.iohk.atala.prism.walletsdk.domain.models.UnknownError import io.iohk.atala.prism.walletsdk.prismagent.PrismAgentError +import io.iohk.atala.prism.walletsdk.prismagent.protocols.ProtocolType import io.iohk.atala.prism.walletsdk.prismagent.protocols.mediation.MediationGrant import io.iohk.atala.prism.walletsdk.prismagent.protocols.mediation.MediationKeysUpdateList import io.iohk.atala.prism.walletsdk.prismagent.protocols.mediation.MediationRequest import io.iohk.atala.prism.walletsdk.prismagent.protocols.pickup.PickupReceived import io.iohk.atala.prism.walletsdk.prismagent.protocols.pickup.PickupRequest import io.iohk.atala.prism.walletsdk.prismagent.protocols.pickup.PickupRunner +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocket +import io.ktor.websocket.Frame +import io.ktor.websocket.readText import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import java.util.UUID +import kotlinx.coroutines.isActive /** * A class that provides an implementation of [MediationHandler] using a Pluto instance and a Mercury instance. It can @@ -84,7 +94,8 @@ class BasicMediatorHandler( val registeredMediator = bootRegisteredMediator() if (registeredMediator == null) { try { - val requestMessage = MediationRequest(from = host, to = mediatorDID).makeMessage() + val requestMessage = + MediationRequest(from = host, to = mediatorDID).makeMessage() val message = mercury.sendMessageParseResponse(message = requestMessage) ?: throw UnknownError.SomethingWentWrongError( message = "BasicMediatorHandler => mercury.sendMessageParseResponse returned null" @@ -167,4 +178,75 @@ class BasicMediatorHandler( } ?: throw PrismAgentError.NoMediatorAvailableError() mercury.sendMessage(requestMessage) } + + /** + * Listens for unread messages from a specified WebSocket service endpoint. + * + * This function creates a WebSocket connection to the provided service endpoint URI + * and listens for incoming messages. Upon receiving messages, it processes and + * dispatches them to the specified callback function. + * + * @param serviceEndpointUri The URI of the service endpoint. It should be a valid WebSocket URI. + * @param onMessageCallback A callback function that is invoked when a message is received. + * This function is responsible for handling the incoming message. + */ + override suspend fun listenUnreadMessages( + serviceEndpointUri: String, + onMessageCallback: OnMessageCallback + ) { + val client = HttpClient { + install(WebSockets) + install(HttpTimeout) { + requestTimeoutMillis = WEBSOCKET_TIMEOUT + connectTimeoutMillis = WEBSOCKET_TIMEOUT + socketTimeoutMillis = WEBSOCKET_TIMEOUT + } + } + if (serviceEndpointUri.contains("wss://") || serviceEndpointUri.contains("ws://")) { + client.webSocket(serviceEndpointUri) { + if (isActive) { + val liveDeliveryMessage = Message( + body = "{\"live_delivery\":true}", + piuri = ProtocolType.LiveDeliveryChange.value, + id = UUID.randomUUID().toString(), + from = mediator?.hostDID, + to = mediatorDID + ) + val packedMessage = mercury.packMessage(liveDeliveryMessage) + send(Frame.Text(packedMessage)) + } + while (isActive) { + try { + for (frame in incoming) { + if (frame is Frame.Text) { + val messages = + handleReceivedMessagesFromSockets(frame.readText()) + onMessageCallback.onMessage(messages) + } + } + } catch (e: Exception) { + e.printStackTrace() + continue + } + } + } + } + } + + private suspend fun handleReceivedMessagesFromSockets(text: String): Array> { + val decryptedMessage = mercury.unpackMessage(text) + if (decryptedMessage.piuri == ProtocolType.PickupStatus.value || + decryptedMessage.piuri == ProtocolType.PickupDelivery.value + ) { + return PickupRunner(decryptedMessage, mercury).run() + } else { + return emptyArray() + } + } } + +fun interface OnMessageCallback { + fun onMessage(messages: Array>) +} + +const val WEBSOCKET_TIMEOUT: Long = 15_000 diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/mediation/MediationHandler.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/mediation/MediationHandler.kt index a24f9b7f5..d5c613ca2 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/mediation/MediationHandler.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/mediation/MediationHandler.kt @@ -57,4 +57,16 @@ interface MediationHandler { * @param ids An array of message IDs to register as read. */ suspend fun registerMessagesAsRead(ids: Array) + + /** + * Listens for unread messages from a specified WebSocket service endpoint. + * + * @param serviceEndpointUri The URI of the service endpoint. It should be a valid WebSocket URI. + * @param onMessageCallback A callback function that is invoked when a message is received. + * This function is responsible for handling the incoming message. + */ + suspend fun listenUnreadMessages( + serviceEndpointUri: String, + onMessageCallback: OnMessageCallback + ) } diff --git a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/ProtocolType.kt b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/ProtocolType.kt index 6aaab7d13..389c20502 100644 --- a/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/ProtocolType.kt +++ b/atala-prism-sdk/src/commonMain/kotlin/io/iohk/atala/prism/walletsdk/prismagent/protocols/ProtocolType.kt @@ -34,6 +34,7 @@ enum class ProtocolType(val value: String) { PickupDelivery("https://didcomm.org/messagepickup/3.0/delivery"), PickupStatus("https://didcomm.org/messagepickup/3.0/status"), PickupReceived("https://didcomm.org/messagepickup/3.0/messages-received"), + LiveDeliveryChange("https://didcomm.org/messagepickup/3.0/live-delivery-change"), None(""); companion object { diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt new file mode 100644 index 000000000..bd683e94c --- /dev/null +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/ConnectionManagerTest.kt @@ -0,0 +1,171 @@ +@file:Suppress("ktlint:standard:import-ordering") + +package io.iohk.atala.prism.walletsdk.prismagent + +import io.iohk.atala.prism.walletsdk.domain.buildingblocks.Castor +import io.iohk.atala.prism.walletsdk.domain.buildingblocks.Mercury +import io.iohk.atala.prism.walletsdk.domain.buildingblocks.Pluto +import io.iohk.atala.prism.walletsdk.domain.models.Curve +import io.iohk.atala.prism.walletsdk.domain.models.DID +import io.iohk.atala.prism.walletsdk.domain.models.DIDDocument +import io.iohk.atala.prism.walletsdk.domain.models.DIDUrl +import io.iohk.atala.prism.walletsdk.domain.models.Message +import io.iohk.atala.prism.walletsdk.prismagent.mediation.MediationHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import java.util.UUID +import kotlin.test.assertNotNull +import kotlin.test.BeforeTest +import kotlin.test.Test + +class ConnectionManagerTest { + + @Mock + lateinit var mercuryMock: Mercury + + @Mock + lateinit var castorMock: Castor + + @Mock + lateinit var plutoMock: Pluto + + @Mock + lateinit var basicMediatorHandlerMock: MediationHandler + + lateinit var connectionManager: ConnectionManager + + val testDispatcher = TestCoroutineDispatcher() + + @BeforeTest + fun setup() { + MockitoAnnotations.openMocks(this) + connectionManager = ConnectionManager( + mercury = mercuryMock, + castor = castorMock, + pluto = plutoMock, + mediationHandler = basicMediatorHandlerMock, + pairings = mutableListOf(), + scope = CoroutineScope(testDispatcher) + ) + } + + @Test + fun testStartFetchingMessages_whenServiceEndpointContainsWSS_thenUseWebsockets() = runTest { + `when`(basicMediatorHandlerMock.mediatorDID) + .thenReturn(DID("did:prism:b6c0c33d701ac1b9a262a14454d1bbde3d127d697a76950963c5fd930605:Cj8KPRI7CgdtYXN0ZXIwEAFKLgoJc2VmsxEiECSTjyV7sUfCr_ArpN9rvCwR9fRMAhcsr_S7ZRiJk4p5k")) + + val vmAuthentication = DIDDocument.VerificationMethod( + id = DIDUrl(DID("2", "1", "0")), + controller = DID("2", "2", "0"), + type = Curve.ED25519.value, + publicKeyJwk = mapOf("crv" to Curve.ED25519.value, "x" to "") + ) + + val vmKeyAgreement = DIDDocument.VerificationMethod( + id = DIDUrl(DID("3", "1", "0")), + controller = DID("3", "2", "0"), + type = Curve.X25519.value, + publicKeyJwk = mapOf("crv" to Curve.X25519.value, "x" to "") + ) + + val vmService = DIDDocument.Service( + id = UUID.randomUUID().toString(), + type = emptyArray(), + serviceEndpoint = DIDDocument.ServiceEndpoint( + uri = "wss://serviceEndpoint" + ) + ) + + val didDoc = DIDDocument( + id = DID("did:prism:asdfasdf"), + coreProperties = arrayOf( + DIDDocument.Authentication( + urls = emptyArray(), + verificationMethods = arrayOf(vmAuthentication) + ), + DIDDocument.KeyAgreement( + urls = emptyArray(), + verificationMethods = arrayOf(vmKeyAgreement) + ), + DIDDocument.Services( + values = arrayOf(vmService) + ) + ) + ) + + `when`(castorMock.resolveDID(any())).thenReturn(didDoc) + + connectionManager.startFetchingMessages() + assertNotNull(connectionManager.fetchingMessagesJob) + verify(basicMediatorHandlerMock).listenUnreadMessages(any(), any()) + } + + @Test + fun testStartFetchingMessages_whenServiceEndpointNotContainsWSS_thenUseAPIRequest() = + runBlockingTest { + `when`(basicMediatorHandlerMock.mediatorDID) + .thenReturn(DID("did:prism:b6c0c33d701ac1b9a262a14454d1bbde3d127d697a76950963c5fd930605:Cj8KPRI7CgdtYXN0ZXIwEAFKLgoJc2VmsxEiECSTjyV7sUfCr_ArpN9rvCwR9fRMAhcsr_S7ZRiJk4p5k")) + + val vmAuthentication = DIDDocument.VerificationMethod( + id = DIDUrl(DID("2", "1", "0")), + controller = DID("2", "2", "0"), + type = Curve.ED25519.value, + publicKeyJwk = mapOf("crv" to Curve.ED25519.value, "x" to "") + ) + + val vmKeyAgreement = DIDDocument.VerificationMethod( + id = DIDUrl(DID("3", "1", "0")), + controller = DID("3", "2", "0"), + type = Curve.X25519.value, + publicKeyJwk = mapOf("crv" to Curve.X25519.value, "x" to "") + ) + + val vmService = DIDDocument.Service( + id = UUID.randomUUID().toString(), + type = emptyArray(), + serviceEndpoint = DIDDocument.ServiceEndpoint( + uri = "https://serviceEndpoint" + ) + ) + + val didDoc = DIDDocument( + id = DID("did:prism:asdfasdf"), + coreProperties = arrayOf( + DIDDocument.Authentication( + urls = emptyArray(), + verificationMethods = arrayOf(vmAuthentication) + ), + DIDDocument.KeyAgreement( + urls = emptyArray(), + verificationMethods = arrayOf(vmKeyAgreement) + ), + DIDDocument.Services( + values = arrayOf(vmService) + ) + ) + ) + + `when`(castorMock.resolveDID(any())).thenReturn(didDoc) + val messages = arrayOf(Pair("1234", Message(piuri = "", body = ""))) + `when`(basicMediatorHandlerMock.pickupUnreadMessages(any())).thenReturn( + flow { + emit( + messages + ) + } + ) + + connectionManager.startFetchingMessages() + assertNotNull(connectionManager.fetchingMessagesJob) + verify(basicMediatorHandlerMock).pickupUnreadMessages(10) + verify(basicMediatorHandlerMock).registerMessagesAsRead(arrayOf("1234")) + } +} diff --git a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/MediationHandlerMock.kt b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/MediationHandlerMock.kt index bc079adf7..c7e074bf2 100644 --- a/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/MediationHandlerMock.kt +++ b/atala-prism-sdk/src/commonTest/kotlin/io/iohk/atala/prism/walletsdk/prismagent/MediationHandlerMock.kt @@ -4,6 +4,7 @@ import io.iohk.atala.prism.walletsdk.domain.models.DID import io.iohk.atala.prism.walletsdk.domain.models.Mediator import io.iohk.atala.prism.walletsdk.domain.models.Message import io.iohk.atala.prism.walletsdk.prismagent.mediation.MediationHandler +import io.iohk.atala.prism.walletsdk.prismagent.mediation.OnMessageCallback import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.util.UUID @@ -44,4 +45,11 @@ class MediationHandlerMock( @Throws() override suspend fun registerMessagesAsRead(ids: Array) { } + + override suspend fun listenUnreadMessages( + serviceEndpointUri: String, + onMessageCallback: OnMessageCallback + ) { + TODO("Not yet implemented") + } } diff --git a/sampleapp/src/main/res/values/strings.xml b/sampleapp/src/main/res/values/strings.xml index b2c618f72..86a5f4ab2 100644 --- a/sampleapp/src/main/res/values/strings.xml +++ b/sampleapp/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ Mediator DID: Agent status: - did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjoiaHR0cDovLzE5Mi4xNjguNjguMTAzOjgwODAiLCJyIjpbXSwiYSI6WyJkaWRjb21tL3YyIl19 + did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjoiaHR0cDovLzE5Mi4xNjguNjguMTAzOjgwODAiLCJyIjpbXSwiYSI6WyJkaWRjb21tL3YyIl19 Credentials Host: