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
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
14 changes: 7 additions & 7 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
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,24 @@ interface IncomingRequestRepository {

suspend fun add(incomingRequest: IncomingRequest)

suspend fun addFirst(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
}

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

private val requestQueue = LinkedList<QueueItem>()

Expand Down Expand Up @@ -68,9 +70,36 @@ 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 addFirst(incomingRequest: IncomingRequest) {
mutex.withLock {
if (requestQueue.firstOrNull() is QueueItem.HighPriorityScreen) {
requestQueue.add(1, QueueItem.RequestItem(incomingRequest))
} else {
requestQueue.addFirst(QueueItem.RequestItem(incomingRequest))
_currentRequestToHandle.value?.let {
Timber.d("🗂 Dismissing request with id ${it.interactionId}")
appEventBus.sendEvent(AppEvent.DismissRequestHandling(it.interactionId))
}
handleNextRequest()
Timber.d(
"🗂 new incoming request with id ${incomingRequest.interactionId} " +
"added in list, so size now is ${getAmountOfRequests()}"
)
}
}
}

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 }
handleNextRequest()
Timber.d("🗂 request $requestId handled so size of list is now: ${getAmountOfRequests()}")
}
Expand All @@ -83,9 +112,11 @@ class IncomingRequestRepositoryImpl @Inject constructor() : IncomingRequestRepos
return
}

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

