[PM-25662] Add CredentialExchangeCompletionManager (#5867)

This commit is contained in:
Patrick Honkonen 2025-09-15 14:36:48 -04:00 committed by GitHub
parent 1dc6ea2227
commit 481a8c8fbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 230 additions and 0 deletions

View File

@ -0,0 +1,16 @@
package com.bitwarden.cxf.manager
import com.bitwarden.cxf.manager.model.ExportCredentialsResult
/**
* A manager for completing the Credential Exchange processes.
*/
interface CredentialExchangeCompletionManager {
/**
* Complete the Credential Exchange export process with the provided [exportResult].
*
* @param exportResult The result of the export operation.
*/
fun completeCredentialExport(exportResult: ExportCredentialsResult)
}

View File

@ -0,0 +1,41 @@
package com.bitwarden.cxf.manager
import android.app.Activity
import android.content.Intent
import androidx.credentials.providerevents.playservices.IntentHandler
import androidx.credentials.providerevents.transfer.ImportCredentialsResponse
import com.bitwarden.cxf.manager.model.ExportCredentialsResult
/**
* Primary implementation of [CredentialExchangeCompletionManager].
*/
internal class CredentialExchangeCompletionManagerImpl(
private val activity: Activity,
) : CredentialExchangeCompletionManager {
override fun completeCredentialExport(exportResult: ExportCredentialsResult) {
val intent = Intent()
when (exportResult) {
is ExportCredentialsResult.Failure -> {
IntentHandler.setImportCredentialsException(
intent = intent,
exception = exportResult.error,
)
}
is ExportCredentialsResult.Success -> {
IntentHandler.setImportCredentialsResponse(
context = activity,
uri = exportResult.uri,
response = ImportCredentialsResponse(
responseJson = exportResult.payload,
),
)
}
}
activity.apply {
setResult(Activity.RESULT_OK, intent)
finish()
}
}
}

View File

@ -0,0 +1,54 @@
@file:OmitFromCoverage
package com.bitwarden.cxf.manager.dsl
import android.app.Activity
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManagerImpl
/**
* A DSL for building a [CredentialExchangeCompletionManager].
*
* This class provides a structured way to configure and create an instance of
* [CredentialExchangeCompletionManager], which is used to finalize the credential
* exchange process by returning a result to the calling application. It is primarily
* used within the [credentialExchangeCompletionManager] builder function.
*
*/
@OmitFromCoverage
class CredentialExchangeCompletionManagerBuilder internal constructor() {
internal fun build(activity: Activity): CredentialExchangeCompletionManager =
CredentialExchangeCompletionManagerImpl(activity = activity)
}
/**
* Creates an instance of [CredentialExchangeCompletionManager] using a DSL-style builder.
*
* This function is the entry point for handling the completion of a credential exchange flow,
* such as after a user has successfully created or selected a passkey.
*
* Example usage:
* ```
* val completionManager = credentialExchangeCompletionManager(activity) {
* // Configuration options can be added here if the DSL is extended in the future.
* }
*
* // Use the completionManager to finish the credential exchange.
* completionManager.completeCredentialExport(...)
* ```
*
* @param activity The [Activity] that initiated the credential exchange operation. This is
* used to send back the result to the calling application (e.g., the browser).
* @param config A lambda with [CredentialExchangeCompletionManagerBuilder] as its receiver,
* allowing for declarative configuration of the manager.
*
* @return A configured [CredentialExchangeCompletionManager] instance.
*/
fun credentialExchangeCompletionManager(
activity: Activity,
config: CredentialExchangeCompletionManagerBuilder.() -> Unit = {},
): CredentialExchangeCompletionManager =
CredentialExchangeCompletionManagerBuilder()
.apply(config)
.build(activity = activity)

View File

@ -0,0 +1,20 @@
package com.bitwarden.cxf.manager.model
import android.net.Uri
import androidx.credentials.providerevents.exception.ImportCredentialsException
/**
* Represents the result of exporting credentials.
*/
sealed class ExportCredentialsResult {
/**
* Represents a successful export.
*/
data class Success(val payload: String, val uri: Uri) : ExportCredentialsResult()
/**
* Represents a failure to export.
*/
data class Failure(val error: ImportCredentialsException) : ExportCredentialsResult()
}

View File

@ -0,0 +1,17 @@
@file:OmitFromCoverage
package com.bitwarden.cxf.ui.composition
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
/**
* Provides access to the Credential Exchange completion manager throughout the app.
*/
@Suppress("MaxLineLength")
val LocalCredentialExchangeCompletionManager: ProvidableCompositionLocal<CredentialExchangeCompletionManager> =
compositionLocalOf {
error("CompositionLocal LocalPermissionsManager not present")
}

View File

@ -0,0 +1,82 @@
package com.bitwarden.cxf.manager
import android.app.Activity
import android.net.Uri
import androidx.credentials.providerevents.exception.ImportCredentialsException
import androidx.credentials.providerevents.playservices.IntentHandler
import com.bitwarden.cxf.manager.model.ExportCredentialsResult
import io.mockk.Ordering
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.runs
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class CredentialExchangeCompletionManagerTest {
private val mockActivity = mockk<Activity>()
private val completionManager = CredentialExchangeCompletionManagerImpl(mockActivity)
@BeforeEach
fun setUp() {
mockkObject(IntentHandler)
every {
IntentHandler.setImportCredentialsResponse(
context = any(),
uri = any(),
response = any(),
)
} just runs
every {
IntentHandler.setImportCredentialsException(
intent = any(),
exception = any(),
)
} just runs
}
@Test
fun `completeCredentialExport sets Success result and finishes the activity`() {
val mockUri = mockk<Uri>()
val exportResult = ExportCredentialsResult.Success("payload", mockUri)
every { mockActivity.setResult(Activity.RESULT_OK, any()) } just runs
every { mockActivity.finish() } just runs
completionManager.completeCredentialExport(exportResult)
verify(ordering = Ordering.ORDERED) {
IntentHandler.setImportCredentialsResponse(
context = mockActivity,
uri = mockUri,
response = any(),
)
mockActivity.setResult(Activity.RESULT_OK, any())
mockActivity.finish()
}
}
@Test
fun `completeCredentialExport sets Failure result and finishes the activity`() {
val importException = mockk<ImportCredentialsException>()
val exportResult = ExportCredentialsResult.Failure(error = importException)
every { mockActivity.setResult(Activity.RESULT_OK, any()) } just runs
every { mockActivity.finish() } just runs
completionManager.completeCredentialExport(exportResult)
verify(ordering = Ordering.ORDERED) {
IntentHandler.setImportCredentialsException(
intent = any(),
exception = importException,
)
mockActivity.setResult(Activity.RESULT_OK, any())
mockActivity.finish()
}
}
}