Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update mobile connect flow and UI #1008

Merged
merged 13 commits into from
Jun 27, 2024
Merged
9 changes: 0 additions & 9 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,6 @@

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="https" />
<data android:host="dr6vsuukf8610.cloudfront.net" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/com/babylon/wallet/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ class MainActivity : FragmentActivity() {
super.onCreate(savedInstanceState)
cloudBackupSyncExecutor.startPeriodicChecks(lifecycleOwner = this)

intent.data?.let { viewModel.handleDeepLink(it) }
intent.data?.let {
intent.replaceExtras(Bundle())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice that you added this here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually can someone explain me what does this line do? Because I see it in the line 103, too.
Does it "clear" the intent with an empty bundle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it removes data from an intent, so deep link is not delivered 2nd time if you move app to background and open from recent

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the app is moved to background and re-opened from recent neither onCreate nor onNewIntent should get called in normal circumstances, unless the app task has been closed by the OS? Is that the case or there are simple repro steps for the issue that this line fixes? I'm really curious about what is happening.

viewModel.handleDeepLink(it)
}
setContent {
RadixWalletTheme {
val isVisible by balanceVisibilityObserver.isBalanceVisible.collectAsState(initial = true)
Expand Down Expand Up @@ -100,7 +103,10 @@ class MainActivity : FragmentActivity() {

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.data?.let { viewModel.handleDeepLink(it) }
intent?.data?.let {
this.intent.replaceExtras(Bundle())
viewModel.handleDeepLink(it)
}
}

private fun setSplashExitAnimation(splashScreen: SplashScreen) {
Expand Down
18 changes: 9 additions & 9 deletions app/src/main/java/com/babylon/wallet/android/WalletApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,26 @@ fun WalletApp(
mainViewModel.oneOffEvent.collect { event ->
when (event) {
is MainEvent.IncomingRequestEvent -> {
if (event.request.needVerification) {
navController.mobileConnect(event.request.interactionId)
return@collect
}
when (val incomingRequest = event.request) {
is IncomingMessage.IncomingRequest.TransactionRequest -> {
navController.transactionReview(
requestId = incomingRequest.interactionId.toString()
requestId = incomingRequest.interactionId
)
}

is IncomingMessage.IncomingRequest.AuthorizedRequest -> {
navController.dAppLoginAuthorized(incomingRequest.interactionId.toString())
navController.dAppLoginAuthorized(incomingRequest.interactionId)
}

is IncomingMessage.IncomingRequest.UnauthorizedRequest -> {
navController.dAppLoginUnauthorized(incomingRequest.interactionId.toString())
navController.dAppLoginUnauthorized(incomingRequest.interactionId)
}
}
}

is MainEvent.MobileConnectLink -> {
navController.mobileConnect(event.request)
}
}
}
}
Expand Down Expand Up @@ -177,10 +177,10 @@ fun WalletApp(
dismissText = null
)
}
if (!state.isProfileInitialized) {
if (state.showMobileConnectWarning) {
BasicPromptAlertDialog(
finish = {
onCloseApp()
mainViewModel.onMobileConnectWarningShown()
},
titleText = "No profile found",
messageText = "You need to create a profile to respond to dApp requests",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.babylon.wallet.android.data.dapp

import com.babylon.wallet.android.domain.model.IncomingMessage.IncomingRequest
import com.babylon.wallet.android.utils.AppEvent
import com.babylon.wallet.android.utils.AppEventBus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
Expand All @@ -17,24 +19,25 @@ interface IncomingRequestRepository {

suspend fun add(incomingRequest: IncomingRequest)

suspend fun addMobileConnectRequest(incomingRequest: IncomingRequest)

suspend fun requestHandled(requestId: String)

suspend fun pauseIncomingRequests()

suspend fun resumeIncomingRequests()

fun getUnauthorizedRequest(requestId: String): IncomingRequest.UnauthorizedRequest?

fun getTransactionWriteRequest(requestId: String): IncomingRequest.TransactionRequest?

fun getAuthorizedRequest(requestId: String): IncomingRequest.AuthorizedRequest?
Comment on lines -26 to -30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice that you removed those 👍

fun getRequest(requestId: String): IncomingRequest?

fun removeAll()

fun getAmountOfRequests(): Int
suspend fun requestDismissed(requestId: String)
jakub-rdx marked this conversation as resolved.
Show resolved Hide resolved
}

class IncomingRequestRepositoryImpl @Inject constructor() : IncomingRequestRepository {
class IncomingRequestRepositoryImpl @Inject constructor(
private val appEventBus: AppEventBus
) : IncomingRequestRepository {

private val requestQueue = LinkedList<QueueItem>()

Expand Down Expand Up @@ -68,25 +71,71 @@ class IncomingRequestRepositoryImpl @Inject constructor() : IncomingRequestRepos
}
}

/**
* There are two path of execution when using this method:
* - there is high priority screen in the queue, so the incoming request is added below it,
* taking priority over requests currently in the queue
* - there is no high priority screen: request is added at the top of queue and if there other request currently handled,
* we send a dismiss event for it so that UI can react and dismiss handling, without removing it from the queue.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"there is no high priority screen: request is added at the top of queue and if there other request currently handled, ..."

here you mean that if wallet receives another high priority request while another non high priority is being handled then we dismiss it and so on... right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually what I am thinking is that the term dismiss is not the right one to describe this particular business logic. "Dismiss" is like reject. We reject this request and we will never see it again.

But what if we use postpone which describes that we delay the handling of this request?
so it becomes override suspend fun requestPostponed and any other "dismiss" rename to "postpone"?

I would like to hear the opinion of others. Also any other word is welcome!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh now I think defer is even better.
override suspend fun requestDeferred

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, whenever this method is used, request added with it is supposed to go on screen while current one is deferred. This does not apply if we are in flows like creating accounts or other flows marked as high priority.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not apply if we are in flows like creating accounts or other flows marked as high priority.

wait I think I lost it here, you mean we really dismiss the request in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, we never dimiss when using addMobileConnectRequest(). It is just that we don't process it immediately. Example: when mobile connect request (MCR) comes in and we are in the middle of some other dApp login, dapp login is deferred and MCR takes over. But when we are in a wallet doing something, like account creation (high priority flow), then request is delivered after that particular screen/flow is complete.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice thank you

* Dismissed request will be handled again when top priority one handling completes
jakub-rdx marked this conversation as resolved.
Show resolved Hide resolved
*/
override suspend fun addMobileConnectRequest(incomingRequest: IncomingRequest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe addDeferredRequest instead of naming it to mobile connect

Copy link
Contributor Author

@jakub-rdx jakub-rdx Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not deferred request, mobile connect ones take priority over others. Will rename to addPriorityRequest

mutex.withLock {
requestQueue.addFirst(QueueItem.RequestItem(incomingRequest))
val currentRequest = _currentRequestToHandle.value
val handlingPaused = requestQueue.contains(QueueItem.HighPriorityScreen)
when {
currentRequest != null -> {
Timber.d("🗂 Dismissing request with id ${currentRequest.interactionId}")
jakub-rdx marked this conversation as resolved.
Show resolved Hide resolved
appEventBus.sendEvent(AppEvent.DismissRequestHandling(currentRequest.interactionId))
}

else -> {
if (!handlingPaused) {
handleNextRequest()
}
}
}
}
}

override suspend fun requestHandled(requestId: String) {
mutex.withLock {
requestQueue.removeIf { it is QueueItem.RequestItem && it.incomingRequest.interactionId.toString() == requestId }
requestQueue.removeIf { it is QueueItem.RequestItem && it.incomingRequest.interactionId == requestId }
clearCurrent(requestId)
handleNextRequest()
Timber.d("🗂 request $requestId handled so size of list is now: ${getAmountOfRequests()}")
}
}

override suspend fun requestDismissed(requestId: String) {
mutex.withLock {
clearCurrent(requestId)
handleNextRequest()
Timber.d("🗂 request $requestId handled so size of list is now: ${getAmountOfRequests()}")
}
}

private suspend fun clearCurrent(requestId: String) {
if (_currentRequestToHandle.value?.interactionId == requestId) {
_currentRequestToHandle.emit(null)
}
}

override suspend fun pauseIncomingRequests() {
mutex.withLock {
// If the queue knows about any high priority item, no need to add it again
if (requestQueue.contains(QueueItem.HighPriorityScreen)) {
return
}

// Put high priority item below any internal request
val topQueueItem = requestQueue.peekFirst()
if (topQueueItem is QueueItem.RequestItem && topQueueItem.incomingRequest.isInternal) {
requestQueue.add(1, QueueItem.HighPriorityScreen)
// Put high priority item below any internal request and mobile connect requests
val index = requestQueue.indexOfFirst {
val item = it as? QueueItem.RequestItem
item != null && !item.incomingRequest.isInternal && !item.incomingRequest.isMobileConnectRequest
}
if (index != -1) {
requestQueue.add(index, QueueItem.HighPriorityScreen)
} else {
requestQueue.addFirst(QueueItem.HighPriorityScreen)
}
Expand All @@ -104,37 +153,14 @@ class IncomingRequestRepositoryImpl @Inject constructor() : IncomingRequestRepos
}
}

override fun getUnauthorizedRequest(requestId: String): IncomingRequest.UnauthorizedRequest? {
val queueItem = requestQueue.find {
it is QueueItem.RequestItem && it.incomingRequest.interactionId.toString() == requestId &&
it.incomingRequest is IncomingRequest.UnauthorizedRequest
}
if (queueItem == null) {
Timber.w("Unauthorized request with id $requestId is null")
}
return (queueItem as? QueueItem.RequestItem)?.incomingRequest as? IncomingRequest.UnauthorizedRequest
}

override fun getTransactionWriteRequest(requestId: String): IncomingRequest.TransactionRequest? {
val queueItem = requestQueue.find {
it is QueueItem.RequestItem && it.incomingRequest.interactionId.toString() == requestId &&
it.incomingRequest is IncomingRequest.TransactionRequest
}
if (queueItem == null) {
Timber.w("Transaction request with id $requestId is null")
}
return (queueItem as? QueueItem.RequestItem)?.incomingRequest as? IncomingRequest.TransactionRequest
}

override fun getAuthorizedRequest(requestId: String): IncomingRequest.AuthorizedRequest? {
override fun getRequest(requestId: String): IncomingRequest? {
val queueItem = requestQueue.find {
it is QueueItem.RequestItem && it.incomingRequest.interactionId.toString() == requestId &&
it.incomingRequest is IncomingRequest.AuthorizedRequest
it is QueueItem.RequestItem && it.incomingRequest.interactionId == requestId
}
if (queueItem == null) {
Timber.w("Authorized request with id $requestId is null")
Timber.w("Request with id $requestId is null")
}
return (queueItem as? QueueItem.RequestItem)?.incomingRequest as? IncomingRequest.AuthorizedRequest
return (queueItem as? QueueItem.RequestItem)?.incomingRequest
}

override fun removeAll() {
Expand All @@ -149,14 +175,13 @@ class IncomingRequestRepositoryImpl @Inject constructor() : IncomingRequestRepos
// In order to emit an incoming request, the topmost item should be
// a. An incoming request
// b. It should not be the same as the one being handled already
if (nextRequest is QueueItem.RequestItem && _currentRequestToHandle.value != nextRequest) {
if (nextRequest is QueueItem.RequestItem && _currentRequestToHandle.value != nextRequest.incomingRequest) {
_currentRequestToHandle.emit(nextRequest.incomingRequest)
}
}

private sealed interface QueueItem {
data object HighPriorityScreen : QueueItem

data class RequestItem(val incomingRequest: IncomingRequest) : QueueItem
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class PeerdroidClientImpl @Inject constructor(
if (interactionVersion != Constants.WALLET_INTERACTION_VERSION) {
throw IncompatibleRequestVersionException(
requestVersion = interactionVersion,
requestId = dappInteraction.interactionId.toString()
requestId = dappInteraction.interactionId
)
}
dappInteraction.toDomainModel(remoteEntityId = IncomingMessage.RemoteEntityID.ConnectorId(remoteConnectorId)).getOrThrow()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.babylon.wallet.android.data.repository

import com.babylon.wallet.android.data.dapp.IncomingRequestRepository
import com.babylon.wallet.android.domain.model.IncomingMessage
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class BufferedDeepLinkRepository @Inject constructor(
private val incomingRequestRepository: IncomingRequestRepository,
) {

private var bufferedRequest: IncomingMessage.IncomingRequest? = null

fun setBufferedRequest(request: IncomingMessage.IncomingRequest) {
bufferedRequest = request
}

suspend fun processBufferedRequest() {
bufferedRequest?.let {
incomingRequestRepository.addMobileConnectRequest(it)
bufferedRequest = null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ sealed interface IncomingMessage {

val value: String

data class RadixMobileConnectRemoteSession(val id: String) : RemoteEntityID {
data class RadixMobileConnectRemoteSession(val id: String, val originVerificationUrl: String? = null) : RemoteEntityID {
override val value: String
get() = id

val needOriginVerification: Boolean = originVerificationUrl != null
}

data class ConnectorId(val id: String) : RemoteEntityID {
Expand All @@ -50,6 +52,9 @@ sealed interface IncomingMessage {
val isMobileConnectRequest: Boolean
get() = remoteEntityId is RemoteEntityID.RadixMobileConnectRemoteSession

val needVerification: Boolean
get() = (remoteEntityId as? RemoteEntityID.RadixMobileConnectRemoteSession)?.needOriginVerification == true

val blockUntilComplete: Boolean
get() {
return metadata.blockUntilCompleted
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,44 @@ package com.babylon.wallet.android.domain.usecases.deeplink

import com.babylon.wallet.android.data.dapp.IncomingRequestRepository
import com.babylon.wallet.android.data.dapp.model.toDomainModel
import com.babylon.wallet.android.data.repository.BufferedDeepLinkRepository
import com.babylon.wallet.android.domain.model.IncomingMessage
import com.babylon.wallet.android.domain.model.deeplink.DeepLinkEvent
import com.radixdlt.sargon.RadixConnectMobile
import rdx.works.profile.domain.GetProfileUseCase
import timber.log.Timber
import javax.inject.Inject

class ProcessDeepLinkUseCase @Inject constructor(
private val radixConnectMobile: RadixConnectMobile,
private val incomingRequestRepository: IncomingRequestRepository
private val incomingRequestRepository: IncomingRequestRepository,
private val getProfileUseCase: GetProfileUseCase,
private val bufferedDeepLinkRepository: BufferedDeepLinkRepository
) {

suspend operator fun invoke(deepLink: String): DeepLinkEvent? {
val sessionRequest = runCatching { radixConnectMobile.handleDeepLink(deepLink) }.onFailure {
suspend operator fun invoke(deepLink: String): Result<DeepLinkProcessingResult> {
return runCatching {
val profileInitialized = getProfileUseCase.isInitialized()
val sessionRequest = radixConnectMobile.handleDeepLink(deepLink)
val request = sessionRequest.interaction.toDomainModel(
remoteEntityId = IncomingMessage.RemoteEntityID.RadixMobileConnectRemoteSession(
id = sessionRequest.sessionId.toString(),
originVerificationUrl = if (sessionRequest.originRequiresValidation) sessionRequest.origin else null
)
).getOrThrow()
if (profileInitialized) {
incomingRequestRepository.addMobileConnectRequest(request)
DeepLinkProcessingResult.PROCESSED
} else {
bufferedDeepLinkRepository.setBufferedRequest(request)
DeepLinkProcessingResult.BUFFERED
}
}.onFailure {
Timber.d("Failed to parse deep link: $deepLink. Error: ${it.message}")
return null
}.getOrThrow()

return if (sessionRequest.originRequiresValidation) {
DeepLinkEvent.MobileConnectVerifyRequest(
request = sessionRequest
)
} else {
incomingRequestRepository.add(
sessionRequest.interaction.toDomainModel(
remoteEntityId = IncomingMessage.RemoteEntityID.RadixMobileConnectRemoteSession(sessionRequest.sessionId.toString())
).getOrThrow()
)
null
}
}
}

enum class DeepLinkProcessingResult {
PROCESSED,
BUFFERED
}
Loading
Loading