override fun getUnauthorizedRequest(requestId: String): IncomingRequest.UnauthorizedRequest? {
override fun getRequest(requestId: String): IncomingRequest? {
val queueItem = requestQueue.find {
it is QueueItem.RequestItem && it.incomingRequest.interactionId.toString() == requestId &&
it.incomingRequest is IncomingRequest.UnauthorizedRequest
it is QueueItem.RequestItem && it.incomingRequest.interactionId == requestId
}
if (queueItem == null) {
Timber.w("Unauthorized request with id $requestId is null")
Timber.w("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? {
val queueItem = requestQueue.find {
it is QueueItem.RequestItem && it.incomingRequest.interactionId.toString() == requestId &&
it.incomingRequest is IncomingRequest.AuthorizedRequest
}
if (queueItem == null) {
Timber.w("Authorized 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 @@ -156,7 +164,7 @@ class IncomingRequestRepositoryImpl @Inject constructor() : IncomingRequestRepos

private sealed interface QueueItem {
data object HighPriorityScreen : QueueItem

data class RequestItem(val incomingRequest: IncomingRequest) : QueueItem
data class MobileConnectItem(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
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 @@ -3,7 +3,6 @@ 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.domain.model.IncomingMessage
import com.babylon.wallet.android.domain.model.deeplink.DeepLinkEvent
import com.radixdlt.sargon.RadixConnectMobile
import timber.log.Timber
import javax.inject.Inject
Expand All @@ -13,23 +12,19 @@ class ProcessDeepLinkUseCase @Inject constructor(
private val incomingRequestRepository: IncomingRequestRepository
) {

suspend operator fun invoke(deepLink: String): DeepLinkEvent? {
suspend operator fun invoke(deepLink: String) {
val sessionRequest = runCatching { radixConnectMobile.handleDeepLink(deepLink) }.onFailure {
Timber.d("Failed to parse deep link: $deepLink. Error: ${it.message}")
return null
return
}.getOrThrow()
jakub-rdx marked this conversation as resolved.
Show resolved Hide resolved

return if (sessionRequest.originRequiresValidation) {
DeepLinkEvent.MobileConnectVerifyRequest(
request = sessionRequest
)
} else {
incomingRequestRepository.add(
sessionRequest.interaction.toDomainModel(
remoteEntityId = IncomingMessage.RemoteEntityID.RadixMobileConnectRemoteSession(sessionRequest.sessionId.toString())
).getOrThrow()
)
null
}
incomingRequestRepository.addFirst(
sessionRequest.interaction.toDomainModel(
remoteEntityId = IncomingMessage.RemoteEntityID.RadixMobileConnectRemoteSession(
id = sessionRequest.sessionId.toString(),
originVerificationUrl = if (sessionRequest.originRequiresValidation) sessionRequest.origin else null
)
).getOrThrow()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import com.radixdlt.sargon.SharedToDappWithPersonaAccountAddresses
import com.radixdlt.sargon.extensions.ReferencesToAuthorizedPersonas
import com.radixdlt.sargon.extensions.init
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
Expand Down Expand Up @@ -92,9 +93,16 @@ class DAppAuthorizedLoginViewModel @Inject constructor(
override fun initialState(): DAppLoginUiState = DAppLoginUiState()

init {
viewModelScope.launch {
appEventBus.events.filterIsInstance<AppEvent.DismissRequestHandling>().collect {
if (it.interactionId == args.interactionId) {
sendEvent(Event.CloseLoginFlow)
}
}
}
observeSigningState()
viewModelScope.launch {
val requestToHandle = incomingRequestRepository.getAuthorizedRequest(args.interactionId)
val requestToHandle = incomingRequestRepository.getRequest(args.interactionId) as? AuthorizedRequest
if (requestToHandle == null) {
sendEvent(Event.CloseLoginFlow)
return@launch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal const val ARG_REQUEST_ID = "request_id"
const val ROUTE_DAPP_LOGIN_UNAUTHORIZED_GRAPH = "dapp_login_unauthorized/{$ARG_REQUEST_ID}"
const val ROUTE_DAPP_LOGIN_UNAUTHORIZED_SCREEN = "dapp_login_unauthorized_screen/{$ARG_REQUEST_ID}"

internal class DAppUnauthorizedLoginArgs(val requestId: String) {
internal class DAppUnauthorizedLoginArgs(val interactionId: String) {
constructor(savedStateHandle: SavedStateHandle) : this(checkNotNull(savedStateHandle[ARG_REQUEST_ID]) as String)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import rdx.works.core.domain.DApp
Expand Down Expand Up @@ -69,7 +70,14 @@ class DAppUnauthorizedLoginViewModel @Inject constructor(
init {
observeSigningState()
viewModelScope.launch {
val requestToHandle = incomingRequestRepository.getUnauthorizedRequest(args.requestId)
appEventBus.events.filterIsInstance<AppEvent.DismissRequestHandling>().collect {
if (it.interactionId == args.interactionId) {
sendEvent(Event.CloseLoginFlow)
}
}
}
viewModelScope.launch {
val requestToHandle = incomingRequestRepository.getRequest(args.interactionId) as? IncomingMessage.IncomingRequest.UnauthorizedRequest
if (requestToHandle == null) {
sendEvent(Event.CloseLoginFlow)
return@launch
Expand Down Expand Up @@ -187,7 +195,7 @@ class DAppUnauthorizedLoginViewModel @Inject constructor(
respondToIncomingRequestUseCase.respondWithFailure(request, exception.ceError, exception.getDappMessage())
_state.update { it.copy(failureDialogState = FailureDialogState.Closed) }
sendEvent(Event.CloseLoginFlow)
incomingRequestRepository.requestHandled(requestId = args.requestId)
incomingRequestRepository.requestHandled(requestId = args.interactionId)
}

fun onMessageShown() {
Expand Down Expand Up @@ -218,7 +226,7 @@ class DAppUnauthorizedLoginViewModel @Inject constructor(

fun onRejectRequest() {
viewModelScope.launch {
incomingRequestRepository.requestHandled(requestId = args.requestId)
incomingRequestRepository.requestHandled(requestId = args.interactionId)
respondToIncomingRequestUseCase.respondWithFailure(request, DappWalletInteractionErrorType.REJECTED_BY_USER)
sendEvent(Event.CloseLoginFlow)
}
Expand Down
Loading
Loading