From 948beac19a9e757f6e62ddccf788a398b80da238 Mon Sep 17 00:00:00 2001 From: David Perez Date: Thu, 20 Aug 2026 17:00:24 -0500 Subject: [PATCH] PM-31118: feat: Add support for SDK pin unlock --- .../vault/repository/VaultRepositoryImpl.kt | 52 ++-- .../repository/di/VaultRepositoryModule.kt | 3 + .../vault/repository/VaultRepositoryTest.kt | 252 +++++++++++++++--- .../core/data/manager/model/FlagKey.kt | 9 + .../core/data/manager/model/FlagKeyTest.kt | 5 + .../components/debug/FeatureFlagListItems.kt | 2 + .../main/res/values/strings_non_localized.xml | 1 + 7 files changed, 266 insertions(+), 58 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 4371e8c75cc..89d52c35481 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.repository import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.data.manager.dispatcher.DispatcherManager +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.error.MissingPropertyException import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow @@ -22,6 +23,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.autofill.util.login import com.x8bit.bitwarden.data.platform.error.NoActiveUserException +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.util.isActive import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource @@ -70,7 +72,7 @@ import javax.crypto.Cipher * Default implementation of [VaultRepository]. */ @Suppress("TooManyFunctions", "LongParameterList") -class VaultRepositoryImpl( +internal class VaultRepositoryImpl( private val vaultDiskSource: VaultDiskSource, private val vaultSdkSource: VaultSdkSource, private val authDiskSource: AuthDiskSource, @@ -82,6 +84,7 @@ class VaultRepositoryImpl( private val vaultSyncManager: VaultSyncManager, private val credentialExchangeImportManager: CredentialExchangeImportManager, private val pinProtectedUserKeyManager: PinProtectedUserKeyManager, + private val featureFlagManager: FeatureFlagManager, dispatcherManager: DispatcherManager, ) : VaultRepository, CipherManager by cipherManager, @@ -371,28 +374,16 @@ class VaultRepositoryImpl( override suspend fun unlockVaultWithPin( pin: String, ): VaultUnlockResult { - val userId = activeUserId - ?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException()) + val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError( + error = NoActiveUserException(), + ) - return authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId) - ?.let { pinProtectedUserKeyEnvelope -> - this.unlockVaultForUser( - userId = userId, - initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope( - pin = pin, - pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope, - ), - ) - } - ?: run { + return authDiskSource + .getPinProtectedUserKey(userId = userId) + ?.let { pinProtectedUserKey -> // This is needed to support unlocking with a legacy pin protected user key. // Once the vault is unlocked, the user's pin protected user key is migrated to // a pin protected user key envelope. - val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId) - ?: return VaultUnlockResult.InvalidStateError( - error = MissingPropertyException("Pin protected key"), - ) - this.unlockVaultForUser( userId = userId, initUserCryptoMethod = InitUserCryptoMethod.Pin( @@ -401,6 +392,29 @@ class VaultRepositoryImpl( ), ) } + ?: run { + if (featureFlagManager.getFeatureFlag(key = FlagKey.SdkPinUnlock)) { + this.unlockVaultForUser( + userId = userId, + initUserCryptoMethod = InitUserCryptoMethod.PinState(pin = pin), + ) + } else { + authDiskSource + .getPinProtectedUserKeyEnvelope(userId = userId) + ?.let { pinProtectedUserKeyEnvelope -> + this.unlockVaultForUser( + userId = userId, + initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope( + pin = pin, + pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope, + ), + ) + } + ?: VaultUnlockResult.InvalidStateError( + error = MissingPropertyException("Pin protected key envelope"), + ) + } + } } override suspend fun generateTotp( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt index 245f111d622..464d37cf9e4 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.repository.di import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.manager.CipherManager @@ -42,6 +43,7 @@ object VaultRepositoryModule { vaultSyncManager: VaultSyncManager, credentialExchangeImportManager: CredentialExchangeImportManager, pinProtectedUserKeyManager: PinProtectedUserKeyManager, + featureFlagManager: FeatureFlagManager, ): VaultRepository = VaultRepositoryImpl( vaultDiskSource = vaultDiskSource, vaultSdkSource = vaultSdkSource, @@ -55,5 +57,6 @@ object VaultRepositoryModule { vaultSyncManager = vaultSyncManager, credentialExchangeImportManager = credentialExchangeImportManager, pinProtectedUserKeyManager = pinProtectedUserKeyManager, + featureFlagManager = featureFlagManager, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 7956627c673..023259fdef5 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -6,6 +6,7 @@ import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.MasterPasswordUnlockData import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.error.MissingPropertyException import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.util.asFailure @@ -35,6 +36,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfRequestModel import com.x8bit.bitwarden.data.auth.repository.model.createMockWrappedAccountCryptographicState import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.platform.error.NoActiveUserException +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAccount @@ -118,8 +120,11 @@ class VaultRepositoryTest { private val pinProtectedUserKeyManager: PinProtectedUserKeyManager = mockk { coEvery { deriveTemporaryPinProtectedUserKeyIfNecessary(userId = any()) } just runs } + private val featureFlagManager: FeatureFlagManager = mockk { + every { getFeatureFlag(FlagKey.SdkPinUnlock) } returns true + } - private val vaultRepository = VaultRepositoryImpl( + private val vaultRepository: VaultRepository = VaultRepositoryImpl( vaultDiskSource = vaultDiskSource, vaultSdkSource = vaultSdkSource, authDiskSource = fakeAuthDiskSource, @@ -132,6 +137,7 @@ class VaultRepositoryTest { vaultSyncManager = vaultSyncManager, credentialExchangeImportManager = credentialExchangeImportManager, pinProtectedUserKeyManager = pinProtectedUserKeyManager, + featureFlagManager = featureFlagManager, ) @BeforeEach @@ -669,8 +675,9 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `unlockVaultWithPin with missing pin-protected user key should return InvalidStateError`() = + fun `unlockVaultWithPin with SdkPinUnlock disabled and no pin keys stored should return InvalidStateError`() = runTest { + every { featureFlagManager.getFeatureFlag(FlagKey.SdkPinUnlock) } returns false fakeAuthDiskSource.storePinProtectedUserKey( userId = "mockId-1", pinProtectedUserKey = null, @@ -727,11 +734,15 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `unlockVaultWithPin with VaultLockManager Success should unlock for the current user and return Success`() = + fun `unlockVaultWithPin with a pin protected user key and SdkPinUnlock enabled should unlock with Pin and return Success`() = runTest { val userId = "mockId-1" val mockVaultUnlockResult = VaultUnlockResult.Success - prepareStateForUnlocking(unlockResult = mockVaultUnlockResult) + prepareStateForUnlocking( + unlockResult = mockVaultUnlockResult, + persistentPinProtectedUserKeyEnvelope = null, + ephemeralPinProtectedUserKeyEnvelope = null, + ) val result = vaultRepository.unlockVaultWithPin(pin = "1234") @@ -739,15 +750,15 @@ class VaultRepositoryTest { mockVaultUnlockResult, result, ) - coVerify { + coVerify(exactly = 1) { vaultLockManager.unlockVault( accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, userId = userId, email = "email", kdf = MOCK_PROFILE.toSdkParams(), - initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope( + initUserCryptoMethod = InitUserCryptoMethod.Pin( pin = "1234", - pinProtectedUserKeyEnvelope = "mockKey-1", + pinProtectedUserKey = "mockKey-1", ), organizationKeys = createMockOrganizationKeys(number = 1), ) @@ -756,27 +767,24 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `unlockVaultWithPin with PinProtectedUserKeyEnvelope null and VaultLockManager Success should unlock with pin for the current user and return Success`() = + fun `unlockVaultWithPin with a pin protected user key and SdkPinUnlock disabled should unlock with Pin and return Success`() = runTest { val userId = "mockId-1" val mockVaultUnlockResult = VaultUnlockResult.Success - prepareStateForUnlocking(unlockResult = mockVaultUnlockResult) - - fakeAuthDiskSource.storeEphemeralPinProtectedUserKeyEnvelope( - userId = userId, - pinProtectedUserKeyEnvelope = null, - ) - fakeAuthDiskSource.storePersistentPinProtectedUserKeyEnvelope( - userId = userId, - pinProtectedUserKeyEnvelope = null, + every { featureFlagManager.getFeatureFlag(FlagKey.SdkPinUnlock) } returns false + prepareStateForUnlocking( + unlockResult = mockVaultUnlockResult, + persistentPinProtectedUserKeyEnvelope = null, + ephemeralPinProtectedUserKeyEnvelope = null, ) val result = vaultRepository.unlockVaultWithPin(pin = "1234") + assertEquals( mockVaultUnlockResult, result, ) - coVerify { + coVerify(exactly = 1) { vaultLockManager.unlockVault( accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, userId = userId, @@ -793,11 +801,105 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `unlockVaultWithPin with VaultLockManager non-Success should unlock for the current user and return the error`() = + fun `unlockVaultWithPin with SdkPinUnlock enabled and no pin protected user key should unlock with PinState and return Success`() = + runTest { + val userId = "mockId-1" + val mockVaultUnlockResult = VaultUnlockResult.Success + prepareStateForUnlocking( + unlockResult = mockVaultUnlockResult, + pinProtectedUserKey = null, + ) + + val result = vaultRepository.unlockVaultWithPin(pin = "1234") + + assertEquals( + mockVaultUnlockResult, + result, + ) + coVerify(exactly = 1) { + vaultLockManager.unlockVault( + accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, + userId = userId, + email = "email", + kdf = MOCK_PROFILE.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.PinState(pin = "1234"), + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVaultWithPin with SdkPinUnlock enabled and no pin keys stored should unlock with PinState and return Success`() = + runTest { + val userId = "mockId-1" + val mockVaultUnlockResult = VaultUnlockResult.Success + prepareStateForUnlocking( + unlockResult = mockVaultUnlockResult, + pinProtectedUserKey = null, + persistentPinProtectedUserKeyEnvelope = null, + ephemeralPinProtectedUserKeyEnvelope = null, + ) + + val result = vaultRepository.unlockVaultWithPin(pin = "1234") + + assertEquals( + mockVaultUnlockResult, + result, + ) + coVerify(exactly = 1) { + vaultLockManager.unlockVault( + accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, + userId = userId, + email = "email", + kdf = MOCK_PROFILE.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.PinState(pin = "1234"), + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVaultWithPin with SdkPinUnlock enabled and VaultLockManager non-Success should return the error`() = runTest { val userId = "mockId-1" val mockVaultUnlockResult = VaultUnlockResult.InvalidStateError(error = null) + prepareStateForUnlocking( + unlockResult = mockVaultUnlockResult, + pinProtectedUserKey = null, + ) + + val result = vaultRepository.unlockVaultWithPin(pin = "1234") + + assertEquals( + mockVaultUnlockResult, + result, + ) + coVerify(exactly = 1) { + vaultLockManager.unlockVault( + accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, + userId = userId, + email = "email", + kdf = MOCK_PROFILE.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.PinState(pin = "1234"), + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVaultWithPin with SdkPinUnlock disabled and a pin protected user key envelope should unlock with PinEnvelope and return Success`() = + runTest { + val userId = "mockId-1" + val mockVaultUnlockResult = VaultUnlockResult.Success + every { featureFlagManager.getFeatureFlag(FlagKey.SdkPinUnlock) } returns false prepareStateForUnlocking(unlockResult = mockVaultUnlockResult) + fakeAuthDiskSource.storePinProtectedUserKey( + userId = userId, + pinProtectedUserKey = null, + ) val result = vaultRepository.unlockVaultWithPin(pin = "1234") @@ -805,7 +907,40 @@ class VaultRepositoryTest { mockVaultUnlockResult, result, ) - coVerify { + coVerify(exactly = 1) { + vaultLockManager.unlockVault( + accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, + userId = userId, + email = "email", + kdf = MOCK_PROFILE.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope( + pin = "1234", + pinProtectedUserKeyEnvelope = "mockKey-1", + ), + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVaultWithPin with SdkPinUnlock disabled and VaultLockManager non-Success should return the error`() = + runTest { + val userId = "mockId-1" + val mockVaultUnlockResult = VaultUnlockResult.InvalidStateError(error = null) + every { featureFlagManager.getFeatureFlag(FlagKey.SdkPinUnlock) } returns false + prepareStateForUnlocking( + unlockResult = mockVaultUnlockResult, + pinProtectedUserKey = null, + ) + + val result = vaultRepository.unlockVaultWithPin(pin = "1234") + + assertEquals( + mockVaultUnlockResult, + result, + ) + coVerify(exactly = 1) { vaultLockManager.unlockVault( accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, userId = userId, @@ -1574,10 +1709,14 @@ class VaultRepositoryTest { /** * Prepares for an unlock call with the given [unlockResult]. */ + @Suppress("LongParameterList") private fun prepareStateForUnlocking( unlockResult: VaultUnlockResult, mockMasterPassword: String = "mockPassword-1", mockPin: String = "1234", + pinProtectedUserKey: String? = "mockKey-1", + persistentPinProtectedUserKeyEnvelope: String? = "mockKey-1", + ephemeralPinProtectedUserKeyEnvelope: String? = "mockKey-1", ) { val userId = "mockId-1" coEvery { @@ -1595,11 +1734,15 @@ class VaultRepositoryTest { ) fakeAuthDiskSource.storePinProtectedUserKey( userId = userId, - pinProtectedUserKey = "mockKey-1", + pinProtectedUserKey = pinProtectedUserKey, + ) + fakeAuthDiskSource.storeEphemeralPinProtectedUserKeyEnvelope( + userId = userId, + pinProtectedUserKeyEnvelope = ephemeralPinProtectedUserKeyEnvelope, ) fakeAuthDiskSource.storePersistentPinProtectedUserKeyEnvelope( userId = userId, - pinProtectedUserKeyEnvelope = "mockKey-1", + pinProtectedUserKeyEnvelope = persistentPinProtectedUserKeyEnvelope, ) fakeAuthDiskSource.storeOrganizationKeys( userId = userId, @@ -1627,31 +1770,62 @@ class VaultRepositoryTest { } returns unlockResult // PIN unlock - coEvery { - vaultLockManager.unlockVault( - accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, - userId = userId, - email = "email", - kdf = MOCK_PROFILE.toSdkParams(), - initUserCryptoMethod = InitUserCryptoMethod.Pin( - pin = mockPin, - pinProtectedUserKey = "mockKey-1", - ), - organizationKeys = createMockOrganizationKeys(number = 1), - ) - } returns unlockResult + pinProtectedUserKey?.let { + coEvery { + vaultLockManager.unlockVault( + accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, + userId = userId, + email = "email", + kdf = MOCK_PROFILE.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.Pin( + pin = mockPin, + pinProtectedUserKey = it, + ), + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } returns unlockResult + } // PIN ENVELOPE unlock + persistentPinProtectedUserKeyEnvelope?.let { + coEvery { + vaultLockManager.unlockVault( + accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, + userId = userId, + email = "email", + kdf = MOCK_PROFILE.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope( + pin = mockPin, + pinProtectedUserKeyEnvelope = it, + ), + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } returns unlockResult + } + ephemeralPinProtectedUserKeyEnvelope?.let { + coEvery { + vaultLockManager.unlockVault( + accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, + userId = userId, + email = "email", + kdf = MOCK_PROFILE.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope( + pin = mockPin, + pinProtectedUserKeyEnvelope = it, + ), + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } returns unlockResult + } + + // PIN STATE unlock coEvery { vaultLockManager.unlockVault( accountCryptographicState = MOCK_ACCOUNT_CRYPTOGRAPHIC_STATE, userId = userId, email = "email", kdf = MOCK_PROFILE.toSdkParams(), - initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope( - pin = mockPin, - pinProtectedUserKeyEnvelope = "mockKey-1", - ), + initUserCryptoMethod = InitUserCryptoMethod.PinState(pin = mockPin), organizationKeys = createMockOrganizationKeys(number = 1), ) } returns unlockResult diff --git a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt index daa306cf012..80ecb19ba3d 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt @@ -50,6 +50,7 @@ sealed class FlagKey { SendControls, SendControlsExistingSends, IdentityAutofill, + SdkPinUnlock, ) } } @@ -228,6 +229,14 @@ sealed class FlagKey { override val defaultValue: Boolean = false } + /** + * Data object holding the feature flag key for gating PIN unlock through SDK. + */ + data object SdkPinUnlock : FlagKey() { + override val keyName: String = "pm-31059-sdk-pin-unlock" + override val defaultValue: Boolean = false + } + //region Dummy keys for testing /** * Data object holding the key for a [Boolean] flag to be used in tests. diff --git a/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt b/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt index 70d083d2ab5..7b7ea30f881 100644 --- a/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt +++ b/core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt @@ -80,6 +80,10 @@ class FlagKeyTest { FlagKey.IdentityAutofill.keyName, "pm-38138-mobile-identity-autofill", ) + assertEquals( + FlagKey.SdkPinUnlock.keyName, + "pm-31059-sdk-pin-unlock", + ) } @Test @@ -104,6 +108,7 @@ class FlagKeyTest { FlagKey.SendControls, FlagKey.SendControlsExistingSends, FlagKey.IdentityAutofill, + FlagKey.SdkPinUnlock, ).all { !it.defaultValue }, diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt index 9fe03183810..15b3a73ba39 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt @@ -44,6 +44,7 @@ fun FlagKey.ListItemContent( FlagKey.SendControls, FlagKey.SendControlsExistingSends, FlagKey.IdentityAutofill, + FlagKey.SdkPinUnlock, -> { @Suppress("UNCHECKED_CAST") BooleanFlagItem( @@ -115,4 +116,5 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { } FlagKey.IdentityAutofill -> stringResource(BitwardenString.identity_autofill) + FlagKey.SdkPinUnlock -> stringResource(BitwardenString.sdk_pin_unlock) } diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index a909e635633..3c46bf31ea8 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -61,6 +61,7 @@ Send Controls Send Controls - Existing Sends Identity Autofill + SDK PIN Unlock