Skip to content

Commit

Permalink
Feature/nevisaccessapp 6287 multi user transaction confirmation fixes (
Browse files Browse the repository at this point in the history
…#27)

- Pass only the filtered (valid) accounts to transaction confirmation and account selector when there are multiple registered accounts
- Change the order Account Selection and Transaction Confirmation screens are shown: the former now displayed before the latter
- Transaction Confirmation screen now only deals with a single Account as that should be selected before that
- Transaction Confirmation message is passed along Account Selection now
- Simplified/clarified logic in AccountSelectorImpl
- Add TransactionConfirmationResponse and TransactionConfirmationUseCase and its implementation, the former is used by AccountSelectorImpl and the latter is used by SelectAccountViewModel to show transaction confirmation when a message is received
  • Loading branch information
balazs-gerlei authored Nov 8, 2024
1 parent 1c9c4bb commit b3519f2
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ import ch.nevis.exampleapp.coroutines.domain.usecase.StartChangePasswordUseCase
import ch.nevis.exampleapp.coroutines.domain.usecase.StartChangePasswordUseCaseImpl
import ch.nevis.exampleapp.coroutines.domain.usecase.StartChangePinUseCase
import ch.nevis.exampleapp.coroutines.domain.usecase.StartChangePinUseCaseImpl
import ch.nevis.exampleapp.coroutines.domain.usecase.TransactionConfirmationUseCase
import ch.nevis.exampleapp.coroutines.domain.usecase.TransactionConfirmationUseCaseImpl
import ch.nevis.exampleapp.coroutines.domain.usecase.VerifyBiometricUseCase
import ch.nevis.exampleapp.coroutines.domain.usecase.VerifyBiometricUseCaseImpl
import ch.nevis.exampleapp.coroutines.domain.usecase.VerifyDevicePasscodeUseCase
Expand Down Expand Up @@ -797,6 +799,14 @@ class ApplicationModule {
clientProvider: ClientProvider
): ChangeDeviceInformationUseCase = ChangeDeviceInformationUseCaseImpl(clientProvider)

/**
* Provides use case for transaction confirmation.
*
* @return The use case for transaction confirmation.
*/
@Provides
fun provideTransactionConfirmationUseCase(): TransactionConfirmationUseCase = TransactionConfirmationUseCaseImpl()

/**
* Provides use case for starting an in-band authentication operation.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/**
* Nevis Mobile Authentication SDK Example App
*
* Copyright © 2022. Nevis Security AG. All rights reserved.
* Copyright © 2022-2024. Nevis Security AG. All rights reserved.
*/

package ch.nevis.exampleapp.coroutines.domain.interaction

import ch.nevis.exampleapp.coroutines.domain.model.error.BusinessException
import ch.nevis.exampleapp.coroutines.domain.model.response.ErrorResponse
import ch.nevis.exampleapp.coroutines.domain.model.response.SelectAccountResponse
import ch.nevis.exampleapp.coroutines.domain.model.response.TransactionConfirmationResponse
import ch.nevis.exampleapp.coroutines.domain.model.state.UserInteractionOperationState
import ch.nevis.exampleapp.coroutines.domain.repository.OperationStateRepository
import ch.nevis.exampleapp.coroutines.timber.sdk
Expand Down Expand Up @@ -46,24 +47,33 @@ class AccountSelectorImpl(
val cancellableContinuation =
operationState.cancellableContinuation ?: throw BusinessException.invalidState()

val transactionConfirmationData =
val transactionConfirmationData: ByteArray? =
accountSelectionContext.transactionConfirmationData().orElse(null)
val accounts = validAccounts(accountSelectionContext)

if (accounts.isEmpty()) {
cancellableContinuation.resume(ErrorResponse(BusinessException.accountsNotFound()))
} else if (transactionConfirmationData == null && accounts.size == 1) {
accountSelectionHandler.username(
accountSelectionContext.accounts().first().username()
)
} else {
cancellableContinuation.resume(
SelectAccountResponse(
operationState.operation,
accountSelectionContext.accounts(),
transactionConfirmationData
when(accounts.size) {
0 -> cancellableContinuation.resume(ErrorResponse(BusinessException.accountsNotFound()))
1 -> {
if (transactionConfirmationData != null) {
cancellableContinuation.resume(
TransactionConfirmationResponse(
account = accounts.first(),
transactionConfirmationMessage = transactionConfirmationData.decodeToString()
)
)
} else {
accountSelectionHandler.username(accounts.first().username())
}
}
else -> {
cancellableContinuation.resume(
SelectAccountResponse(
operation = operationState.operation,
accounts = accounts,
transactionConfirmationMessage = transactionConfirmationData?.decodeToString()
)
)
)
}
}
}
//endregion
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Nevis Mobile Authentication SDK Example App
*
* Copyright © 2022. Nevis Security AG. All rights reserved.
* Copyright © 2022-2024. Nevis Security AG. All rights reserved.
*/

package ch.nevis.exampleapp.coroutines.domain.model.response
Expand All @@ -12,14 +12,14 @@ import ch.nevis.mobile.sdk.api.localdata.Account
/**
* [Response] class that indicates an account selection has to be started.
* Typically the received [Account] set is shown to the user and he/she selects one of them.
* After the account selection [ch.nevis.exampleapp.coroutines.domain.usecase.SelectAccountUseCase] is called
* to continue the operation.
* After the account selection [ch.nevis.exampleapp.coroutines.domain.usecase.SelectAccountUseCase]
* is called to continue the operation.
*
* @constructor Creates a new instance.
* @param operation The [Operation] the account selection requested for.
* @param accounts The set of enrolled accounts the user has to choose from.
* @param transactionConfirmationData The optional transaction data that might be sent during an
* authentication process.
* @param transactionConfirmationMessage The optional transaction data/message that might be sent
* during an authentication process.
*/
class SelectAccountResponse(

Expand All @@ -34,7 +34,7 @@ class SelectAccountResponse(
val accounts: Set<Account>,

/**
* The optional transaction data that might be sent during an authentication process.
* The optional transaction data/message that is sent during an authentication process.
*/
val transactionConfirmationData: ByteArray? = null
val transactionConfirmationMessage: String? = null
) : Response
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Nevis Mobile Authentication SDK Example App
*
* Copyright © 2024. Nevis Security AG. All rights reserved.
*/

package ch.nevis.exampleapp.coroutines.domain.model.response

import ch.nevis.mobile.sdk.api.localdata.Account

/**
* [Response] class that indicates a transaction confirmation has to be started.
* After the transaction confirmation, [ch.nevis.exampleapp.coroutines.domain.usecase.SelectAccountUseCase]
* should be called with the contained account to continue the operation.
*
* @constructor Creates a new instance.
* @param account The previously selected account.
* @param transactionConfirmationMessage The transaction data/message that is be sent during an
* authentication process.
*/
class TransactionConfirmationResponse (

/**
* The previously selected account.
*/
val account: Account,

/**
* The optional transaction data/message that is sent during an authentication process.
*/
val transactionConfirmationMessage: String
) : Response
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Nevis Mobile Authentication SDK Example App
*
* Copyright © 2024. Nevis Security AG. All rights reserved.
*/

package ch.nevis.exampleapp.coroutines.domain.usecase

import ch.nevis.exampleapp.coroutines.domain.model.response.Response
import ch.nevis.mobile.sdk.api.localdata.Account

/**
* Use-case interface for transaction confirmation.
*/
interface TransactionConfirmationUseCase {

/**
* Executes the use-case.
*
* @param account The account that is used for the out-of-band authentication.
* @param transactionConfirmationMessage The transaction data/message that is be sent during an
* authentication process.
* @return A [Response] object that indicates the result of the use-case execution.
*/
suspend fun execute(account: Account, transactionConfirmationMessage: String): Response
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Nevis Mobile Authentication SDK Example App
*
* Copyright © 2024. Nevis Security AG. All rights reserved.
*/

package ch.nevis.exampleapp.coroutines.domain.usecase

import ch.nevis.exampleapp.coroutines.domain.model.response.Response
import ch.nevis.exampleapp.coroutines.domain.model.response.TransactionConfirmationResponse
import ch.nevis.mobile.sdk.api.localdata.Account
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

class TransactionConfirmationUseCaseImpl : TransactionConfirmationUseCase {

//region TransactionConfirmationUseCase
override suspend fun execute(account: Account, transactionConfirmationMessage: String): Response {
return suspendCancellableCoroutine { cancellableContinuation ->
cancellableContinuation.resume(
TransactionConfirmationResponse(
account,
transactionConfirmationMessage
)
)
}
}
//endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ch.nevis.exampleapp.coroutines.domain.model.response.ErrorResponse
import ch.nevis.exampleapp.coroutines.domain.model.response.Response
import ch.nevis.exampleapp.coroutines.domain.model.response.SelectAccountResponse
import ch.nevis.exampleapp.coroutines.domain.model.response.SelectAuthenticatorResponse
import ch.nevis.exampleapp.coroutines.domain.model.response.TransactionConfirmationResponse
import ch.nevis.exampleapp.coroutines.domain.model.response.VerifyBiometricResponse
import ch.nevis.exampleapp.coroutines.domain.model.response.VerifyDevicePasscodeResponse
import ch.nevis.exampleapp.coroutines.domain.model.response.VerifyFingerprintResponse
Expand Down Expand Up @@ -93,26 +94,23 @@ abstract class ResponseObserverFragment : Fragment() {
}

is SelectAccountResponse -> {
response.transactionConfirmationData?.also {
val parameter = TransactionConfirmationNavigationParameter(
response.operation,
response.accounts,
it.decodeToString()
)
val action =
NavigationGraphDirections.actionGlobalTransactionConfirmationFragment(
parameter
)
navController.navigate(action)
} ?: run {
val parameter = SelectAccountNavigationParameter(
response.operation,
response.accounts
)
val action =
NavigationGraphDirections.actionGlobalSelectAccountFragment(parameter)
navController.navigate(action)
}
val navigationParameter = SelectAccountNavigationParameter(
response.operation,
response.accounts,
response.transactionConfirmationMessage
)
val action =
NavigationGraphDirections.actionGlobalSelectAccountFragment(navigationParameter)
navController.navigate(action)
}

is TransactionConfirmationResponse -> {
val navigationParameter = TransactionConfirmationNavigationParameter(
response.account,
response.transactionConfirmationMessage
)
val action = NavigationGraphDirections.actionGlobalTransactionConfirmationFragment(navigationParameter)
navController.navigate(action)
}

is EnrollPinResponse -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Nevis Mobile Authentication SDK Example App
*
* Copyright © 2022. Nevis Security AG. All rights reserved.
* Copyright © 2022-2024. Nevis Security AG. All rights reserved.
*/

package ch.nevis.exampleapp.coroutines.ui.selectAccount
Expand Down Expand Up @@ -90,8 +90,9 @@ class SelectAccountFragment : ResponseObserverFragment(),
//region AccountSelectedListener
override fun onAccountSelected(account: Account) {
viewModel.selectAccount(
navigationArguments.parameter.operation,
account.username()
operation = navigationArguments.parameter.operation,
account = account,
transactionConfirmationMessage = navigationArguments.parameter.message
)
}
//endregion
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Nevis Mobile Authentication SDK Example App
*
* Copyright © 2022. Nevis Security AG. All rights reserved.
* Copyright © 2022-2024. Nevis Security AG. All rights reserved.
*/

package ch.nevis.exampleapp.coroutines.ui.selectAccount
Expand All @@ -10,8 +10,13 @@ import androidx.lifecycle.viewModelScope
import ch.nevis.exampleapp.coroutines.dagger.ApplicationModule
import ch.nevis.exampleapp.coroutines.domain.model.error.BusinessException
import ch.nevis.exampleapp.coroutines.domain.model.operation.Operation
import ch.nevis.exampleapp.coroutines.domain.usecase.*
import ch.nevis.exampleapp.coroutines.domain.usecase.InBandAuthenticationUseCase
import ch.nevis.exampleapp.coroutines.domain.usecase.SelectAccountUseCase
import ch.nevis.exampleapp.coroutines.domain.usecase.StartChangePasswordUseCase
import ch.nevis.exampleapp.coroutines.domain.usecase.StartChangePinUseCase
import ch.nevis.exampleapp.coroutines.domain.usecase.TransactionConfirmationUseCase
import ch.nevis.exampleapp.coroutines.ui.base.CancellableOperationViewModel
import ch.nevis.mobile.sdk.api.localdata.Account
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
Expand All @@ -32,6 +37,7 @@ import javax.inject.Named
class SelectAccountViewModel @Inject constructor(
private val startChangePinUseCase: StartChangePinUseCase,
private val startChangePasswordUseCase: StartChangePasswordUseCase,
private val transactionConfirmationUseCase: TransactionConfirmationUseCase,
@Named(ApplicationModule.IN_BAND_AUTHENTICATION_USE_CASE_DEFAULT)
private val inBandAuthenticationUseCase: InBandAuthenticationUseCase,
@Named(ApplicationModule.IN_BAND_AUTHENTICATION_USE_CASE_FOR_DEREGISTRATION)
Expand All @@ -46,16 +52,23 @@ class SelectAccountViewModel @Inject constructor(
* [Operation.CHANGE_PASSWORD] and [Operation.OUT_OF_BAND_AUTHENTICATION].
*
* @param operation The operation the account selected for.
* @param username The username assigned to the selected account.
* @param account The selected account.
* @param transactionConfirmationMessage The transaction confirmation message
* that need to be confirmed or cancelled by the user.
*/
fun selectAccount(operation: Operation, username: String) {
fun selectAccount(operation: Operation, account: Account, transactionConfirmationMessage: String?) {
viewModelScope.launch(errorHandler) {
when (operation) {
Operation.AUTHENTICATION -> inBandAuthentication(username)
Operation.DEREGISTRATION -> inBandAuthenticationForDeregistration(username)
Operation.CHANGE_PIN -> changePin(username)
Operation.CHANGE_PASSWORD -> changePassword(username)
Operation.OUT_OF_BAND_AUTHENTICATION -> outOfBandAuthentication(username)
if (transactionConfirmationMessage != null) {
// Transaction confirmation data is received from the SDK
// Show it to the user for confirmation or cancellation
// The AccountSelectionHandler will be invoked or cancelled there.
confirm(transactionConfirmationMessage, account)
} else when (operation) {
Operation.AUTHENTICATION -> inBandAuthenticate(account.username())
Operation.DEREGISTRATION -> inBandAuthenticationForDeregistration(account.username())
Operation.CHANGE_PIN -> changePin(account.username())
Operation.CHANGE_PASSWORD -> changePassword(account.username())
Operation.OUT_OF_BAND_AUTHENTICATION -> outOfBandAuthentication(account.username())
else -> throw BusinessException.invalidState()
}
}
Expand Down Expand Up @@ -83,12 +96,23 @@ class SelectAccountViewModel @Inject constructor(
mutableResponseLiveData.postValue(response)
}

/**
* Confirms the transaction.
*
* @param message: The transaction confirmation message that need to be confirmed or cancelled by the user.
* @param account: The current account.
*/
private suspend fun confirm(message: String, account: Account) {
val response = transactionConfirmationUseCase.execute(account, message)
mutableResponseLiveData.postValue(response)
}

/**
* Starts an in-band authentication.
*
* @param username The username that identifies the account the in-band authentication must be started for.
*/
private suspend fun inBandAuthentication(username: String) {
private suspend fun inBandAuthenticate(username: String) {
val response = inBandAuthenticationUseCase.execute(username)
mutableResponseLiveData.postValue(response)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Nevis Mobile Authentication SDK Example App
*
* Copyright © 2022. Nevis Security AG. All rights reserved.
* Copyright © 2022-2024. Nevis Security AG. All rights reserved.
*/

package ch.nevis.exampleapp.coroutines.ui.selectAccount.parameter
Expand Down Expand Up @@ -31,5 +31,10 @@ data class SelectAccountNavigationParameter(
* The list of available accounts the user can select from.
*/
@IgnoredOnParcel
val accounts: Set<Account>? = null
val accounts: Set<Account>? = null,

/**
* The message to confirm if there is any.
*/
val message: String? = null
) : Parcelable
Loading

0 comments on commit b3519f2

Please sign in to comment.