mirror of
https://github.com/bitwarden/android.git
synced 2026-06-10 00:28:29 -05:00
Compare commits
11 Commits
release/20
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbc73fdd5c | ||
|
|
43f70f5b3b | ||
|
|
15689fcace | ||
|
|
3e928489c7 | ||
|
|
d3590935b0 | ||
|
|
ee4b9823d1 | ||
|
|
a3bcff9463 | ||
|
|
aca9949874 | ||
|
|
217bfc1097 | ||
|
|
a94978c8e2 | ||
|
|
0a920d5800 |
@@ -36,6 +36,8 @@ import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
|
||||
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.accessibilityDisclosureDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.navigateToAccessibilityDisclosure
|
||||
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.cookieAcquisitionDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.navigateToCookieAcquisition
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
|
||||
@@ -110,6 +112,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
var shouldShowSplashScreen = true
|
||||
@@ -166,6 +169,10 @@ class MainActivity : AppCompatActivity() {
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
accessibilityDisclosureDestination(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,6 +261,10 @@ class MainActivity : AppCompatActivity() {
|
||||
navController.navigateToLocalNetworkAccess()
|
||||
}
|
||||
|
||||
MainEvent.NavigateToAccessibilityDisclosure -> {
|
||||
navController.navigateToAccessibilityDisclosure()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
@@ -151,6 +152,12 @@ class MainViewModel @Inject constructor(
|
||||
.onEach(::trySendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.hasShownAccessibilityDisclaimerFlow
|
||||
.map { MainAction.Internal.HasShownAccessibilityDisclaimerUpdate(it) }
|
||||
.onEach(::trySendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
merge(
|
||||
authRepository
|
||||
.userStateFlow
|
||||
@@ -235,6 +242,17 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.CookieAcquisitionReady -> handleCookieAcquisitionReady()
|
||||
is MainAction.Internal.LocalNetworkAccessRequired -> handleLocalNetworkAccessRequired()
|
||||
is MainAction.Internal.ResizeHasBeenRequested -> handleResizeHasBeenRequested()
|
||||
is MainAction.Internal.HasShownAccessibilityDisclaimerUpdate -> {
|
||||
handleHasShownAccessibilityDisclaimerUpdate(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHasShownAccessibilityDisclaimerUpdate(
|
||||
action: MainAction.Internal.HasShownAccessibilityDisclaimerUpdate,
|
||||
) {
|
||||
if (!action.hasBeenShown) {
|
||||
sendEvent(MainEvent.NavigateToAccessibilityDisclosure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,6 +710,11 @@ sealed class MainAction {
|
||||
* Indicates that resize has been requested on the Activity
|
||||
*/
|
||||
data object ResizeHasBeenRequested : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the accessibility disclaimer has been displayed.
|
||||
*/
|
||||
data class HasShownAccessibilityDisclaimerUpdate(val hasBeenShown: Boolean) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,6 +754,11 @@ sealed class MainEvent {
|
||||
*/
|
||||
data object NavigateToLocalNetworkAccess : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the accessibility disclosure screen.
|
||||
*/
|
||||
data object NavigateToAccessibilityDisclosure : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app language has been updated.
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.policies.OrganizationUserPolicyContext
|
||||
@@ -24,6 +25,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNul
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
|
||||
import com.x8bit.bitwarden.data.platform.datasource.sdk.BaseSdkSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a
|
||||
@@ -31,6 +33,7 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class AuthSdkSourceImpl(
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
sdkClientManager: SdkClientManager,
|
||||
) : BaseSdkSource(sdkClientManager = sdkClientManager),
|
||||
AuthSdkSource {
|
||||
@@ -45,10 +48,8 @@ class AuthSdkSourceImpl(
|
||||
masterPasswordHint: String?,
|
||||
shouldResetPasswordEnroll: Boolean,
|
||||
): Result<JitMasterPasswordRegistrationResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.auth()
|
||||
.registration()
|
||||
.postKeysForJitPasswordRegistration(
|
||||
withContext(context = dispatcherManager.io) {
|
||||
getClient(userId = userId).auth().registration().postKeysForJitPasswordRegistration(
|
||||
request = JitMasterPasswordRegistrationRequest(
|
||||
orgId = organizationId,
|
||||
orgPublicKey = organizationPublicKey,
|
||||
@@ -60,6 +61,7 @@ class AuthSdkSourceImpl(
|
||||
resetPasswordEnroll = shouldResetPasswordEnroll,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun postKeysForKeyConnectorRegistration(
|
||||
@@ -68,11 +70,13 @@ class AuthSdkSourceImpl(
|
||||
keyConnectorUrl: String,
|
||||
ssoOrganizationIdentifier: String,
|
||||
): Result<KeyConnectorRegistrationResult> = runCatchingWithLogs {
|
||||
useClient(userId = userId, accessToken = accessToken) {
|
||||
auth().registration().postKeysForKeyConnectorRegistration(
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
ssoOrgIdentifier = ssoOrganizationIdentifier,
|
||||
)
|
||||
withContext(context = dispatcherManager.io) {
|
||||
useClient(userId = userId, accessToken = accessToken) {
|
||||
auth().registration().postKeysForKeyConnectorRegistration(
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
ssoOrgIdentifier = ssoOrganizationIdentifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +87,8 @@ class AuthSdkSourceImpl(
|
||||
deviceIdentifier: String,
|
||||
shouldTrustDevice: Boolean,
|
||||
): Result<TdeRegistrationResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.auth()
|
||||
.registration()
|
||||
.postKeysForTdeRegistration(
|
||||
withContext(context = dispatcherManager.io) {
|
||||
getClient(userId = userId).auth().registration().postKeysForTdeRegistration(
|
||||
request = TdeRegistrationRequest(
|
||||
orgId = organizationId,
|
||||
orgPublicKey = organizationPublicKey,
|
||||
@@ -95,6 +97,7 @@ class AuthSdkSourceImpl(
|
||||
trustDevice = shouldTrustDevice,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun postKeysForUserPasswordRegistration(
|
||||
@@ -104,23 +107,25 @@ class AuthSdkSourceImpl(
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String,
|
||||
): Result<UserMasterPasswordRegistrationResponse> = runCatchingWithLogs {
|
||||
useClient {
|
||||
auth().registration().postKeysForUserPasswordRegistration(
|
||||
request = UserMasterPasswordRegistrationRequest(
|
||||
email = email,
|
||||
salt = salt,
|
||||
masterPassword = masterPassword,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
organizationUserId = null,
|
||||
orgInviteToken = null,
|
||||
orgSponsoredFreeFamilyPlanToken = null,
|
||||
acceptEmergencyAccessInviteToken = null,
|
||||
acceptEmergencyAccessId = null,
|
||||
providerInviteToken = null,
|
||||
providerUserId = null,
|
||||
),
|
||||
)
|
||||
withContext(context = dispatcherManager.io) {
|
||||
useClient {
|
||||
auth().registration().postKeysForUserPasswordRegistration(
|
||||
request = UserMasterPasswordRegistrationRequest(
|
||||
email = email,
|
||||
salt = salt,
|
||||
masterPassword = masterPassword,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
organizationUserId = null,
|
||||
orgInviteToken = null,
|
||||
orgSponsoredFreeFamilyPlanToken = null,
|
||||
acceptEmergencyAccessInviteToken = null,
|
||||
acceptEmergencyAccessId = null,
|
||||
providerInviteToken = null,
|
||||
providerUserId = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk.di
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
@@ -19,8 +20,10 @@ object AuthSdkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthSdkSource(
|
||||
dispatcherManager: DispatcherManager,
|
||||
sdkClientManager: SdkClientManager,
|
||||
): AuthSdkSource = AuthSdkSourceImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
sdkClientManager = sdkClientManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.WrappedAccountCryptographicState
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.crypto.Kdf
|
||||
@@ -17,7 +16,6 @@ import com.x8bit.bitwarden.data.auth.repository.util.toAccountCryptographicState
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.DeriveKeyConnectorResult
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* The default implementation of the [KeyConnectorManager].
|
||||
@@ -27,7 +25,6 @@ class KeyConnectorManagerImpl(
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
) : KeyConnectorManager {
|
||||
override suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
@@ -97,26 +94,24 @@ class KeyConnectorManagerImpl(
|
||||
organizationIdentifier: String,
|
||||
): Result<MigrateNewUserToKeyConnectorResult> =
|
||||
if (featureFlagManager.getFeatureFlag(FlagKey.V2EncryptionKeyConnector)) {
|
||||
withContext(dispatcherManager.io) {
|
||||
authSdkSource
|
||||
.postKeysForKeyConnectorRegistration(
|
||||
userId = userId,
|
||||
accessToken = accessToken,
|
||||
keyConnectorUrl = url,
|
||||
ssoOrganizationIdentifier = organizationIdentifier,
|
||||
authSdkSource
|
||||
.postKeysForKeyConnectorRegistration(
|
||||
userId = userId,
|
||||
accessToken = accessToken,
|
||||
keyConnectorUrl = url,
|
||||
ssoOrganizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
.map {
|
||||
MigrateNewUserToKeyConnectorResult(
|
||||
masterKey = it.keyConnectorKey,
|
||||
encryptedUserKey = it.keyConnectorKeyWrappedUserKey,
|
||||
privateKey = when (val state = it.accountCryptographicState) {
|
||||
is WrappedAccountCryptographicState.V1 -> state.privateKey
|
||||
is WrappedAccountCryptographicState.V2 -> state.privateKey
|
||||
},
|
||||
accountCryptographicState = it.accountCryptographicState,
|
||||
)
|
||||
.map {
|
||||
MigrateNewUserToKeyConnectorResult(
|
||||
masterKey = it.keyConnectorKey,
|
||||
encryptedUserKey = it.keyConnectorKeyWrappedUserKey,
|
||||
privateKey = when (val state = it.accountCryptographicState) {
|
||||
is WrappedAccountCryptographicState.V1 -> state.privateKey
|
||||
is WrappedAccountCryptographicState.V2 -> state.privateKey
|
||||
},
|
||||
accountCryptographicState = it.accountCryptographicState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
legacyMigrateNewUserToKeyConnector(
|
||||
accountKeys = accountKeys,
|
||||
|
||||
@@ -90,14 +90,12 @@ object AuthManagerModule {
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): KeyConnectorManager =
|
||||
KeyConnectorManagerImpl(
|
||||
accountsService = accountsService,
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
featureFlagManager = featureFlagManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -153,7 +153,6 @@ import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
@@ -190,7 +189,7 @@ class AuthRepositoryImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
logsManager: LogsManager,
|
||||
pushManager: PushManager,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager,
|
||||
BiometricsEncryptionManager by biometricsEncryptionManager,
|
||||
@@ -565,15 +564,14 @@ class AuthRepositoryImpl(
|
||||
): Result<VaultUnlockResult> {
|
||||
val userId = profile.userId
|
||||
val shouldTrustDevice = authDiskSource.getShouldTrustDevice(userId = userId) == true
|
||||
return withContext(dispatcherManager.io) {
|
||||
authSdkSource.postKeysForTdeRegistration(
|
||||
return authSdkSource
|
||||
.postKeysForTdeRegistration(
|
||||
userId = userId,
|
||||
organizationId = orgAutoEnrollStatus.organizationId,
|
||||
organizationPublicKey = orgKeys.publicKey,
|
||||
deviceIdentifier = authDiskSource.uniqueAppId,
|
||||
shouldTrustDevice = shouldTrustDevice,
|
||||
)
|
||||
}
|
||||
.map { response ->
|
||||
// Clear the 'should trust device' flag, since the SDK trusted the device above.
|
||||
authDiskSource.storeShouldTrustDevice(userId = userId, shouldTrustDevice = null)
|
||||
@@ -976,15 +974,14 @@ class AuthRepositoryImpl(
|
||||
return RegisterResult.WeakPassword
|
||||
}
|
||||
if (featureFlagManager.getFeatureFlag(key = FlagKey.V2EncryptionPassword)) {
|
||||
return withContext(dispatcherManager.io) {
|
||||
authSdkSource.postKeysForUserPasswordRegistration(
|
||||
return authSdkSource
|
||||
.postKeysForUserPasswordRegistration(
|
||||
email = email,
|
||||
salt = email,
|
||||
masterPassword = masterPassword,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { RegisterResult.Success },
|
||||
onFailure = { RegisterResult.Error(errorMessage = null, error = it) },
|
||||
@@ -1060,7 +1057,7 @@ class AuthRepositoryImpl(
|
||||
?: return RemovePasswordResult.Error(error = MissingPropertyException("User Key"))
|
||||
val keyConnectorUrl = organizations
|
||||
.find {
|
||||
it.shouldUseKeyConnector &&
|
||||
it.isKeyConnectorEnabled &&
|
||||
it.role != OrganizationType.OWNER &&
|
||||
it.role != OrganizationType.ADMIN
|
||||
}
|
||||
@@ -1281,18 +1278,16 @@ class AuthRepositoryImpl(
|
||||
.map { orgKeys -> enrollStatus to orgKeys }
|
||||
}
|
||||
.flatMap { (enrollStatus, orgKeys) ->
|
||||
withContext(dispatcherManager.io) {
|
||||
authSdkSource.postKeysForJitPasswordRegistration(
|
||||
userId = userId,
|
||||
organizationId = enrollStatus.organizationId,
|
||||
organizationPublicKey = orgKeys.publicKey,
|
||||
organizationSsoIdentifier = organizationIdentifier,
|
||||
salt = profile.email,
|
||||
masterPassword = password,
|
||||
masterPasswordHint = passwordHint,
|
||||
shouldResetPasswordEnroll = enrollStatus.isResetPasswordEnabled,
|
||||
)
|
||||
}
|
||||
authSdkSource.postKeysForJitPasswordRegistration(
|
||||
userId = userId,
|
||||
organizationId = enrollStatus.organizationId,
|
||||
organizationPublicKey = orgKeys.publicKey,
|
||||
organizationSsoIdentifier = organizationIdentifier,
|
||||
salt = profile.email,
|
||||
masterPassword = password,
|
||||
masterPasswordHint = passwordHint,
|
||||
shouldResetPasswordEnroll = enrollStatus.isResetPasswordEnabled,
|
||||
)
|
||||
}
|
||||
.onSuccess { response ->
|
||||
authDiskSource.storeAccountKeys(
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.bitwarden.network.model.OrganizationType
|
||||
* @property name The name of the organization (if applicable).
|
||||
* @property shouldManageResetPassword Indicates that this user has the permission to manage their
|
||||
* own password.
|
||||
* @property shouldUseKeyConnector Indicates that the organization uses a key connector.
|
||||
* @property isKeyConnectorEnabled Indicates that the organization uses a key connector.
|
||||
* @property role The user's role in the organization.
|
||||
* @property keyConnectorUrl The key connector domain (if applicable).
|
||||
* @property userIsClaimedByOrganization Indicates that the user is claimed by the organization.
|
||||
@@ -20,7 +20,7 @@ data class Organization(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val shouldManageResetPassword: Boolean,
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val isKeyConnectorEnabled: Boolean,
|
||||
val role: OrganizationType,
|
||||
val keyConnectorUrl: String?,
|
||||
val userIsClaimedByOrganization: Boolean,
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.network.model.MemberDecryptionType
|
||||
import com.bitwarden.network.model.OrganizationStatusType
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.ProductTierType
|
||||
import com.bitwarden.network.model.ProviderType
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.organizations.OrganizationUserStatusType
|
||||
import com.bitwarden.organizations.OrganizationUserType
|
||||
import com.bitwarden.organizations.Permissions
|
||||
import com.bitwarden.organizations.ProfileOrganization
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.bitwarden.policies.PolicyView
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.bitwarden.organizations.MemberDecryptionType as SdkMemberDecryptionType
|
||||
import com.bitwarden.organizations.ProductTierType as SdkProductTierType
|
||||
import com.bitwarden.organizations.ProviderType as SdkProviderType
|
||||
|
||||
private val JSON = Json {
|
||||
ignoreUnknownKeys = true
|
||||
@@ -22,7 +34,7 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization? =
|
||||
Organization(
|
||||
id = this.id,
|
||||
name = it,
|
||||
shouldUseKeyConnector = this.shouldUseKeyConnector,
|
||||
isKeyConnectorEnabled = this.isKeyConnectorEnabled,
|
||||
role = this.type,
|
||||
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
|
||||
keyConnectorUrl = this.keyConnectorUrl,
|
||||
@@ -39,6 +51,86 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization? =
|
||||
fun List<SyncResponseJson.Profile.Organization>.toOrganizations(): List<Organization> =
|
||||
this.mapNotNull { it.toOrganization() }
|
||||
|
||||
/**
|
||||
* Maps the given list of [SyncResponseJson.Profile.Organization] to a list of
|
||||
* [ProfileOrganization]s.
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
fun List<SyncResponseJson.Profile.Organization>.toSdkProfileOrganizations(): List<ProfileOrganization> =
|
||||
this.mapNotNull { it.toSdkProfileOrganization() }
|
||||
|
||||
/**
|
||||
* Maps the given [SyncResponseJson.Profile.Organization] to a [ProfileOrganization] or `null` if
|
||||
* the [SyncResponseJson.Profile.Organization.name] is not present.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
private fun SyncResponseJson.Profile.Organization.toSdkProfileOrganization(): ProfileOrganization? =
|
||||
this.name?.let {
|
||||
ProfileOrganization(
|
||||
id = this.id,
|
||||
name = it,
|
||||
status = this.status.toSdkOrganizationUserStatusType(),
|
||||
type = this.type.toSdkOrganizationUserType(),
|
||||
enabled = this.isEnabled,
|
||||
usePolicies = this.shouldUsePolicies,
|
||||
useGroups = this.shouldUseGroups,
|
||||
useDirectory = this.shouldUseDirectory,
|
||||
useEvents = this.shouldUseEvents,
|
||||
useTotp = this.shouldUseTotp,
|
||||
use2fa = this.use2fa,
|
||||
useApi = this.shouldUseApi,
|
||||
useSso = this.useSso,
|
||||
useOrganizationDomains = this.useOrganizationDomains,
|
||||
useKeyConnector = this.shouldUseKeyConnector,
|
||||
useScim = this.useScim,
|
||||
useCustomPermissions = this.useCustomPermissions,
|
||||
useResetPassword = this.useResetPassword,
|
||||
useSecretsManager = this.useSecretsManager,
|
||||
usePasswordManager = this.usePasswordManager,
|
||||
useActivateAutofillPolicy = this.useActivateAutofillPolicy,
|
||||
useAutomaticUserConfirmation = this.useAutomaticUserConfirmation,
|
||||
selfHost = this.isSelfHost,
|
||||
usersGetPremium = this.shouldUsersGetPremium,
|
||||
seats = this.seats,
|
||||
maxCollections = this.maxCollections,
|
||||
maxStorageGb = this.maxStorageGb,
|
||||
ssoBound = this.ssoBound,
|
||||
identifier = this.identifier,
|
||||
permissions = this.permissions.toSdkPermissions(),
|
||||
resetPasswordEnrolled = this.resetPasswordEnrolled,
|
||||
userId = this.userId,
|
||||
organizationUserId = this.organizationUserId,
|
||||
hasPublicAndPrivateKeys = this.hasPublicAndPrivateKeys,
|
||||
providerId = this.providerId,
|
||||
providerName = this.providerName,
|
||||
providerType = this.providerType?.toSdkProviderType(),
|
||||
isProviderUser = this.isProviderUser,
|
||||
isMember = this.isMember,
|
||||
familySponsorshipFriendlyName = this.familySponsorshipFriendlyName,
|
||||
familySponsorshipAvailable = this.familySponsorshipAvailable,
|
||||
productTierType = this.productTierType.toSdkProductTierType(),
|
||||
keyConnectorEnabled = this.isKeyConnectorEnabled,
|
||||
keyConnectorUrl = this.keyConnectorUrl,
|
||||
familySponsorshipLastSyncDate = this.familySponsorshipLastSyncDate,
|
||||
familySponsorshipValidUntil = this.familySponsorshipValidUntil,
|
||||
familySponsorshipToDelete = this.familySponsorshipToDelete,
|
||||
accessSecretsManager = this.accessSecretsManager,
|
||||
limitCollectionCreation = this.limitCollectionCreation,
|
||||
limitCollectionDeletion = this.limitCollectionDeletion,
|
||||
limitItemDeletion = this.limitItemDeletion,
|
||||
allowAdminAccessToAllCollectionItems = this.allowAdminAccessToAllCollectionItems,
|
||||
userIsManagedByOrganization = this.userIsClaimedByOrganization,
|
||||
useAccessIntelligence = this.useAccessIntelligence,
|
||||
useAdminSponsoredFamilies = this.useAdminSponsoredFamilies,
|
||||
useDisableSmAdsForUsers = this.useDisableSmAdsForUsers,
|
||||
isAdminInitiated = this.isAdminInitiated,
|
||||
ssoEnabled = this.ssoEnabled,
|
||||
ssoMemberDecryptionType = this.ssoMemberDecryptionType?.toSdkMemberDecryptionType(),
|
||||
usePhishingBlocker = this.usePhishingBlocker,
|
||||
useMyItems = this.useMyItems,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the JSON data of the [PolicyView] object into [PolicyInformation] data.
|
||||
*/
|
||||
@@ -64,3 +156,60 @@ val PolicyView.policyInformation: PolicyInformation?
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun SyncResponseJson.Profile.Permissions.toSdkPermissions(): Permissions =
|
||||
Permissions(
|
||||
accessEventLogs = this.accessEventLogs,
|
||||
accessImportExport = this.accessImportExport,
|
||||
accessReports = this.accessReports,
|
||||
createNewCollections = this.createNewCollections,
|
||||
editAnyCollection = this.editAnyCollection,
|
||||
deleteAnyCollection = this.deleteAnyCollection,
|
||||
manageGroups = this.manageGroups,
|
||||
manageSso = this.manageSso,
|
||||
managePolicies = this.shouldManagePolicies,
|
||||
manageUsers = this.manageUsers,
|
||||
manageResetPassword = this.shouldManageResetPassword,
|
||||
manageScim = this.manageScim,
|
||||
)
|
||||
|
||||
private fun OrganizationStatusType.toSdkOrganizationUserStatusType(): OrganizationUserStatusType =
|
||||
when (this) {
|
||||
OrganizationStatusType.REVOKED -> OrganizationUserStatusType.REVOKED
|
||||
OrganizationStatusType.INVITED -> OrganizationUserStatusType.INVITED
|
||||
OrganizationStatusType.ACCEPTED -> OrganizationUserStatusType.ACCEPTED
|
||||
OrganizationStatusType.CONFIRMED -> OrganizationUserStatusType.CONFIRMED
|
||||
}
|
||||
|
||||
private fun OrganizationType.toSdkOrganizationUserType(): OrganizationUserType =
|
||||
when (this) {
|
||||
OrganizationType.OWNER -> OrganizationUserType.OWNER
|
||||
OrganizationType.ADMIN -> OrganizationUserType.ADMIN
|
||||
OrganizationType.USER -> OrganizationUserType.USER
|
||||
OrganizationType.CUSTOM -> OrganizationUserType.CUSTOM
|
||||
}
|
||||
|
||||
private fun ProviderType.toSdkProviderType(): SdkProviderType =
|
||||
when (this) {
|
||||
ProviderType.MSP -> SdkProviderType.MSP
|
||||
ProviderType.RESELLER -> SdkProviderType.RESELLER
|
||||
ProviderType.BUSINESS_UNIT -> SdkProviderType.BUSINESS_UNIT
|
||||
}
|
||||
|
||||
private fun ProductTierType.toSdkProductTierType(): SdkProductTierType =
|
||||
when (this) {
|
||||
ProductTierType.FREE -> SdkProductTierType.FREE
|
||||
ProductTierType.FAMILIES -> SdkProductTierType.FAMILIES
|
||||
ProductTierType.TEAMS -> SdkProductTierType.TEAMS
|
||||
ProductTierType.ENTERPRISE -> SdkProductTierType.ENTERPRISE
|
||||
ProductTierType.TEAMS_STARTER -> SdkProductTierType.TEAMS_STARTER
|
||||
}
|
||||
|
||||
private fun MemberDecryptionType.toSdkMemberDecryptionType(): SdkMemberDecryptionType =
|
||||
when (this) {
|
||||
MemberDecryptionType.MASTER_PASSWORD -> SdkMemberDecryptionType.MASTER_PASSWORD
|
||||
MemberDecryptionType.KEY_CONNECTOR -> SdkMemberDecryptionType.KEY_CONNECTOR
|
||||
MemberDecryptionType.TRUSTED_DEVICE_ENCRYPTION -> {
|
||||
SdkMemberDecryptionType.TRUSTED_DEVICE_ENCRYPTION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.billing.repository.util
|
||||
import com.bitwarden.network.model.BitwardenDiscountJson
|
||||
import com.bitwarden.network.model.BitwardenSubscriptionResponseJson
|
||||
import com.bitwarden.network.model.CadenceTypeJson
|
||||
import com.bitwarden.network.model.CartItemJson
|
||||
import com.bitwarden.network.model.DiscountTypeJson
|
||||
import com.bitwarden.network.model.SubscriptionStatusJson
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.PlanCadence
|
||||
@@ -11,28 +12,35 @@ import com.x8bit.bitwarden.data.billing.repository.model.SubscriptionInfo
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
private val PERCENT_DIVISOR: BigDecimal = BigDecimal("100")
|
||||
private const val MONEY_SCALE: Int = 2
|
||||
|
||||
/**
|
||||
* Maps a [BitwardenSubscriptionResponseJson] into a [SubscriptionInfo] domain
|
||||
* model.
|
||||
*
|
||||
* `discountAmount` is resolved at mapping time: fixed-amount discounts pass
|
||||
* through as-is; percent-off discounts apply to the password manager subtotal
|
||||
* (`seatsCost + storageCost`). `nextChargeTotal` is computed client-side as
|
||||
* `seatsCost + storageCost - discountAmount + estimatedTax` because the server
|
||||
* Each line item's `cost` is a per-unit price, so its contribution is
|
||||
* `cost * quantity`. Two discount channels are combined into `discountAmount`:
|
||||
* the cart-level discount applies to the password manager subtotal
|
||||
* (`seatsCost + storageCost`), and the Password Manager seats item-level
|
||||
* discount applies to the seats line total. Item-level discounts on other line
|
||||
* items are intentionally ignored, mirroring the web client. Fixed-amount
|
||||
* discounts pass through as-is; percent-off discounts treat a value below 1 as
|
||||
* an already-decimal fraction and round half-up. `nextChargeTotal` is computed
|
||||
* client-side as `subtotal - discountAmount + estimatedTax` because the server
|
||||
* does not expose a precomputed total.
|
||||
*/
|
||||
fun BitwardenSubscriptionResponseJson.toSubscriptionInfo(): SubscriptionInfo {
|
||||
val seatsCost = cart.passwordManager.seats.cost
|
||||
val storageCost = cart.passwordManager.additionalStorage?.cost
|
||||
val discountAmount = cart.discount?.toMoneyAmount(
|
||||
subtotal = seatsCost + (storageCost ?: BigDecimal.ZERO),
|
||||
)
|
||||
val seatsCost = cart.passwordManager.seats.lineTotal()
|
||||
val storageCost = cart.passwordManager.additionalStorage?.lineTotal()
|
||||
val subtotal = seatsCost + (storageCost ?: BigDecimal.ZERO)
|
||||
val cartDiscount = cart.discount?.toDiscountAmount(baseAmount = subtotal)
|
||||
val seatsDiscount = cart.passwordManager.seats.discount
|
||||
?.toDiscountAmount(baseAmount = seatsCost)
|
||||
val discountAmount = listOfNotNull(cartDiscount, seatsDiscount)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.reduce(BigDecimal::add)
|
||||
val estimatedTax = cart.estimatedTax
|
||||
val nextChargeTotal = seatsCost +
|
||||
(storageCost ?: BigDecimal.ZERO) -
|
||||
val nextChargeTotal = subtotal -
|
||||
(discountAmount ?: BigDecimal.ZERO) +
|
||||
estimatedTax
|
||||
|
||||
@@ -76,16 +84,18 @@ private fun BitwardenSubscriptionResponseJson.toPremiumSubscriptionStatus():
|
||||
SubscriptionStatusJson.PAUSED -> PremiumSubscriptionStatus.PAUSED
|
||||
}
|
||||
|
||||
private fun CartItemJson.lineTotal(): BigDecimal = cost.multiply(quantity.toBigDecimal())
|
||||
|
||||
private fun CadenceTypeJson.toPlanCadence(): PlanCadence = when (this) {
|
||||
CadenceTypeJson.ANNUALLY -> PlanCadence.ANNUALLY
|
||||
CadenceTypeJson.MONTHLY -> PlanCadence.MONTHLY
|
||||
}
|
||||
|
||||
private fun BitwardenDiscountJson.toMoneyAmount(subtotal: BigDecimal): BigDecimal =
|
||||
private fun BitwardenDiscountJson.toDiscountAmount(baseAmount: BigDecimal): BigDecimal =
|
||||
when (type) {
|
||||
DiscountTypeJson.AMOUNT_OFF -> value
|
||||
DiscountTypeJson.PERCENT_OFF ->
|
||||
subtotal
|
||||
.multiply(value)
|
||||
.divide(PERCENT_DIVISOR, MONEY_SCALE, RoundingMode.HALF_EVEN)
|
||||
DiscountTypeJson.PERCENT_OFF -> {
|
||||
val percentage = if (value < BigDecimal.ONE) value else value.movePointLeft(2)
|
||||
baseAmount.multiply(percentage).setScale(MONEY_SCALE, RoundingMode.HALF_UP)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,17 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
*/
|
||||
var initialAutofillDialogShown: Boolean?
|
||||
|
||||
/**
|
||||
* Indicates if the accessibility disclaimer has been displayed to the user.
|
||||
*/
|
||||
var hasShownAccessibilityDisclaimer: Boolean?
|
||||
|
||||
/**
|
||||
* Emits up-to-date values indicating if the accessibility disclaimer has been displayed to
|
||||
* the user.
|
||||
*/
|
||||
val hasShownAccessibilityDisclaimerFlow: Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* The currently persisted app theme (or `null` if not set).
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,7 @@ private const val ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY = "accountBiometricInteg
|
||||
private const val CRASH_LOGGING_ENABLED_KEY = "crashLoggingEnabled"
|
||||
private const val CLEAR_CLIPBOARD_INTERVAL_KEY = "clearClipboard"
|
||||
private const val INITIAL_AUTOFILL_DIALOG_SHOWN = "addSitePromptShown"
|
||||
private const val HAS_SHOWN_ACCESSIBILITY_DISCLAIMER_KEY = "hasShownAccessibilityDisclaimer"
|
||||
private const val HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY = "hasUserLoggedInOrCreatedAccount"
|
||||
private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
|
||||
private const val SHOW_BROWSER_AUTOFILL_SETTING_BADGE = "showBrowserAutofillSettingBadge"
|
||||
@@ -128,6 +129,8 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
private val mutableIsDynamicColorsEnabledFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasShownAccessibilityDisclaimerFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
init {
|
||||
migrateScreenCaptureSetting()
|
||||
}
|
||||
@@ -167,6 +170,17 @@ class SettingsDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override var hasShownAccessibilityDisclaimer: Boolean?
|
||||
set(value) {
|
||||
putBoolean(HAS_SHOWN_ACCESSIBILITY_DISCLAIMER_KEY, value)
|
||||
mutableHasShownAccessibilityDisclaimerFlow.tryEmit(value)
|
||||
}
|
||||
get() = getBoolean(HAS_SHOWN_ACCESSIBILITY_DISCLAIMER_KEY)
|
||||
|
||||
override val hasShownAccessibilityDisclaimerFlow: Flow<Boolean?>
|
||||
get() = mutableHasShownAccessibilityDisclaimerFlow
|
||||
.onSubscription { emit(hasShownAccessibilityDisclaimer) }
|
||||
|
||||
override var systemBiometricIntegritySource: String?
|
||||
get() = getString(key = SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY)
|
||||
set(value) {
|
||||
@@ -270,6 +284,7 @@ class SettingsDiskSourceImpl(
|
||||
// - Upgraded to Premium action card consumed
|
||||
// - Upgraded to Premium action card pending
|
||||
// - Premium upgrade pending
|
||||
// - Has shown accessibility disclaimer dialog
|
||||
}
|
||||
|
||||
override fun getIntroducingArchiveActionCardDismissed(userId: String): Boolean? =
|
||||
|
||||
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@@ -95,15 +96,15 @@ class PolicyManagerImpl(
|
||||
},
|
||||
featureFlagManager.getFeatureFlagFlow(key = FlagKey.PoliciesInAcceptedState),
|
||||
) { policies, organizations, isEnabled ->
|
||||
this
|
||||
.filterPolicies(
|
||||
type = type,
|
||||
policies = policies,
|
||||
organizations = organizations,
|
||||
isPoliciesInAcceptedStateEnabled = isEnabled,
|
||||
)
|
||||
.orEmpty()
|
||||
filterPolicies(
|
||||
type = type,
|
||||
policies = policies,
|
||||
organizations = organizations,
|
||||
isPoliciesInAcceptedStateEnabled = isEnabled,
|
||||
)
|
||||
}
|
||||
// We do not have any policies yet if it is null, so do not emit at all.
|
||||
.filterNotNull()
|
||||
|
||||
private fun filterPolicies(
|
||||
type: PolicyType,
|
||||
|
||||
@@ -187,6 +187,16 @@ interface SettingsRepository : FlightRecorderManager {
|
||||
*/
|
||||
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Whether the accessibility disclaimer has been displayed to the user.
|
||||
*/
|
||||
val hasShownAccessibilityDisclaimerFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Stores that the accessibility disclaimer has been displayed to the user.
|
||||
*/
|
||||
fun accessibilityDisclaimerHasBeenShown()
|
||||
|
||||
/**
|
||||
* Disables autofill if it is currently enabled.
|
||||
*/
|
||||
|
||||
@@ -372,6 +372,16 @@ class SettingsRepositoryImpl(
|
||||
initialValue = isScreenCaptureAllowed,
|
||||
)
|
||||
|
||||
override val hasShownAccessibilityDisclaimerFlow: StateFlow<Boolean>
|
||||
get() = settingsDiskSource
|
||||
.hasShownAccessibilityDisclaimerFlow
|
||||
.map { it ?: false }
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = settingsDiskSource.hasShownAccessibilityDisclaimer ?: false,
|
||||
)
|
||||
|
||||
init {
|
||||
policyManager
|
||||
.getActivePoliciesFlow(type = PolicyType.MAXIMUM_VAULT_TIMEOUT)
|
||||
@@ -379,6 +389,10 @@ class SettingsRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
|
||||
override fun accessibilityDisclaimerHasBeenShown() {
|
||||
settingsDiskSource.hasShownAccessibilityDisclaimer = true
|
||||
}
|
||||
|
||||
override fun disableAutofill() {
|
||||
autofillManager.disableAutofillServices()
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.generators.PassphraseGeneratorRequest
|
||||
import com.bitwarden.generators.PasswordGeneratorRequest
|
||||
import com.bitwarden.generators.UsernameGeneratorRequest
|
||||
import com.bitwarden.sdk.GeneratorClients
|
||||
import com.x8bit.bitwarden.data.platform.datasource.sdk.BaseSdkSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Implementation of [GeneratorSdkSource] that delegates password generation.
|
||||
@@ -14,6 +16,7 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
* [GeneratorClients] provided by the Bitwarden SDK.
|
||||
*/
|
||||
class GeneratorSdkSourceImpl(
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
sdkClientManager: SdkClientManager,
|
||||
) : BaseSdkSource(sdkClientManager = sdkClientManager),
|
||||
GeneratorSdkSource {
|
||||
@@ -51,6 +54,8 @@ class GeneratorSdkSourceImpl(
|
||||
override suspend fun generateForwardedServiceEmail(
|
||||
request: UsernameGeneratorRequest.Forwarded,
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
useClient { generators().username(request) }
|
||||
withContext(context = dispatcherManager.io) {
|
||||
useClient { generators().username(request) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.sdk.di
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSourceImpl
|
||||
@@ -19,6 +20,10 @@ object GeneratorSdkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGeneratorSdkSource(
|
||||
dispatcherManager: DispatcherManager,
|
||||
sdkClientManager: SdkClientManager,
|
||||
): GeneratorSdkSource = GeneratorSdkSourceImpl(sdkClientManager = sdkClientManager)
|
||||
): GeneratorSdkSource = GeneratorSdkSourceImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
sdkClientManager = sdkClientManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -55,7 +54,7 @@ class GeneratorRepositoryImpl(
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : GeneratorRepository {
|
||||
|
||||
private val scope = CoroutineScope(dispatcherManager.io)
|
||||
@@ -193,8 +192,9 @@ class GeneratorRepositoryImpl(
|
||||
|
||||
override suspend fun generateForwardedServiceUsername(
|
||||
forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded,
|
||||
): GeneratedForwardedServiceUsernameResult = withContext(dispatcherManager.io) {
|
||||
generatorSdkSource.generateForwardedServiceEmail(forwardedServiceGeneratorRequest)
|
||||
): GeneratedForwardedServiceUsernameResult =
|
||||
generatorSdkSource
|
||||
.generateForwardedServiceEmail(forwardedServiceGeneratorRequest)
|
||||
.fold(
|
||||
onSuccess = { generatedEmail ->
|
||||
GeneratedForwardedServiceUsernameResult.Success(generatedEmail)
|
||||
@@ -203,7 +203,6 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message, error = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPasscodeGenerationOptions(): PasscodeGenerationOptions? {
|
||||
val userId = authDiskSource.userState?.activeUserId
|
||||
|
||||
@@ -32,7 +32,7 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
val org = authRepository.userStateFlow.value
|
||||
?.activeAccount
|
||||
?.organizations
|
||||
?.firstOrNull { it.shouldUseKeyConnector }
|
||||
?.firstOrNull { it.isKeyConnectorEnabled }
|
||||
|
||||
RemovePasswordState(
|
||||
input = "",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type-safe route for the accessibility disclosure screen.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@Serializable
|
||||
data object AccessibilityDisclosureRoute
|
||||
|
||||
/**
|
||||
* Add the accessibility disclosure screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.accessibilityDisclosureDestination(
|
||||
onDismiss: () -> Unit,
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<AccessibilityDisclosureRoute> {
|
||||
AccessibilityDisclosureScreen(onDismiss = onDismiss)
|
||||
// If we are displaying the accessibility disclosure screen, then we can just hide
|
||||
// the splash screen.
|
||||
onSplashScreenRemoved()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the accessibility disclosure screen.
|
||||
*/
|
||||
fun NavController.navigateToAccessibilityDisclosure() {
|
||||
this.navigate(route = AccessibilityDisclosureRoute) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.composition.LocalExitManager
|
||||
import com.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Top-level composable for the Accessibility Disclosure screen.
|
||||
*/
|
||||
@Composable
|
||||
fun AccessibilityDisclosureScreen(
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: AccessibilityDisclosureViewModel = hiltViewModel(),
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
) {
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is AccessibilityDisclosureEvent.Dismiss -> onDismiss()
|
||||
is AccessibilityDisclosureEvent.CloseApp -> exitManager.exitApplication()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler { viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick) }
|
||||
BitwardenScaffold(
|
||||
contentWindowInsets = ScaffoldDefaults
|
||||
.contentWindowInsets
|
||||
.union(WindowInsets.displayCutout)
|
||||
.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top),
|
||||
) {
|
||||
AccessibilityDisclosureContent(
|
||||
onAcceptClick = {
|
||||
viewModel.trySendAction(AccessibilityDisclosureAction.AcceptClicked)
|
||||
},
|
||||
onCloseAppClick = {
|
||||
viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccessibilityDisclosureContent(
|
||||
onAcceptClick: () -> Unit,
|
||||
onCloseAppClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.verticalScroll(state = rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(height = 32.dp))
|
||||
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.ill_autofill),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.size(size = 100.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.accessibility_service_disclosure),
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.accessibility_disclosure_start_up_text),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.accept),
|
||||
onClick = onAcceptClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = BitwardenString.close_app),
|
||||
onClick = onCloseAppClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AccessibilityDisclosureContent_preview() {
|
||||
BitwardenTheme {
|
||||
AccessibilityDisclosureContent(
|
||||
onAcceptClick = {},
|
||||
onCloseAppClick = {},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* ViewModel for the Accessibility Disclosure screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AccessibilityDisclosureViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) : BaseViewModel<
|
||||
AccessibilityDisclosureState,
|
||||
AccessibilityDisclosureEvent,
|
||||
AccessibilityDisclosureAction,
|
||||
>(
|
||||
initialState = AccessibilityDisclosureState,
|
||||
) {
|
||||
override fun handleAction(action: AccessibilityDisclosureAction) {
|
||||
when (action) {
|
||||
AccessibilityDisclosureAction.AcceptClicked -> handleAcceptClicked()
|
||||
AccessibilityDisclosureAction.CloseAppClick -> handleCloseAppClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAcceptClicked() {
|
||||
settingsRepository.accessibilityDisclaimerHasBeenShown()
|
||||
sendEvent(AccessibilityDisclosureEvent.Dismiss)
|
||||
}
|
||||
|
||||
private fun handleCloseAppClick() {
|
||||
sendEvent(AccessibilityDisclosureEvent.CloseApp)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State for the Accessibility Disclosure screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data object AccessibilityDisclosureState : Parcelable
|
||||
|
||||
/**
|
||||
* Events for the Accessibility Disclosure screen.
|
||||
*/
|
||||
sealed class AccessibilityDisclosureEvent {
|
||||
/**
|
||||
* Navigate back, dismissing the screen.
|
||||
*/
|
||||
data object Dismiss : AccessibilityDisclosureEvent()
|
||||
|
||||
/**
|
||||
* Closes the app.
|
||||
*/
|
||||
data object CloseApp : AccessibilityDisclosureEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions for the Accessibility Disclosure screen.
|
||||
*/
|
||||
sealed class AccessibilityDisclosureAction {
|
||||
/**
|
||||
* User clicked the accept button.
|
||||
*/
|
||||
data object AcceptClicked : AccessibilityDisclosureAction()
|
||||
|
||||
/**
|
||||
* User clicked the close app button.
|
||||
*/
|
||||
data object CloseAppClick : AccessibilityDisclosureAction()
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import com.x8bit.bitwarden.data.billing.manager.PremiumStateManager
|
||||
import com.x8bit.bitwarden.data.billing.repository.BillingRepository
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.CheckoutSessionResult
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.CustomerPortalResult
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.PlanCadence
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.PremiumPlanPricingResult
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.PremiumSubscriptionStatus
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.SubscriptionInfo
|
||||
@@ -32,6 +31,10 @@ import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.toBillingAmountText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.toDiscountMoneyText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.toPresentMoneyText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.toRequiredMoneyText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -40,7 +43,6 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.math.BigDecimal
|
||||
import java.text.NumberFormat
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
@@ -713,11 +715,11 @@ class PlanViewModel @Inject constructor(
|
||||
|
||||
return PlanState.ViewState.Premium(
|
||||
status = status,
|
||||
billingAmountText = seatsCost.toBillingAmountText(cadence),
|
||||
storageCostText = storageCost.toOptionalMoneyText(),
|
||||
discountAmountText = discountAmount.toOptionalMoneyText(negative = true),
|
||||
estimatedTaxText = estimatedTax.toRequiredMoneyText(),
|
||||
totalText = nextChargeTotal.toBillingAmountText(cadence),
|
||||
billingAmountText = seatsCost.toBillingAmountText(cadence, currencyFormatter),
|
||||
storageCostText = storageCost.toPresentMoneyText(currencyFormatter),
|
||||
discountAmountText = discountAmount.toDiscountMoneyText(currencyFormatter),
|
||||
estimatedTaxText = estimatedTax.toRequiredMoneyText(currencyFormatter),
|
||||
totalText = nextChargeTotal.toBillingAmountText(cadence, currencyFormatter),
|
||||
nextChargeTotalText = formattedTotal,
|
||||
nextChargeDateText = formattedDate,
|
||||
cancelAtDateText = formattedCancelAt,
|
||||
@@ -728,36 +730,6 @@ class PlanViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal.toBillingAmountText(cadence: PlanCadence): Text {
|
||||
val formatted = currencyFormatter.format(this)
|
||||
val cadenceRes = when (cadence) {
|
||||
PlanCadence.ANNUALLY -> BitwardenString.billing_rate_per_year
|
||||
PlanCadence.MONTHLY -> BitwardenString.billing_rate_per_month
|
||||
}
|
||||
return cadenceRes.asText(formatted)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats this amount for an always-rendered line item. Null is coerced to zero so the row
|
||||
* still shows the locale-formatted `$0.00`, matching the Web convention of always rendering
|
||||
* the Estimated Tax and Total rows.
|
||||
*/
|
||||
private fun BigDecimal?.toRequiredMoneyText(): String =
|
||||
currencyFormatter.format(this ?: BigDecimal.ZERO)
|
||||
|
||||
/**
|
||||
* Formats this amount for a hide-when-absent line item. Returns `null` when the amount is
|
||||
* `null` or non-positive so the caller can omit the row entirely (Discount, Storage).
|
||||
* When [negative] is true, the formatted value is prefixed with `-` to match the canonical
|
||||
* Web discount styling.
|
||||
*/
|
||||
private fun BigDecimal?.toOptionalMoneyText(negative: Boolean = false): String? =
|
||||
when {
|
||||
this == null || this.signum() <= 0 -> null
|
||||
negative -> "-${currencyFormatter.format(this)}"
|
||||
else -> currencyFormatter.format(this)
|
||||
}
|
||||
|
||||
private fun Instant.toLocalizedDate(): String =
|
||||
toFormattedDateStyle(
|
||||
dateStyle = FormatStyle.LONG,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.premium.plan.util
|
||||
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.PlanCadence
|
||||
import java.math.BigDecimal
|
||||
import java.text.NumberFormat
|
||||
|
||||
/**
|
||||
* Formats this amount as a cadence-qualified billing rate (e.g. "$10.00 per year"), using
|
||||
* [currencyFormatter] for the locale-aware currency value.
|
||||
*/
|
||||
fun BigDecimal.toBillingAmountText(
|
||||
cadence: PlanCadence,
|
||||
currencyFormatter: NumberFormat,
|
||||
): Text {
|
||||
val formatted = currencyFormatter.format(this)
|
||||
val cadenceRes = when (cadence) {
|
||||
PlanCadence.ANNUALLY -> BitwardenString.billing_rate_per_year
|
||||
PlanCadence.MONTHLY -> BitwardenString.billing_rate_per_month
|
||||
}
|
||||
return cadenceRes.asText(formatted)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats this amount for an always-rendered line item. Null is coerced to zero so the row still
|
||||
* shows the locale-formatted `$0.00`, as the Estimated Tax and Total rows always render.
|
||||
*/
|
||||
fun BigDecimal?.toRequiredMoneyText(currencyFormatter: NumberFormat): String =
|
||||
currencyFormatter.format(this ?: BigDecimal.ZERO)
|
||||
|
||||
/**
|
||||
* Formats this amount for a render-when-present line item (Storage), rendering `$0.00` for a
|
||||
* free line and returning `null` only when the amount is `null`.
|
||||
*/
|
||||
fun BigDecimal?.toPresentMoneyText(currencyFormatter: NumberFormat): String? =
|
||||
this?.let { currencyFormatter.format(it) }
|
||||
|
||||
/**
|
||||
* Formats this amount as a negative money string for the Discount line item (e.g. "-$5.00"),
|
||||
* returning `null` when the amount is `null` or non-positive so the row is omitted when there is
|
||||
* no discount.
|
||||
*/
|
||||
fun BigDecimal?.toDiscountMoneyText(currencyFormatter: NumberFormat): String? =
|
||||
this
|
||||
?.takeIf { it.signum() > 0 }
|
||||
?.let { "\u2212${currencyFormatter.format(it)}" }
|
||||
@@ -268,7 +268,7 @@ class RootNavViewModel @Inject constructor(
|
||||
?.let(::parseJwtTokenDataOrNull)
|
||||
?.isExternal == true
|
||||
val usesKeyConnectorAndNotAdmin = this.activeAccount.organizations.any {
|
||||
it.shouldUseKeyConnector &&
|
||||
it.isKeyConnectorEnabled &&
|
||||
it.role != OrganizationType.OWNER &&
|
||||
it.role != OrganizationType.ADMIN
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
private val mutableAppLanguageFlow = MutableStateFlow(AppLanguage.DEFAULT)
|
||||
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true)
|
||||
private val mutableIsDynamicColorsEnabledFlow = MutableStateFlow(false)
|
||||
private val mutableHasShownAccessibilityDisclaimerFlow = MutableStateFlow(true)
|
||||
private val settingsRepository = mockk<SettingsRepository> {
|
||||
every { appTheme } returns AppTheme.DEFAULT
|
||||
every { appThemeStateFlow } returns mutableAppThemeFlow
|
||||
@@ -124,6 +125,10 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
every { appLanguage = any() } just runs
|
||||
every { isDynamicColorsEnabled } returns false
|
||||
every { isDynamicColorsEnabledFlow } returns mutableIsDynamicColorsEnabledFlow
|
||||
every {
|
||||
hasShownAccessibilityDisclaimerFlow
|
||||
} returns mutableHasShownAccessibilityDisclaimerFlow
|
||||
every { accessibilityDisclaimerHasBeenShown() } just runs
|
||||
}
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { activeUserId } returns DEFAULT_USER_STATE.activeUserId
|
||||
@@ -1385,6 +1390,23 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on HasShownAccessibilityDisclaimerUpdate with false should show the accessibility disclosure`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
// We skip the first 2 events because they are the default appTheme and appLanguage
|
||||
skipItems(2)
|
||||
|
||||
mutableHasShownAccessibilityDisclaimerFlow.value = false
|
||||
assertEquals(MainEvent.NavigateToAccessibilityDisclosure, awaitItem())
|
||||
|
||||
mutableHasShownAccessibilityDisclaimerFlow.value = true
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
): MainViewModel = MainViewModel(
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
@@ -57,6 +58,7 @@ class AuthSdkSourceTest {
|
||||
}
|
||||
|
||||
private val authSkdSource: AuthSdkSource = AuthSdkSourceImpl(
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
sdkClientManager = sdkClientManager,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.manager
|
||||
import com.bitwarden.auth.KeyConnectorRegistrationResult
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.WrappedAccountCryptographicState
|
||||
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
@@ -40,7 +39,6 @@ class KeyConnectorManagerTest {
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
featureFlagManager = featureFlagManager,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
)
|
||||
|
||||
@Test
|
||||
|
||||
@@ -5383,7 +5383,7 @@ class AuthRepositoryTest {
|
||||
val organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
shouldUseKeyConnector = true,
|
||||
isKeyConnectorEnabled = true,
|
||||
type = OrganizationType.USER,
|
||||
keyConnectorUrl = null,
|
||||
),
|
||||
@@ -5408,7 +5408,7 @@ class AuthRepositoryTest {
|
||||
val organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
shouldUseKeyConnector = true,
|
||||
isKeyConnectorEnabled = true,
|
||||
type = OrganizationType.USER,
|
||||
keyConnectorUrl = url,
|
||||
),
|
||||
@@ -5441,7 +5441,7 @@ class AuthRepositoryTest {
|
||||
val organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
shouldUseKeyConnector = true,
|
||||
isKeyConnectorEnabled = true,
|
||||
type = OrganizationType.USER,
|
||||
keyConnectorUrl = url,
|
||||
),
|
||||
@@ -5477,7 +5477,7 @@ class AuthRepositoryTest {
|
||||
val organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
shouldUseKeyConnector = true,
|
||||
isKeyConnectorEnabled = true,
|
||||
type = OrganizationType.USER,
|
||||
keyConnectorUrl = url,
|
||||
),
|
||||
@@ -5512,7 +5512,7 @@ class AuthRepositoryTest {
|
||||
val organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
shouldUseKeyConnector = true,
|
||||
isKeyConnectorEnabled = true,
|
||||
type = OrganizationType.USER,
|
||||
keyConnectorUrl = url,
|
||||
),
|
||||
|
||||
@@ -22,7 +22,7 @@ fun createMockOrganization(
|
||||
id = id,
|
||||
name = name,
|
||||
shouldManageResetPassword = shouldManageResetPassword,
|
||||
shouldUseKeyConnector = shouldUseKeyConnector,
|
||||
isKeyConnectorEnabled = shouldUseKeyConnector,
|
||||
role = role,
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
userIsClaimedByOrganization = userIsClaimedByOrganization,
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
@file:Suppress("LongParameterList")
|
||||
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.bitwarden.organizations.MemberDecryptionType
|
||||
import com.bitwarden.organizations.OrganizationUserStatusType
|
||||
import com.bitwarden.organizations.OrganizationUserType
|
||||
import com.bitwarden.organizations.Permissions
|
||||
import com.bitwarden.organizations.ProductTierType
|
||||
import com.bitwarden.organizations.ProfileOrganization
|
||||
import com.bitwarden.organizations.ProviderType
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Creates a mock [ProfileOrganization] with the given [number].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun createMockSdkProfileOrganization(
|
||||
number: Int,
|
||||
usePolicies: Boolean = false,
|
||||
keyConnectorEnabled: Boolean = false,
|
||||
keyConnectorUrl: String? = "mockKeyConnectorUrl-$number",
|
||||
type: OrganizationUserType = OrganizationUserType.ADMIN,
|
||||
seats: UInt? = 1u,
|
||||
enabled: Boolean = false,
|
||||
providerType: ProviderType? = ProviderType.RESELLER,
|
||||
maxCollections: UInt? = 1u,
|
||||
selfHost: Boolean = false,
|
||||
permissions: Permissions = createMockSdkPermissions(),
|
||||
providerId: String? = "mockProviderId-$number",
|
||||
id: String = "mockId-$number",
|
||||
useGroups: Boolean = false,
|
||||
useDirectory: Boolean = false,
|
||||
providerName: String? = "mockProviderName-$number",
|
||||
usersGetPremium: Boolean = false,
|
||||
maxStorageGb: UInt? = 1u,
|
||||
identifier: String? = "mockIdentifier-$number",
|
||||
use2fa: Boolean = false,
|
||||
familySponsorshipToDelete: Boolean? = false,
|
||||
userId: String? = "mockUserId-$number",
|
||||
shouldUseEvents: Boolean = false,
|
||||
familySponsorshipFriendlyName: String? = "mockFamilySponsorshipFriendlyName-$number",
|
||||
useTotp: Boolean = false,
|
||||
familySponsorshipLastSyncDate: Instant? = Instant.parse("2023-10-27T12:00:00Z"),
|
||||
name: String = "mockName-$number",
|
||||
useApi: Boolean = false,
|
||||
familySponsorshipValidUntil: Instant? = Instant.parse("2023-10-27T12:00:00Z"),
|
||||
status: OrganizationUserStatusType = OrganizationUserStatusType.CONFIRMED,
|
||||
userIsClaimedByOrganization: Boolean = false,
|
||||
limitItemDeletion: Boolean = false,
|
||||
useSso: Boolean = false,
|
||||
useOrganizationDomains: Boolean = false,
|
||||
shouldUseKeyConnector: Boolean = false,
|
||||
useScim: Boolean = false,
|
||||
useCustomPermissions: Boolean = false,
|
||||
useResetPassword: Boolean = false,
|
||||
useSecretsManager: Boolean = false,
|
||||
usePasswordManager: Boolean = false,
|
||||
useActivateAutofillPolicy: Boolean = false,
|
||||
useAutomaticUserConfirmation: Boolean = false,
|
||||
ssoBound: Boolean = false,
|
||||
resetPasswordEnrolled: Boolean = false,
|
||||
organizationUserId: String? = "mockOrganizationUserId-$number",
|
||||
hasPublicAndPrivateKeys: Boolean = false,
|
||||
isProviderUser: Boolean = false,
|
||||
isMember: Boolean = false,
|
||||
familySponsorshipAvailable: Boolean = false,
|
||||
productTierType: ProductTierType = ProductTierType.FREE,
|
||||
accessSecretsManager: Boolean = false,
|
||||
limitCollectionCreation: Boolean = false,
|
||||
limitCollectionDeletion: Boolean = false,
|
||||
allowAdminAccessToAllCollectionItems: Boolean = false,
|
||||
useAccessIntelligence: Boolean = false,
|
||||
useAdminSponsoredFamilies: Boolean = false,
|
||||
useDisableSmAdsForUsers: Boolean = false,
|
||||
isAdminInitiated: Boolean = false,
|
||||
ssoEnabled: Boolean = false,
|
||||
ssoMemberDecryptionType: MemberDecryptionType? = null,
|
||||
usePhishingBlocker: Boolean = false,
|
||||
useMyItems: Boolean = false,
|
||||
): ProfileOrganization =
|
||||
ProfileOrganization(
|
||||
id = id,
|
||||
name = name,
|
||||
status = status,
|
||||
type = type,
|
||||
enabled = enabled,
|
||||
usePolicies = usePolicies,
|
||||
useGroups = useGroups,
|
||||
useDirectory = useDirectory,
|
||||
useEvents = shouldUseEvents,
|
||||
useTotp = useTotp,
|
||||
use2fa = use2fa,
|
||||
useApi = useApi,
|
||||
useSso = useSso,
|
||||
useOrganizationDomains = useOrganizationDomains,
|
||||
useKeyConnector = shouldUseKeyConnector,
|
||||
useScim = useScim,
|
||||
useCustomPermissions = useCustomPermissions,
|
||||
useResetPassword = useResetPassword,
|
||||
useSecretsManager = useSecretsManager,
|
||||
usePasswordManager = usePasswordManager,
|
||||
useActivateAutofillPolicy = useActivateAutofillPolicy,
|
||||
useAutomaticUserConfirmation = useAutomaticUserConfirmation,
|
||||
selfHost = selfHost,
|
||||
usersGetPremium = usersGetPremium,
|
||||
seats = seats,
|
||||
maxCollections = maxCollections,
|
||||
maxStorageGb = maxStorageGb,
|
||||
ssoBound = ssoBound,
|
||||
identifier = identifier,
|
||||
permissions = permissions,
|
||||
resetPasswordEnrolled = resetPasswordEnrolled,
|
||||
userId = userId,
|
||||
organizationUserId = organizationUserId,
|
||||
hasPublicAndPrivateKeys = hasPublicAndPrivateKeys,
|
||||
providerId = providerId,
|
||||
providerName = providerName,
|
||||
providerType = providerType,
|
||||
isProviderUser = isProviderUser,
|
||||
isMember = isMember,
|
||||
familySponsorshipFriendlyName = familySponsorshipFriendlyName,
|
||||
familySponsorshipAvailable = familySponsorshipAvailable,
|
||||
productTierType = productTierType,
|
||||
keyConnectorEnabled = keyConnectorEnabled,
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
familySponsorshipLastSyncDate = familySponsorshipLastSyncDate,
|
||||
familySponsorshipValidUntil = familySponsorshipValidUntil,
|
||||
familySponsorshipToDelete = familySponsorshipToDelete,
|
||||
accessSecretsManager = accessSecretsManager,
|
||||
limitCollectionCreation = limitCollectionCreation,
|
||||
limitCollectionDeletion = limitCollectionDeletion,
|
||||
limitItemDeletion = limitItemDeletion,
|
||||
allowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems,
|
||||
userIsManagedByOrganization = userIsClaimedByOrganization,
|
||||
useAccessIntelligence = useAccessIntelligence,
|
||||
useAdminSponsoredFamilies = useAdminSponsoredFamilies,
|
||||
useDisableSmAdsForUsers = useDisableSmAdsForUsers,
|
||||
isAdminInitiated = isAdminInitiated,
|
||||
ssoEnabled = ssoEnabled,
|
||||
ssoMemberDecryptionType = ssoMemberDecryptionType,
|
||||
usePhishingBlocker = usePhishingBlocker,
|
||||
useMyItems = useMyItems,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [Permissions].
|
||||
*/
|
||||
fun createMockSdkPermissions(
|
||||
shouldManageResetPassword: Boolean = false,
|
||||
shouldManagePolicies: Boolean = false,
|
||||
accessEventLogs: Boolean = false,
|
||||
accessImportExport: Boolean = false,
|
||||
accessReports: Boolean = false,
|
||||
createNewCollections: Boolean = false,
|
||||
editAnyCollection: Boolean = false,
|
||||
deleteAnyCollection: Boolean = false,
|
||||
manageGroups: Boolean = false,
|
||||
manageSso: Boolean = false,
|
||||
manageUsers: Boolean = false,
|
||||
manageScim: Boolean = false,
|
||||
): Permissions =
|
||||
Permissions(
|
||||
manageResetPassword = shouldManageResetPassword,
|
||||
managePolicies = shouldManagePolicies,
|
||||
accessEventLogs = accessEventLogs,
|
||||
accessImportExport = accessImportExport,
|
||||
accessReports = accessReports,
|
||||
createNewCollections = createNewCollections,
|
||||
editAnyCollection = editAnyCollection,
|
||||
deleteAnyCollection = deleteAnyCollection,
|
||||
manageGroups = manageGroups,
|
||||
manageSso = manageSso,
|
||||
manageUsers = manageUsers,
|
||||
manageScim = manageScim,
|
||||
)
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.createMockOrganizationNetwork
|
||||
import com.bitwarden.network.model.createMockPermissions
|
||||
import com.bitwarden.organizations.ProfileOrganization
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.createMockOrganization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.createMockSdkProfileOrganization
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPolicyView
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -36,7 +39,7 @@ class SyncResponseJsonExtensionsTest {
|
||||
),
|
||||
),
|
||||
listOf(
|
||||
createMockOrganizationNetwork(number = 1, shouldUseKeyConnector = true),
|
||||
createMockOrganizationNetwork(number = 1, isKeyConnectorEnabled = true),
|
||||
createMockOrganizationNetwork(
|
||||
number = 2,
|
||||
type = OrganizationType.USER,
|
||||
@@ -47,6 +50,62 @@ class SyncResponseJsonExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSdkProfileOrganizations should correctly map a single organization`() {
|
||||
assertEquals(
|
||||
listOf(createMockSdkProfileOrganization(number = 1)),
|
||||
listOf(createMockOrganizationNetwork(number = 1)).toSdkProfileOrganizations(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSdkProfileOrganizations should correctly map multiple organizations`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
createMockSdkProfileOrganization(number = 1),
|
||||
createMockSdkProfileOrganization(number = 2),
|
||||
),
|
||||
listOf(
|
||||
createMockOrganizationNetwork(number = 1),
|
||||
createMockOrganizationNetwork(number = 2),
|
||||
)
|
||||
.toSdkProfileOrganizations(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSdkProfileOrganizations should filter out organizations with null names`() {
|
||||
assertEquals(
|
||||
listOf(createMockSdkProfileOrganization(number = 1)),
|
||||
listOf(
|
||||
createMockOrganizationNetwork(number = 1),
|
||||
createMockOrganizationNetwork(number = 2, name = null),
|
||||
)
|
||||
.toSdkProfileOrganizations(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toSdkProfileOrganizations should return empty list when all organizations have null names`() {
|
||||
assertEquals(
|
||||
emptyList<ProfileOrganization>(),
|
||||
listOf(
|
||||
createMockOrganizationNetwork(number = 1, name = null),
|
||||
createMockOrganizationNetwork(number = 2, name = null),
|
||||
)
|
||||
.toSdkProfileOrganizations(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSdkProfileOrganizations should return empty list for empty input`() {
|
||||
assertEquals(
|
||||
emptyList<ProfileOrganization>(),
|
||||
emptyList<SyncResponseJson.Profile.Organization>().toSdkProfileOrganizations(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `policyInformation converts the MasterPassword Json data to policy information`() {
|
||||
val policyInformation = PolicyInformation.MasterPassword(
|
||||
|
||||
@@ -126,6 +126,24 @@ class BitwardenSubscriptionResponseJsonExtensionsTest {
|
||||
assertEquals(BigDecimal("24.00"), info.storageCost)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo storageCost multiplies unit cost by quantity`() {
|
||||
val info = buildResponse(
|
||||
storageCost = BigDecimal("4"),
|
||||
storageQuantity = 3,
|
||||
).toSubscriptionInfo()
|
||||
assertEquals(BigDecimal("12"), info.storageCost)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo seatsCost multiplies unit cost by quantity`() {
|
||||
val info = buildResponse(
|
||||
seatsCost = BigDecimal("19.80"),
|
||||
seatsQuantity = 2,
|
||||
).toSubscriptionInfo()
|
||||
assertEquals(BigDecimal("39.60"), info.seatsCost)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo discountAmount is null when no discount`() {
|
||||
val info = buildResponse(discount = null).toSubscriptionInfo()
|
||||
@@ -157,6 +175,77 @@ class BitwardenSubscriptionResponseJsonExtensionsTest {
|
||||
assertEquals(BigDecimal("4.50"), info.discountAmount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo PERCENT_OFF discount treats value below one as a decimal fraction`() {
|
||||
val info = buildResponse(
|
||||
seatsCost = BigDecimal("20.00"),
|
||||
discount = BitwardenDiscountJson(
|
||||
type = DiscountTypeJson.PERCENT_OFF,
|
||||
value = BigDecimal("0.5"),
|
||||
),
|
||||
).toSubscriptionInfo()
|
||||
assertEquals(BigDecimal("10.00"), info.discountAmount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo applies PM seats item-level discount`() {
|
||||
val info = buildResponse(
|
||||
seatsCost = BigDecimal("19.80"),
|
||||
seatsDiscount = BitwardenDiscountJson(
|
||||
type = DiscountTypeJson.AMOUNT_OFF,
|
||||
value = BigDecimal("5.00"),
|
||||
),
|
||||
).toSubscriptionInfo()
|
||||
assertEquals(BigDecimal("5.00"), info.discountAmount)
|
||||
assertEquals(BigDecimal("14.80"), info.nextChargeTotal)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo PERCENT_OFF seats item discount applies to seats line total`() {
|
||||
val info = buildResponse(
|
||||
seatsCost = BigDecimal("19.80"),
|
||||
seatsQuantity = 2,
|
||||
seatsDiscount = BitwardenDiscountJson(
|
||||
type = DiscountTypeJson.PERCENT_OFF,
|
||||
value = BigDecimal("10.00"),
|
||||
),
|
||||
).toSubscriptionInfo()
|
||||
assertEquals(BigDecimal("3.96"), info.discountAmount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo combines cart-level and PM seats item discounts`() {
|
||||
val info = buildResponse(
|
||||
seatsCost = BigDecimal("19.80"),
|
||||
seatsDiscount = BitwardenDiscountJson(
|
||||
type = DiscountTypeJson.AMOUNT_OFF,
|
||||
value = BigDecimal("3.00"),
|
||||
),
|
||||
discount = BitwardenDiscountJson(
|
||||
type = DiscountTypeJson.AMOUNT_OFF,
|
||||
value = BigDecimal("2.00"),
|
||||
),
|
||||
estimatedTax = BigDecimal("1.00"),
|
||||
).toSubscriptionInfo()
|
||||
assertEquals(BigDecimal("5.00"), info.discountAmount)
|
||||
assertEquals(BigDecimal("15.80"), info.nextChargeTotal)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo ignores additionalStorage item-level discount`() {
|
||||
val info = buildResponse(
|
||||
seatsCost = BigDecimal("19.80"),
|
||||
storageCost = BigDecimal("4"),
|
||||
storageQuantity = 3,
|
||||
storageDiscount = BitwardenDiscountJson(
|
||||
type = DiscountTypeJson.AMOUNT_OFF,
|
||||
value = BigDecimal("5.00"),
|
||||
),
|
||||
).toSubscriptionInfo()
|
||||
assertNull(info.discountAmount)
|
||||
assertEquals(BigDecimal("31.80"), info.nextChargeTotal)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo passes estimatedTax through`() {
|
||||
val info = buildResponse(estimatedTax = BigDecimal("3.85")).toSubscriptionInfo()
|
||||
@@ -178,6 +267,31 @@ class BitwardenSubscriptionResponseJsonExtensionsTest {
|
||||
assertEquals(BigDecimal("45.55"), info.nextChargeTotal)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo nextChargeTotal multiplies storage line by quantity`() {
|
||||
val info = buildResponse(
|
||||
seatsCost = BigDecimal("19.80"),
|
||||
storageCost = BigDecimal("4"),
|
||||
storageQuantity = 3,
|
||||
estimatedTax = BigDecimal("2.04"),
|
||||
).toSubscriptionInfo()
|
||||
assertEquals(BigDecimal("33.84"), info.nextChargeTotal)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo PERCENT_OFF discount applies to quantity-multiplied subtotal`() {
|
||||
val info = buildResponse(
|
||||
seatsCost = BigDecimal("19.80"),
|
||||
storageCost = BigDecimal("4"),
|
||||
storageQuantity = 3,
|
||||
discount = BitwardenDiscountJson(
|
||||
type = DiscountTypeJson.PERCENT_OFF,
|
||||
value = BigDecimal("10.00"),
|
||||
),
|
||||
).toSubscriptionInfo()
|
||||
assertEquals(BigDecimal("3.18"), info.discountAmount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSubscriptionInfo nextChargeTotal with minimal cart equals seatsCost`() {
|
||||
// User-provided JSON: 19.80 + 0 - 0 + 0 = 19.80
|
||||
@@ -217,7 +331,11 @@ class BitwardenSubscriptionResponseJsonExtensionsTest {
|
||||
status: SubscriptionStatusJson = SubscriptionStatusJson.ACTIVE,
|
||||
cadence: CadenceTypeJson = CadenceTypeJson.ANNUALLY,
|
||||
seatsCost: BigDecimal = BigDecimal("19.80"),
|
||||
seatsQuantity: Long = 1,
|
||||
seatsDiscount: BitwardenDiscountJson? = null,
|
||||
storageCost: BigDecimal? = null,
|
||||
storageQuantity: Long = 1,
|
||||
storageDiscount: BitwardenDiscountJson? = null,
|
||||
discount: BitwardenDiscountJson? = null,
|
||||
estimatedTax: BigDecimal = BigDecimal.ZERO,
|
||||
storage: StorageJson? = null,
|
||||
@@ -232,16 +350,16 @@ class BitwardenSubscriptionResponseJsonExtensionsTest {
|
||||
passwordManager = PasswordManagerCartItemsJson(
|
||||
seats = CartItemJson(
|
||||
translationKey = "premiumMembership",
|
||||
quantity = 1,
|
||||
quantity = seatsQuantity,
|
||||
cost = seatsCost,
|
||||
discount = null,
|
||||
discount = seatsDiscount,
|
||||
),
|
||||
additionalStorage = storageCost?.let {
|
||||
CartItemJson(
|
||||
translationKey = "additionalStorage",
|
||||
quantity = 1,
|
||||
quantity = storageQuantity,
|
||||
cost = it,
|
||||
discount = null,
|
||||
discount = storageDiscount,
|
||||
)
|
||||
},
|
||||
),
|
||||
|
||||
@@ -473,6 +473,42 @@ class SettingsDiskSourceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasShownAccessibilityDisclaimer should pull from and update SharedPreferences`() {
|
||||
val hasShownAccessibilityDisclaimerKey =
|
||||
"bwPreferencesStorage:hasShownAccessibilityDisclaimer"
|
||||
val expected = true
|
||||
|
||||
assertNull(settingsDiskSource.hasShownAccessibilityDisclaimer)
|
||||
|
||||
fakeSharedPreferences.edit {
|
||||
putBoolean(hasShownAccessibilityDisclaimerKey, expected)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
expected,
|
||||
settingsDiskSource.hasShownAccessibilityDisclaimer,
|
||||
)
|
||||
|
||||
settingsDiskSource.hasShownAccessibilityDisclaimer = false
|
||||
assertFalse(fakeSharedPreferences.getBoolean(hasShownAccessibilityDisclaimerKey, true))
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `hasShownAccessibilityDisclaimerFlow should react to changes in hasShownAccessibilityDisclaimer`() =
|
||||
runTest {
|
||||
settingsDiskSource.hasShownAccessibilityDisclaimerFlow.test {
|
||||
// The initial values of the Flow and the property are in sync
|
||||
assertNull(settingsDiskSource.hasShownAccessibilityDisclaimer)
|
||||
assertNull(awaitItem())
|
||||
settingsDiskSource.hasShownAccessibilityDisclaimer = true
|
||||
assertEquals(true, awaitItem())
|
||||
settingsDiskSource.hasShownAccessibilityDisclaimer = false
|
||||
assertEquals(false, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getVaultTimeoutInMinutes when values are present should pull from SharedPreferences`() {
|
||||
val vaultTimeoutBaseKey = "bwPreferencesStorage:vaultTimeout"
|
||||
|
||||
@@ -93,6 +93,7 @@ class FakeSettingsDiskSource(
|
||||
private var hasSeenAddLoginCoachMark: Boolean? = null
|
||||
private var hasSeenGeneratorCoachMark: Boolean? = null
|
||||
private var storedIsDynamicColorsEnabled: Boolean? = null
|
||||
private var storedHasShownAccessibilityDisclaimer: Boolean? = null
|
||||
private var storedBrowserAutofillDialogReshowTime: Instant? = null
|
||||
|
||||
private val mutableShowAutoFillSettingBadgeFlowMap =
|
||||
@@ -110,6 +111,8 @@ class FakeSettingsDiskSource(
|
||||
private val mutableIsDynamicColorsEnabled =
|
||||
bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasShownAccessibilityDisclaimerFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableVaultRegisteredForExportFlow =
|
||||
bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
@@ -162,6 +165,18 @@ class FakeSettingsDiskSource(
|
||||
emit(isDynamicColorsEnabled)
|
||||
}
|
||||
|
||||
override var hasShownAccessibilityDisclaimer: Boolean?
|
||||
get() = storedHasShownAccessibilityDisclaimer
|
||||
set(value) {
|
||||
storedHasShownAccessibilityDisclaimer = value
|
||||
mutableHasShownAccessibilityDisclaimerFlow.tryEmit(value)
|
||||
}
|
||||
|
||||
override val hasShownAccessibilityDisclaimerFlow: Flow<Boolean?>
|
||||
get() = mutableHasShownAccessibilityDisclaimerFlow.onSubscription {
|
||||
emit(hasShownAccessibilityDisclaimer)
|
||||
}
|
||||
|
||||
override var screenCaptureAllowed: Boolean?
|
||||
get() = storedScreenCaptureAllowed
|
||||
set(value) {
|
||||
|
||||
@@ -1119,6 +1119,30 @@ class SettingsRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasShownAccessibilityDisclaimerFlow should emit changes from SettingsDiskSource`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.hasShownAccessibilityDisclaimer = null
|
||||
settingsRepository.hasShownAccessibilityDisclaimerFlow.test {
|
||||
assertFalse(awaitItem())
|
||||
|
||||
fakeSettingsDiskSource.hasShownAccessibilityDisclaimer = true
|
||||
assertTrue(awaitItem())
|
||||
|
||||
fakeSettingsDiskSource.hasShownAccessibilityDisclaimer = false
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accessibilityDisclaimerHasBeenShown should update SettingsDiskSource`() {
|
||||
assertNull(fakeSettingsDiskSource.hasShownAccessibilityDisclaimer)
|
||||
|
||||
settingsRepository.accessibilityDisclaimerHasBeenShown()
|
||||
|
||||
assertTrue(fakeSettingsDiskSource.hasShownAccessibilityDisclaimer == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearClipboardFrequency should pull from and update SettingsDiskSource`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.generators.AppendType
|
||||
import com.bitwarden.generators.ForwarderServiceType
|
||||
@@ -27,7 +28,10 @@ class GeneratorSdkSourceTest {
|
||||
val slot = slot<suspend Client.() -> String>()
|
||||
coEvery { singleUseClient(block = capture(slot)) } coAnswers { slot.captured(client) }
|
||||
}
|
||||
private val generatorSdkSource: GeneratorSdkSource = GeneratorSdkSourceImpl(sdkClientManager)
|
||||
private val generatorSdkSource: GeneratorSdkSource = GeneratorSdkSourceImpl(
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
sdkClientManager = sdkClientManager,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `generatePassword should call SDK and return a Result with the generated password`() =
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class AccessibilityDisclosureScreenTest : BitwardenComposeTest() {
|
||||
|
||||
private var onDismissCalled = false
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<AccessibilityDisclosureEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(AccessibilityDisclosureState)
|
||||
private val viewModel = mockk<AccessibilityDisclosureViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
private val exitManager = mockk<ExitManager> {
|
||||
every { exitApplication() } just runs
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
setContent(exitManager = exitManager) {
|
||||
AccessibilityDisclosureScreen(
|
||||
onDismiss = { onDismissCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accept button click should send AcceptClicked action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Accept")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(AccessibilityDisclosureAction.AcceptClicked)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close app button click should send CloseAppClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Close app")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dismiss event should call onDismiss`() {
|
||||
mutableEventFlow.tryEmit(AccessibilityDisclosureEvent.Dismiss)
|
||||
assertTrue(onDismissCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseApp event should exit the application`() {
|
||||
mutableEventFlow.tryEmit(AccessibilityDisclosureEvent.CloseApp)
|
||||
verify(exactly = 1) {
|
||||
exitManager.exitApplication()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `system back should not dismiss the screen`() {
|
||||
backDispatcher?.onBackPressed()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AccessibilityDisclosureViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { accessibilityDisclaimerHasBeenShown() } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(AccessibilityDisclosureState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AcceptClicked should mark disclaimer as shown and emit Dismiss event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccessibilityDisclosureAction.AcceptClicked)
|
||||
assertEquals(AccessibilityDisclosureEvent.Dismiss, awaitItem())
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.accessibilityDisclaimerHasBeenShown()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseAppClick should emit CloseApp event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick)
|
||||
assertEquals(AccessibilityDisclosureEvent.CloseApp, awaitItem())
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
settingsRepository.accessibilityDisclaimerHasBeenShown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): AccessibilityDisclosureViewModel =
|
||||
AccessibilityDisclosureViewModel(
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
}
|
||||
@@ -1333,7 +1333,7 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubscriptionResultReceive Success with zero line items hides discount and storage rows`() =
|
||||
fun `SubscriptionResultReceive Success renders zero storage row but hides zero discount`() =
|
||||
runTest {
|
||||
markUserPremium()
|
||||
|
||||
@@ -1351,7 +1351,7 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(
|
||||
DEFAULT_PREMIUM_LOADED_STATE.copy(
|
||||
viewState = DEFAULT_PREMIUM_ACTIVE_VIEW_STATE.copy(
|
||||
storageCostText = null,
|
||||
storageCostText = "$0.00",
|
||||
discountAmountText = null,
|
||||
estimatedTaxText = "$0.00",
|
||||
),
|
||||
@@ -1904,7 +1904,7 @@ private val DEFAULT_PREMIUM_ACTIVE_VIEW_STATE = PlanState.ViewState.Premium(
|
||||
status = PremiumSubscriptionStatus.ACTIVE,
|
||||
billingAmountText = BitwardenString.billing_rate_per_year.asText("$19.80"),
|
||||
storageCostText = "$24.00",
|
||||
discountAmountText = "-$2.10",
|
||||
discountAmountText = "\u2212$2.10",
|
||||
estimatedTaxText = "$3.85",
|
||||
totalText = BitwardenString.billing_rate_per_year.asText("$45.55"),
|
||||
nextChargeTotalText = "$45.55",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.premium.plan.util
|
||||
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.PlanCadence
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class BigDecimalExtensionsTest {
|
||||
private val currencyFormatter: NumberFormat = NumberFormat.getCurrencyInstance(Locale.US)
|
||||
|
||||
@Test
|
||||
fun `toBillingAmountText returns the per-year rate for an annual cadence`() {
|
||||
assertEquals(
|
||||
BitwardenString.billing_rate_per_year.asText("$10.00"),
|
||||
BigDecimal("10").toBillingAmountText(PlanCadence.ANNUALLY, currencyFormatter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toBillingAmountText returns the per-month rate for a monthly cadence`() {
|
||||
assertEquals(
|
||||
BitwardenString.billing_rate_per_month.asText("$10.00"),
|
||||
BigDecimal("10").toBillingAmountText(PlanCadence.MONTHLY, currencyFormatter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toRequiredMoneyText coerces null to a formatted zero`() {
|
||||
assertEquals("$0.00", null.toRequiredMoneyText(currencyFormatter))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toRequiredMoneyText formats zero and positive amounts`() {
|
||||
assertEquals("$0.00", BigDecimal.ZERO.toRequiredMoneyText(currencyFormatter))
|
||||
assertEquals("$10.00", BigDecimal("10").toRequiredMoneyText(currencyFormatter))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toPresentMoneyText returns null only when the amount is null`() {
|
||||
assertNull(null.toPresentMoneyText(currencyFormatter))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toPresentMoneyText renders zero and positive amounts`() {
|
||||
assertEquals("$0.00", BigDecimal.ZERO.toPresentMoneyText(currencyFormatter))
|
||||
assertEquals("$10.00", BigDecimal("10").toPresentMoneyText(currencyFormatter))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toDiscountMoneyText returns null when the amount is null or non-positive`() {
|
||||
assertNull(null.toDiscountMoneyText(currencyFormatter))
|
||||
assertNull(BigDecimal.ZERO.toDiscountMoneyText(currencyFormatter))
|
||||
assertNull(BigDecimal("-5").toDiscountMoneyText(currencyFormatter))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toDiscountMoneyText formats a positive amount as a negative money string`() {
|
||||
assertEquals("\u2212$5.00", BigDecimal("5").toDiscountMoneyText(currencyFormatter))
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ androdixAutofill = "1.3.0"
|
||||
androidxBiometrics = "1.2.0-alpha05"
|
||||
androidxBrowser = "1.10.0"
|
||||
androidxCamera = "1.6.1"
|
||||
androidxComposeBom = "2026.05.00"
|
||||
androidxComposeBom = "2026.05.01"
|
||||
androidxCore = "1.18.0"
|
||||
androidxCredentials = "1.6.0"
|
||||
androidxCredentialsProviderEvents = "1.0.0-alpha06"
|
||||
@@ -33,7 +33,7 @@ androidxWork = "2.11.2"
|
||||
bitwardenSdk = "3.0.0-7126-025e5d85"
|
||||
crashlytics = "3.0.7"
|
||||
detekt = "1.23.8"
|
||||
firebaseBom = "34.13.0"
|
||||
firebaseBom = "34.14.0"
|
||||
glide = "5.0.7"
|
||||
glideCompose = "1.0.0-beta01"
|
||||
googleBilling = "8.3.0"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.core.data.serializer.BaseEnumeratedIntSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type of encryption used for an SSO user organization.
|
||||
*/
|
||||
@Serializable(with = MemberDecryptionTypeSerializer::class)
|
||||
enum class MemberDecryptionType {
|
||||
/**
|
||||
* Decryption using the user's master password.
|
||||
*/
|
||||
@SerialName("0")
|
||||
MASTER_PASSWORD,
|
||||
|
||||
/**
|
||||
* Decryption via Key Connector.
|
||||
*/
|
||||
@SerialName("1")
|
||||
KEY_CONNECTOR,
|
||||
|
||||
/**
|
||||
* Decryption via Trusted Device Encryption.
|
||||
*/
|
||||
@SerialName("2")
|
||||
TRUSTED_DEVICE_ENCRYPTION,
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class MemberDecryptionTypeSerializer : BaseEnumeratedIntSerializer<MemberDecryptionType>(
|
||||
className = "MemberDecryptionType",
|
||||
values = MemberDecryptionType.entries.toTypedArray(),
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.core.data.serializer.BaseEnumeratedIntSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The subscription tier of an organization.
|
||||
*/
|
||||
@Serializable(with = ProductTierTypeSerializer::class)
|
||||
enum class ProductTierType {
|
||||
/**
|
||||
* Free tier with limited features.
|
||||
*/
|
||||
@SerialName("0")
|
||||
FREE,
|
||||
|
||||
/**
|
||||
* Families plan for personal use.
|
||||
*/
|
||||
@SerialName("1")
|
||||
FAMILIES,
|
||||
|
||||
/**
|
||||
* Teams plan for small organizations.
|
||||
*/
|
||||
@SerialName("2")
|
||||
TEAMS,
|
||||
|
||||
/**
|
||||
* Enterprise plan with full features.
|
||||
*/
|
||||
@SerialName("3")
|
||||
ENTERPRISE,
|
||||
|
||||
/**
|
||||
* Starter tier for small teams.
|
||||
*/
|
||||
@SerialName("4")
|
||||
TEAMS_STARTER,
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class ProductTierTypeSerializer : BaseEnumeratedIntSerializer<ProductTierType>(
|
||||
className = "ProductTierType",
|
||||
values = ProductTierType.entries.toTypedArray(),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.core.data.serializer.BaseEnumeratedIntSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type of provider.
|
||||
*/
|
||||
@Serializable(with = ProviderTypeSerializer::class)
|
||||
enum class ProviderType {
|
||||
/**
|
||||
* Managed Service Provider - sells and manages its clients' Bitwarden organizations.
|
||||
*/
|
||||
@SerialName("0")
|
||||
MSP,
|
||||
|
||||
/**
|
||||
* Reseller partner - sells Bitwarden to its clients but does not have any administrative
|
||||
* access.
|
||||
*/
|
||||
@SerialName("1")
|
||||
RESELLER,
|
||||
|
||||
/**
|
||||
* Business unit provider - used to manage multiple organizations which form part of a single
|
||||
* large enterprise.
|
||||
*/
|
||||
@SerialName("2")
|
||||
BUSINESS_UNIT,
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class ProviderTypeSerializer : BaseEnumeratedIntSerializer<ProviderType>(
|
||||
className = "ProviderType",
|
||||
values = ProviderType.entries.toTypedArray(),
|
||||
)
|
||||
@@ -252,41 +252,108 @@ data class SyncResponseJson(
|
||||
val organizations: List<Organization>? get() = newOrganizations ?: legacyOrganizations
|
||||
|
||||
/**
|
||||
* Represents an organization in the vault response.
|
||||
* Represents an organization profile for the current user, containing the organization's
|
||||
* feature flags, membership details, and configuration settings.
|
||||
*
|
||||
* @property shouldUsePolicies If the organization should use policies.
|
||||
* @property keyConnectorUrl The key connector URL of the organization (nullable).
|
||||
* @property type The type of organization.
|
||||
* @property seats The number of seats in the organization (nullable).
|
||||
* @property isEnabled If the organization is enabled.
|
||||
* @property providerType They type of provider for the organization (nullable).
|
||||
* @property maxCollections The max collections of the organization (nullable).
|
||||
* @property isSelfHost If the organization is self hosted.
|
||||
* @property permissions The permissions of the organization.
|
||||
* @property providerId The provider ID of the organization (nullable).
|
||||
* @property id The ID of the organization.
|
||||
* @property shouldUseGroups If the organization should use groups.
|
||||
* @property shouldUseDirectory If the organization should use a directory.
|
||||
* @property shouldUsePolicies Whether the organization has access to policies features.
|
||||
* @property isKeyConnectorEnabled Whether Key Connector is enabled for this organization.
|
||||
* @property keyConnectorUrl The URL of the Key Connector service, if enabled.
|
||||
* @property type The user's role in the organization.
|
||||
* @property seats The number of licensed seats for the organization.
|
||||
* @property isEnabled Whether the organization is currently enabled.
|
||||
* @property providerType The type of provider managing this organization, if any.
|
||||
* @property isProviderUser Whether the current user accesses this organization through a
|
||||
* provider.
|
||||
* @property maxCollections The maximum number of collections the organization can create.
|
||||
* @property isSelfHost Whether the organization can create a license file for a self-hosted
|
||||
* instance.
|
||||
* @property permissions The current user's custom permissions, relevant when
|
||||
* [OrganizationType.CUSTOM] is the user's type.
|
||||
* @property providerId The ID of the provider managing this organization, if any.
|
||||
* @property id Unique identifier for the organization.
|
||||
* @property shouldUseGroups Whether the organization has access to groups features.
|
||||
* @property shouldUseDirectory Whether the organization has access to directory sync
|
||||
* features.
|
||||
* @property key The key of the organization (nullable).
|
||||
* @property providerName The provider name of the organization (nullable).
|
||||
* @property shouldUsersGetPremium If users of the organization get Premium.
|
||||
* @property maxStorageGb The max storage in Gb of the organization (nullable).
|
||||
* @property identifier The identifier of the organization (nullable).
|
||||
* @property use2fa If the organization uses 2FA.
|
||||
* @property familySponsorshipToDelete If the organization has a
|
||||
* family sponsorship to delete (nullable).
|
||||
* @property userId The user id (nullable).
|
||||
* @property shouldUseEvents If the organization should use events.
|
||||
* @property familySponsorshipFriendlyName If the family sponsorship is a friendly name.
|
||||
* @property shouldUseTotp If he organization should use TOTP.
|
||||
* @property familySponsorshipLastSyncDate The last date the family sponsorship
|
||||
* was synced (nullable).
|
||||
* @property name The name of the organization (nullable).
|
||||
* @property shouldUseApi If the organization should use API.
|
||||
* @property familySponsorshipValidUntil The family sponsorship valid until
|
||||
* of the organization (nullable).
|
||||
* @property status The status of the organization.
|
||||
* @property limitItemDeletion If the organization limits item deletion.
|
||||
* @property providerName The name of the provider managing this organization, if any.
|
||||
* @property shouldUsersGetPremium Whether organization members receive premium features.
|
||||
* @property maxStorageGb The maximum encrypted storage in gigabytes, if limited.
|
||||
* @property identifier The organization's SSO identifier.
|
||||
* @property use2fa Whether the organization has access to two-factor authentication
|
||||
* features.
|
||||
* @property familySponsorshipToDelete Whether the families sponsorship is scheduled for
|
||||
* deletion.
|
||||
* @property userId The current user's personal user ID.
|
||||
* @property shouldUseEvents Whether the organization has access to event logging features.
|
||||
* @property familySponsorshipFriendlyName The friendly name of a pending families
|
||||
* sponsorship, if any.
|
||||
* @property shouldUseTotp Whether the organization can enforce TOTP for members.
|
||||
* @property familySponsorshipLastSyncDate The date the families sponsorship was last
|
||||
* synced, if applicable.
|
||||
* @property name Display name of the organization.
|
||||
* @property shouldUseApi Whether the organization has access to the Bitwarden Public API.
|
||||
* @property familySponsorshipValidUntil The date the families sponsorship expires, if
|
||||
* applicable.
|
||||
* @property status The user's membership status in the organization.
|
||||
* @property userIsClaimedByOrganization Whether the current user has been claimed by this
|
||||
* organization.
|
||||
* @property limitItemDeletion Whether item deletion is restricted to members with the
|
||||
* Manage collection permission. When false, members with Edit permission can also delete
|
||||
* items within their collections.
|
||||
* @property useSso Whether the organization has access to SSO features.
|
||||
* @property useOrganizationDomains Whether the organization can manage verified domains.
|
||||
* @property useScim Whether the organization has access to SCIM provisioning.
|
||||
* @property useCustomPermissions Whether the organization can use the
|
||||
* [OrganizationType.CUSTOM] role.
|
||||
* @property useResetPassword Whether the organization has access to the account recovery
|
||||
* (admin password reset) feature.
|
||||
* @property useSecretsManager Whether the organization has access to Secrets Manager.
|
||||
* @property usePasswordManager Whether the organization has access to Password Manager.
|
||||
* @property useActivateAutofillPolicy Whether the organization can use the activate
|
||||
* autofill policy.
|
||||
* @property useAutomaticUserConfirmation Whether the organization can automatically
|
||||
* confirm new members without manual admin approval.
|
||||
* @property ssoBound Whether the current user's account is bound to this organization
|
||||
* via SSO.
|
||||
* @property resetPasswordEnrolled Whether the current user is enrolled in account recovery
|
||||
* for this organization.
|
||||
* @property organizationUserId The current user's organization membership ID.
|
||||
* @property hasPublicAndPrivateKeys Whether the organization has both a public and private
|
||||
* key configured.
|
||||
* @property isMember Whether the current user is a direct member of this organization (as
|
||||
* opposed to provider-only access).
|
||||
* @property familySponsorshipAvailable Whether the organization can sponsor a families
|
||||
* plan for the current user.
|
||||
* @property productTierType The subscription tier of the organization.
|
||||
* @property shouldUseKeyConnector Whether the organization uses Key Connector for
|
||||
* decryption.
|
||||
* @property accessSecretsManager Whether the current user has access to Secrets Manager
|
||||
* for this organization.
|
||||
* @property limitCollectionCreation Whether collection creation is restricted to owners
|
||||
* and admins only. When false, any member can create collections and automatically
|
||||
* receives manage permissions over collections they create.
|
||||
* @property limitCollectionDeletion Whether collection deletion is restricted to owners and
|
||||
* admins only. When true, regular users cannot delete collections that they manage.
|
||||
* @property allowAdminAccessToAllCollectionItems Whether owners and admins have implicit
|
||||
* manage permissions over all collections. When true, owners and admins can alter items,
|
||||
* groups, and permissions across all collections without requiring explicit collection
|
||||
* assignments. When false, admins can only access collections where they have been
|
||||
* explicitly assigned.
|
||||
* @property useAccessIntelligence Whether the organization has access to Access
|
||||
* Intelligence features.
|
||||
* @property useAdminSponsoredFamilies Whether the organization can sponsor families plans
|
||||
* for members (Families For Enterprises).
|
||||
* @property useDisableSmAdsForUsers Whether Secrets Manager ads are disabled for users.
|
||||
* @property isAdminInitiated Whether the organization's Families For Enterprises
|
||||
* sponsorship was initiated by an admin.
|
||||
* @property ssoEnabled Whether SSO login is currently enabled for this organization.
|
||||
* @property ssoMemberDecryptionType The decryption type used for SSO members, if SSO is
|
||||
* enabled.
|
||||
* @property usePhishingBlocker Whether the organization has access to phishing blocker
|
||||
* features.
|
||||
* @property useMyItems Whether the organization has access to the My Items collection
|
||||
* feature. This allows users to store personal items in the organization vault if the
|
||||
* Centralize Organization Ownership policy is enabled.
|
||||
*/
|
||||
@Serializable
|
||||
data class Organization(
|
||||
@@ -294,7 +361,7 @@ data class SyncResponseJson(
|
||||
val shouldUsePolicies: Boolean,
|
||||
|
||||
@SerialName("keyConnectorEnabled")
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val isKeyConnectorEnabled: Boolean,
|
||||
|
||||
@SerialName("keyConnectorUrl")
|
||||
val keyConnectorUrl: String?,
|
||||
@@ -303,19 +370,19 @@ data class SyncResponseJson(
|
||||
val type: OrganizationType,
|
||||
|
||||
@SerialName("seats")
|
||||
val seats: Int?,
|
||||
val seats: UInt?,
|
||||
|
||||
@SerialName("enabled")
|
||||
val isEnabled: Boolean,
|
||||
|
||||
@SerialName("providerType")
|
||||
val providerType: Int?,
|
||||
val providerType: ProviderType?,
|
||||
|
||||
@SerialName("isProviderUser")
|
||||
val isProviderUser: Boolean = false,
|
||||
|
||||
@SerialName("maxCollections")
|
||||
val maxCollections: Int?,
|
||||
val maxCollections: UInt?,
|
||||
|
||||
@SerialName("selfHost")
|
||||
val isSelfHost: Boolean,
|
||||
@@ -345,7 +412,7 @@ data class SyncResponseJson(
|
||||
val shouldUsersGetPremium: Boolean,
|
||||
|
||||
@SerialName("maxStorageGb")
|
||||
val maxStorageGb: Int?,
|
||||
val maxStorageGb: UInt?,
|
||||
|
||||
@SerialName("identifier")
|
||||
val identifier: String?,
|
||||
@@ -390,6 +457,93 @@ data class SyncResponseJson(
|
||||
|
||||
@SerialName("limitItemDeletion")
|
||||
val limitItemDeletion: Boolean = false,
|
||||
|
||||
@SerialName("useSso")
|
||||
val useSso: Boolean = false,
|
||||
|
||||
@SerialName("useOrganizationDomains")
|
||||
val useOrganizationDomains: Boolean = false,
|
||||
|
||||
@SerialName("useScim")
|
||||
val useScim: Boolean = false,
|
||||
|
||||
@SerialName("useCustomPermissions")
|
||||
val useCustomPermissions: Boolean = false,
|
||||
|
||||
@SerialName("useResetPassword")
|
||||
val useResetPassword: Boolean = false,
|
||||
|
||||
@SerialName("useSecretsManager")
|
||||
val useSecretsManager: Boolean = false,
|
||||
|
||||
@SerialName("usePasswordManager")
|
||||
val usePasswordManager: Boolean = false,
|
||||
|
||||
@SerialName("useActivateAutofillPolicy")
|
||||
val useActivateAutofillPolicy: Boolean = false,
|
||||
|
||||
@SerialName("useAutomaticUserConfirmation")
|
||||
val useAutomaticUserConfirmation: Boolean = false,
|
||||
|
||||
@SerialName("ssoBound")
|
||||
val ssoBound: Boolean = false,
|
||||
|
||||
@SerialName("resetPasswordEnrolled")
|
||||
val resetPasswordEnrolled: Boolean = false,
|
||||
|
||||
@SerialName("organizationUserId")
|
||||
val organizationUserId: String?,
|
||||
|
||||
@SerialName("hasPublicAndPrivateKeys")
|
||||
val hasPublicAndPrivateKeys: Boolean = false,
|
||||
|
||||
@SerialName("isMember")
|
||||
val isMember: Boolean = false,
|
||||
|
||||
@SerialName("familySponsorshipAvailable")
|
||||
val familySponsorshipAvailable: Boolean = false,
|
||||
|
||||
@SerialName("productTierType")
|
||||
val productTierType: ProductTierType = ProductTierType.FREE,
|
||||
|
||||
@SerialName("useKeyConnector")
|
||||
val shouldUseKeyConnector: Boolean = false,
|
||||
|
||||
@SerialName("accessSecretsManager")
|
||||
val accessSecretsManager: Boolean = false,
|
||||
|
||||
@SerialName("limitCollectionCreation")
|
||||
val limitCollectionCreation: Boolean = false,
|
||||
|
||||
@SerialName("limitCollectionDeletion")
|
||||
val limitCollectionDeletion: Boolean = false,
|
||||
|
||||
@SerialName("allowAdminAccessToAllCollectionItems")
|
||||
val allowAdminAccessToAllCollectionItems: Boolean = false,
|
||||
|
||||
@SerialName("useAccessIntelligence")
|
||||
val useAccessIntelligence: Boolean = false,
|
||||
|
||||
@SerialName("useAdminSponsoredFamilies")
|
||||
val useAdminSponsoredFamilies: Boolean = false,
|
||||
|
||||
@SerialName("useDisableSmAdsForUsers")
|
||||
val useDisableSmAdsForUsers: Boolean = false,
|
||||
|
||||
@SerialName("isAdminInitiated")
|
||||
val isAdminInitiated: Boolean = false,
|
||||
|
||||
@SerialName("ssoEnabled")
|
||||
val ssoEnabled: Boolean = false,
|
||||
|
||||
@SerialName("ssoMemberDecryptionType")
|
||||
val ssoMemberDecryptionType: MemberDecryptionType?,
|
||||
|
||||
@SerialName("usePhishingBlocker")
|
||||
val usePhishingBlocker: Boolean = false,
|
||||
|
||||
@SerialName("useMyItems")
|
||||
val useMyItems: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -436,10 +590,24 @@ data class SyncResponseJson(
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents permissions in the vault response.
|
||||
* Custom permission set for a user with the [OrganizationType.CUSTOM] role.
|
||||
*
|
||||
* @property shouldManageResetPassword If reset password should be managed.
|
||||
* @property shouldManagePolicies If policies should be managed.
|
||||
* @property shouldManageResetPassword Can manage the account recovery (password reset)
|
||||
* feature.
|
||||
* @property shouldManagePolicies Can manage organization policies.
|
||||
* @property accessEventLogs Can view the organization's event logs.
|
||||
* @property accessImportExport Can import and export organization vault data.
|
||||
* @property accessReports Can access organization reports.
|
||||
* @property createNewCollections Can create new collections.
|
||||
* @property editAnyCollection Can edit any collection, including those they are not
|
||||
* assigned to.
|
||||
* @property deleteAnyCollection Can delete any collection, including those they are not
|
||||
* assigned to.
|
||||
* @property manageGroups Can manage groups within the organization.
|
||||
* @property manageSso Can manage SSO configuration.
|
||||
* @property manageUsers Can manage organization members.
|
||||
* @property manageScim Can manage SCIM (System for Cross-domain Identity Management)
|
||||
* configuration.
|
||||
*/
|
||||
@Serializable
|
||||
data class Permissions(
|
||||
@@ -448,6 +616,36 @@ data class SyncResponseJson(
|
||||
|
||||
@SerialName("managePolicies")
|
||||
val shouldManagePolicies: Boolean,
|
||||
|
||||
@SerialName("accessEventLogs")
|
||||
val accessEventLogs: Boolean = false,
|
||||
|
||||
@SerialName("accessImportExport")
|
||||
val accessImportExport: Boolean = false,
|
||||
|
||||
@SerialName("accessReports")
|
||||
val accessReports: Boolean = false,
|
||||
|
||||
@SerialName("createNewCollections")
|
||||
val createNewCollections: Boolean = false,
|
||||
|
||||
@SerialName("editAnyCollection")
|
||||
val editAnyCollection: Boolean = false,
|
||||
|
||||
@SerialName("deleteAnyCollection")
|
||||
val deleteAnyCollection: Boolean = false,
|
||||
|
||||
@SerialName("manageGroups")
|
||||
val manageGroups: Boolean = false,
|
||||
|
||||
@SerialName("manageSso")
|
||||
val manageSso: Boolean = false,
|
||||
|
||||
@SerialName("manageUsers")
|
||||
val manageUsers: Boolean = false,
|
||||
|
||||
@SerialName("manageScim")
|
||||
val manageScim: Boolean = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,8 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"editAnyCollection": false,
|
||||
"accessEventLogs": false,
|
||||
"createNewCollections": false,
|
||||
"editAssignedCollections": false
|
||||
"editAssignedCollections": false,
|
||||
"manageScim": false
|
||||
},
|
||||
"providerId": "mockProviderId-1",
|
||||
"id": "mockId-1",
|
||||
@@ -119,7 +120,38 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"useApi": false,
|
||||
"familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z",
|
||||
"status": 2,
|
||||
"userIsClaimedByOrganization": false
|
||||
"userIsClaimedByOrganization": false,
|
||||
"useSso": false,
|
||||
"useOrganizationDomains": false,
|
||||
"useKeyConnector": false,
|
||||
"useScim": false,
|
||||
"useCustomPermissions": false,
|
||||
"useResetPassword": false,
|
||||
"useSecretsManager": false,
|
||||
"usePasswordManager": false,
|
||||
"useActivateAutofillPolicy": false,
|
||||
"useAutomaticUserConfirmation": false,
|
||||
"ssoBound": false,
|
||||
"resetPasswordEnrolled": false,
|
||||
"organizationUserId": "mockOrganizationUserId-1",
|
||||
"hasPublicAndPrivateKeys": false,
|
||||
"isProviderUser": false,
|
||||
"isMember": false,
|
||||
"familySponsorshipAvailable": false,
|
||||
"productTierType": 0,
|
||||
"accessSecretsManager": false,
|
||||
"limitCollectionCreation": false,
|
||||
"limitCollectionDeletion": false,
|
||||
"limitItemDeletion": false,
|
||||
"allowAdminAccessToAllCollectionItems": false,
|
||||
"useAccessIntelligence": false,
|
||||
"useAdminSponsoredFamilies": false,
|
||||
"useDisableSmAdsForUsers": false,
|
||||
"isAdminInitiated": false,
|
||||
"ssoEnabled": false,
|
||||
"ssoMemberDecryptionType": null,
|
||||
"usePhishingBlocker": false,
|
||||
"useMyItems": false
|
||||
}
|
||||
],
|
||||
"organizationsNew": [
|
||||
@@ -146,7 +178,8 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"editAnyCollection": false,
|
||||
"accessEventLogs": false,
|
||||
"createNewCollections": false,
|
||||
"editAssignedCollections": false
|
||||
"editAssignedCollections": false,
|
||||
"manageScim": false
|
||||
},
|
||||
"providerId": "mockProviderId-1",
|
||||
"id": "mockId-1",
|
||||
@@ -168,7 +201,38 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"useApi": false,
|
||||
"familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z",
|
||||
"status": 2,
|
||||
"userIsClaimedByOrganization": false
|
||||
"userIsClaimedByOrganization": false,
|
||||
"useSso": false,
|
||||
"useOrganizationDomains": false,
|
||||
"useKeyConnector": false,
|
||||
"useScim": false,
|
||||
"useCustomPermissions": false,
|
||||
"useResetPassword": false,
|
||||
"useSecretsManager": false,
|
||||
"usePasswordManager": false,
|
||||
"useActivateAutofillPolicy": false,
|
||||
"useAutomaticUserConfirmation": false,
|
||||
"ssoBound": false,
|
||||
"resetPasswordEnrolled": false,
|
||||
"organizationUserId": "mockOrganizationUserId-1",
|
||||
"hasPublicAndPrivateKeys": false,
|
||||
"isProviderUser": false,
|
||||
"isMember": false,
|
||||
"familySponsorshipAvailable": false,
|
||||
"productTierType": 0,
|
||||
"accessSecretsManager": false,
|
||||
"limitCollectionCreation": false,
|
||||
"limitCollectionDeletion": false,
|
||||
"limitItemDeletion": false,
|
||||
"allowAdminAccessToAllCollectionItems": false,
|
||||
"useAccessIntelligence": false,
|
||||
"useAdminSponsoredFamilies": false,
|
||||
"useDisableSmAdsForUsers": false,
|
||||
"isAdminInitiated": false,
|
||||
"ssoEnabled": false,
|
||||
"ssoMemberDecryptionType": null,
|
||||
"usePhishingBlocker": false,
|
||||
"useMyItems": false
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
@@ -187,7 +251,8 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"editAnyCollection": false,
|
||||
"accessEventLogs": false,
|
||||
"createNewCollections": false,
|
||||
"editAssignedCollections": false
|
||||
"editAssignedCollections": false,
|
||||
"manageScim": false
|
||||
},
|
||||
"name": "mockName-1",
|
||||
"id": "mockId-1",
|
||||
@@ -222,7 +287,8 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"editAnyCollection": false,
|
||||
"accessEventLogs": false,
|
||||
"createNewCollections": false,
|
||||
"editAssignedCollections": false
|
||||
"editAssignedCollections": false,
|
||||
"manageScim": false
|
||||
},
|
||||
"providerId": "mockProviderId-1",
|
||||
"id": "mockId-1",
|
||||
@@ -244,7 +310,38 @@ private const val SYNC_SUCCESS_JSON = """
|
||||
"useApi": false,
|
||||
"familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z",
|
||||
"status": 2,
|
||||
"userIsClaimedByOrganization": false
|
||||
"userIsClaimedByOrganization": false,
|
||||
"useSso": false,
|
||||
"useOrganizationDomains": false,
|
||||
"useKeyConnector": false,
|
||||
"useScim": false,
|
||||
"useCustomPermissions": false,
|
||||
"useResetPassword": false,
|
||||
"useSecretsManager": false,
|
||||
"usePasswordManager": false,
|
||||
"useActivateAutofillPolicy": false,
|
||||
"useAutomaticUserConfirmation": false,
|
||||
"ssoBound": false,
|
||||
"resetPasswordEnrolled": false,
|
||||
"organizationUserId": "mockOrganizationUserId-1",
|
||||
"hasPublicAndPrivateKeys": false,
|
||||
"isProviderUser": false,
|
||||
"isMember": false,
|
||||
"familySponsorshipAvailable": false,
|
||||
"productTierType": 0,
|
||||
"accessSecretsManager": false,
|
||||
"limitCollectionCreation": false,
|
||||
"limitCollectionDeletion": false,
|
||||
"limitItemDeletion": false,
|
||||
"allowAdminAccessToAllCollectionItems": false,
|
||||
"useAccessIntelligence": false,
|
||||
"useAdminSponsoredFamilies": false,
|
||||
"useDisableSmAdsForUsers": false,
|
||||
"isAdminInitiated": false,
|
||||
"ssoEnabled": false,
|
||||
"ssoMemberDecryptionType": null,
|
||||
"usePhishingBlocker": false,
|
||||
"useMyItems": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -63,16 +63,17 @@ fun createMockProfile(
|
||||
/**
|
||||
* Create a mock [SyncResponseJson.Profile.Organization] with a given [number].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun createMockOrganizationNetwork(
|
||||
number: Int,
|
||||
shouldUsePolicies: Boolean = false,
|
||||
shouldUseKeyConnector: Boolean = false,
|
||||
isKeyConnectorEnabled: Boolean = false,
|
||||
keyConnectorUrl: String? = "mockKeyConnectorUrl-$number",
|
||||
type: OrganizationType = OrganizationType.ADMIN,
|
||||
seats: Int? = 1,
|
||||
seats: UInt? = 1u,
|
||||
isEnabled: Boolean = false,
|
||||
providerType: Int? = 1,
|
||||
maxCollections: Int? = 1,
|
||||
providerType: ProviderType? = ProviderType.RESELLER,
|
||||
maxCollections: UInt? = 1u,
|
||||
isSelfHost: Boolean = false,
|
||||
permissions: SyncResponseJson.Profile.Permissions = createMockPermissions(),
|
||||
providerId: String? = "mockProviderId-$number",
|
||||
@@ -82,7 +83,7 @@ fun createMockOrganizationNetwork(
|
||||
key: String? = "mockKey-$number",
|
||||
providerName: String? = "mockProviderName-$number",
|
||||
shouldUsersGetPremium: Boolean = false,
|
||||
maxStorageGb: Int? = 1,
|
||||
maxStorageGb: UInt? = 1u,
|
||||
identifier: String? = "mockIdentifier-$number",
|
||||
use2fa: Boolean = false,
|
||||
familySponsorshipToDelete: Boolean? = false,
|
||||
@@ -97,10 +98,40 @@ fun createMockOrganizationNetwork(
|
||||
status: OrganizationStatusType = OrganizationStatusType.CONFIRMED,
|
||||
userIsClaimedByOrganization: Boolean = false,
|
||||
limitItemDeletion: Boolean = false,
|
||||
useSso: Boolean = false,
|
||||
useOrganizationDomains: Boolean = false,
|
||||
shouldUseKeyConnector: Boolean = false,
|
||||
useScim: Boolean = false,
|
||||
useCustomPermissions: Boolean = false,
|
||||
useResetPassword: Boolean = false,
|
||||
useSecretsManager: Boolean = false,
|
||||
usePasswordManager: Boolean = false,
|
||||
useActivateAutofillPolicy: Boolean = false,
|
||||
useAutomaticUserConfirmation: Boolean = false,
|
||||
ssoBound: Boolean = false,
|
||||
resetPasswordEnrolled: Boolean = false,
|
||||
organizationUserId: String? = "mockOrganizationUserId-$number",
|
||||
hasPublicAndPrivateKeys: Boolean = false,
|
||||
isProviderUser: Boolean = false,
|
||||
isMember: Boolean = false,
|
||||
familySponsorshipAvailable: Boolean = false,
|
||||
productTierType: ProductTierType = ProductTierType.FREE,
|
||||
accessSecretsManager: Boolean = false,
|
||||
limitCollectionCreation: Boolean = false,
|
||||
limitCollectionDeletion: Boolean = false,
|
||||
allowAdminAccessToAllCollectionItems: Boolean = false,
|
||||
useAccessIntelligence: Boolean = false,
|
||||
useAdminSponsoredFamilies: Boolean = false,
|
||||
useDisableSmAdsForUsers: Boolean = false,
|
||||
isAdminInitiated: Boolean = false,
|
||||
ssoEnabled: Boolean = false,
|
||||
ssoMemberDecryptionType: MemberDecryptionType? = null,
|
||||
usePhishingBlocker: Boolean = false,
|
||||
useMyItems: Boolean = false,
|
||||
): SyncResponseJson.Profile.Organization =
|
||||
SyncResponseJson.Profile.Organization(
|
||||
shouldUsePolicies = shouldUsePolicies,
|
||||
shouldUseKeyConnector = shouldUseKeyConnector,
|
||||
isKeyConnectorEnabled = isKeyConnectorEnabled,
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
type = type,
|
||||
seats = seats,
|
||||
@@ -131,6 +162,36 @@ fun createMockOrganizationNetwork(
|
||||
status = status,
|
||||
userIsClaimedByOrganization = userIsClaimedByOrganization,
|
||||
limitItemDeletion = limitItemDeletion,
|
||||
useSso = useSso,
|
||||
useOrganizationDomains = useOrganizationDomains,
|
||||
shouldUseKeyConnector = shouldUseKeyConnector,
|
||||
useScim = useScim,
|
||||
useCustomPermissions = useCustomPermissions,
|
||||
useResetPassword = useResetPassword,
|
||||
useSecretsManager = useSecretsManager,
|
||||
usePasswordManager = usePasswordManager,
|
||||
useActivateAutofillPolicy = useActivateAutofillPolicy,
|
||||
useAutomaticUserConfirmation = useAutomaticUserConfirmation,
|
||||
ssoBound = ssoBound,
|
||||
resetPasswordEnrolled = resetPasswordEnrolled,
|
||||
organizationUserId = organizationUserId,
|
||||
hasPublicAndPrivateKeys = hasPublicAndPrivateKeys,
|
||||
isProviderUser = isProviderUser,
|
||||
isMember = isMember,
|
||||
familySponsorshipAvailable = familySponsorshipAvailable,
|
||||
productTierType = productTierType,
|
||||
accessSecretsManager = accessSecretsManager,
|
||||
limitCollectionCreation = limitCollectionCreation,
|
||||
limitCollectionDeletion = limitCollectionDeletion,
|
||||
allowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems,
|
||||
useAccessIntelligence = useAccessIntelligence,
|
||||
useAdminSponsoredFamilies = useAdminSponsoredFamilies,
|
||||
useDisableSmAdsForUsers = useDisableSmAdsForUsers,
|
||||
isAdminInitiated = isAdminInitiated,
|
||||
ssoEnabled = ssoEnabled,
|
||||
ssoMemberDecryptionType = ssoMemberDecryptionType,
|
||||
usePhishingBlocker = usePhishingBlocker,
|
||||
useMyItems = useMyItems,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -150,10 +211,30 @@ fun createMockOrganizationKeys(
|
||||
fun createMockPermissions(
|
||||
shouldManageResetPassword: Boolean = false,
|
||||
shouldManagePolicies: Boolean = false,
|
||||
accessEventLogs: Boolean = false,
|
||||
accessImportExport: Boolean = false,
|
||||
accessReports: Boolean = false,
|
||||
createNewCollections: Boolean = false,
|
||||
editAnyCollection: Boolean = false,
|
||||
deleteAnyCollection: Boolean = false,
|
||||
manageGroups: Boolean = false,
|
||||
manageSso: Boolean = false,
|
||||
manageUsers: Boolean = false,
|
||||
manageScim: Boolean = false,
|
||||
): SyncResponseJson.Profile.Permissions =
|
||||
SyncResponseJson.Profile.Permissions(
|
||||
shouldManageResetPassword = shouldManageResetPassword,
|
||||
shouldManagePolicies = shouldManagePolicies,
|
||||
accessEventLogs = accessEventLogs,
|
||||
accessImportExport = accessImportExport,
|
||||
accessReports = accessReports,
|
||||
createNewCollections = createNewCollections,
|
||||
editAnyCollection = editAnyCollection,
|
||||
deleteAnyCollection = deleteAnyCollection,
|
||||
manageGroups = manageGroups,
|
||||
manageSso = manageSso,
|
||||
manageUsers = manageUsers,
|
||||
manageScim = manageScim,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
package com.bitwarden.ui.platform.components.dialog
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.foundation.layout.requiredWidthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight
|
||||
import com.bitwarden.ui.platform.components.dialog.util.maxDialogWidth
|
||||
import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
@@ -36,54 +55,85 @@ fun BitwardenBasicDialog(
|
||||
throwable: Throwable? = null,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
AlertDialog(
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
BitwardenTextButton(
|
||||
label = confirmButtonLabel,
|
||||
onClick = onDismissRequest,
|
||||
modifier = Modifier.testTag(tag = "AcceptAlertButton"),
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.semantics {
|
||||
testTagsAsResourceId = true
|
||||
testTag = "AlertPopup"
|
||||
}
|
||||
.requiredHeightIn(max = configuration.maxDialogHeight)
|
||||
.requiredWidthIn(max = configuration.maxDialogWidth)
|
||||
.background(
|
||||
color = BitwardenTheme.colorScheme.background.primary,
|
||||
shape = BitwardenTheme.shapes.dialog,
|
||||
),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "AlertTitleText")
|
||||
.padding(horizontal = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
}
|
||||
if (scrollState.canScrollBackward) {
|
||||
BitwardenHorizontalDivider()
|
||||
}
|
||||
Text(
|
||||
text = message,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "AlertContentText")
|
||||
.weight(weight = 1f, fill = false)
|
||||
.verticalScroll(state = scrollState)
|
||||
.padding(horizontal = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
dismissButton = throwable
|
||||
?.let { error ->
|
||||
{
|
||||
if (scrollState.canScrollForward) {
|
||||
BitwardenHorizontalDivider()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
throwable?.let { error ->
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.share_error_details),
|
||||
onClick = {
|
||||
intentManager.shareErrorReport(throwable = error)
|
||||
onDismissRequest()
|
||||
},
|
||||
modifier = Modifier.testTag(tag = "ShareErrorDetailsAlertButton"),
|
||||
modifier = Modifier
|
||||
.testTag(tag = "ShareErrorDetailsAlertButton")
|
||||
.padding(horizontal = 4.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
title = title?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
modifier = Modifier.testTag(tag = "AlertTitleText"),
|
||||
BitwardenTextButton(
|
||||
label = confirmButtonLabel,
|
||||
onClick = onDismissRequest,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "AcceptAlertButton")
|
||||
.padding(horizontal = 4.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = message,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
modifier = Modifier.testTag(tag = "AlertContentText"),
|
||||
)
|
||||
},
|
||||
shape = BitwardenTheme.shapes.dialog,
|
||||
containerColor = BitwardenTheme.colorScheme.background.primary,
|
||||
iconContentColor = BitwardenTheme.colorScheme.icon.secondary,
|
||||
titleContentColor = BitwardenTheme.colorScheme.text.primary,
|
||||
textContentColor = BitwardenTheme.colorScheme.text.primary,
|
||||
modifier = Modifier.semantics {
|
||||
testTagsAsResourceId = true
|
||||
testTag = "AlertPopup"
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
76
ui/src/main/res/drawable/ill_autofill.xml
Normal file
76
ui/src/main/res/drawable/ill_autofill.xml
Normal file
@@ -0,0 +1,76 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="201dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="201">
|
||||
<path
|
||||
android:name="secondary"
|
||||
android:fillColor="#AAC3EF"
|
||||
android:pathData="M0,42.67C0,33.46 7.46,26 16.67,26H183.33C192.54,26 200,33.46 200,42.67V159.33C200,168.54 192.54,176 183.33,176H16.67C7.46,176 0,168.54 0,159.33V42.67Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M183.33,30.17H16.67C9.76,30.17 4.17,35.76 4.17,42.67V159.33C4.17,166.24 9.76,171.83 16.67,171.83H183.33C190.24,171.83 195.83,166.24 195.83,159.33V42.67C195.83,35.76 190.24,30.17 183.33,30.17ZM16.67,26C7.46,26 0,33.46 0,42.67V159.33C0,168.54 7.46,176 16.67,176H183.33C192.54,176 200,168.54 200,159.33V42.67C200,33.46 192.54,26 183.33,26H16.67Z" />
|
||||
<path
|
||||
android:name="primary"
|
||||
android:fillColor="#DBE5F6"
|
||||
android:pathData="M18.75,57.25C18.75,54.95 20.62,53.08 22.92,53.08H177.08C179.38,53.08 181.25,54.95 181.25,57.25V82.25C181.25,84.55 179.38,86.42 177.08,86.42H22.92C20.62,86.42 18.75,84.55 18.75,82.25V57.25Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M177.08,57.25H22.92L22.92,82.25H177.08V57.25ZM22.92,53.08C20.62,53.08 18.75,54.95 18.75,57.25V82.25C18.75,84.55 20.62,86.42 22.92,86.42H177.08C179.38,86.42 181.25,84.55 181.25,82.25V57.25C181.25,54.95 179.38,53.08 177.08,53.08H22.92Z" />
|
||||
<path
|
||||
android:name="primary"
|
||||
android:fillColor="#DBE5F6"
|
||||
android:pathData="M18.75,119.75C18.75,117.45 20.62,115.58 22.92,115.58H177.08C179.38,115.58 181.25,117.45 181.25,119.75V144.75C181.25,147.05 179.38,148.92 177.08,148.92H22.92C20.62,148.92 18.75,147.05 18.75,144.75V119.75Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M177.08,119.75H22.92L22.92,144.75H177.08V119.75ZM22.92,115.58C20.62,115.58 18.75,117.45 18.75,119.75V144.75C18.75,147.05 20.62,148.92 22.92,148.92H177.08C179.38,148.92 181.25,147.05 181.25,144.75V119.75C181.25,117.45 179.38,115.58 177.08,115.58H22.92Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M35.43,125.11C36.58,125.11 37.51,126.04 37.51,127.19V130.11L40.24,129.21C41.34,128.86 42.51,129.45 42.87,130.55C43.23,131.64 42.63,132.82 41.54,133.17L38.77,134.08L40.5,136.51C41.17,137.45 40.96,138.75 40.02,139.42C39.08,140.09 37.78,139.87 37.11,138.93L35.43,136.57L33.74,138.93C33.07,139.87 31.77,140.09 30.83,139.42C29.9,138.75 29.68,137.45 30.35,136.51L32.08,134.08L29.31,133.17C28.22,132.82 27.62,131.64 27.98,130.55C28.34,129.45 29.51,128.86 30.61,129.21L33.34,130.11V127.19C33.34,126.04 34.28,125.11 35.43,125.11ZM54.18,125.11C55.33,125.11 56.26,126.04 56.26,127.19V130.11L58.99,129.21C60.09,128.86 61.26,129.45 61.62,130.55C61.98,131.64 61.38,132.82 60.29,133.17L57.52,134.08L59.25,136.51C59.92,137.45 59.71,138.75 58.77,139.42C57.83,140.09 56.53,139.87 55.86,138.93L54.18,136.57L52.49,138.93C51.82,139.87 50.52,140.09 49.58,139.42C48.65,138.75 48.43,137.45 49.1,136.51L50.83,134.08L48.06,133.17C46.97,132.82 46.37,131.64 46.73,130.55C47.09,129.45 48.26,128.86 49.36,129.21L52.09,130.11V127.19C52.09,126.04 53.03,125.11 54.18,125.11ZM72.93,125.11C74.08,125.11 75.01,126.04 75.01,127.19V130.11L77.74,129.21C78.84,128.86 80.01,129.45 80.37,130.55C80.73,131.64 80.13,132.82 79.04,133.17L76.27,134.08L78,136.51C78.67,137.45 78.46,138.75 77.52,139.42C76.58,140.09 75.28,139.87 74.61,138.93L72.93,136.57L71.24,138.93C70.57,139.87 69.27,140.09 68.33,139.42C67.4,138.75 67.18,137.45 67.85,136.51L69.58,134.08L66.81,133.17C65.72,132.82 65.12,131.64 65.48,130.55C65.84,129.45 67.01,128.86 68.11,129.21L70.84,130.11V127.19C70.84,126.04 71.78,125.11 72.93,125.11ZM91.68,125.11C92.83,125.11 93.76,126.04 93.76,127.19V130.11L96.49,129.21C97.59,128.86 98.76,129.45 99.12,130.55C99.48,131.64 98.88,132.82 97.79,133.17L95.02,134.08L96.75,136.51C97.42,137.45 97.21,138.75 96.27,139.42C95.33,140.09 94.03,139.87 93.36,138.93L91.68,136.57L89.99,138.93C89.32,139.87 88.02,140.09 87.08,139.42C86.15,138.75 85.93,137.45 86.6,136.51L88.33,134.08L85.56,133.17C84.47,132.82 83.87,131.64 84.23,130.55C84.59,129.45 85.76,128.86 86.86,129.21L89.59,130.11V127.19C89.59,126.04 90.53,125.11 91.68,125.11ZM110.43,125.11C111.58,125.11 112.51,126.04 112.51,127.19V130.11L115.24,129.21C116.34,128.86 117.51,129.45 117.87,130.55C118.23,131.64 117.63,132.82 116.54,133.17L113.77,134.08L115.5,136.51C116.17,137.45 115.96,138.75 115.02,139.42C114.08,140.09 112.78,139.87 112.11,138.93L110.43,136.57L108.74,138.93C108.07,139.87 106.77,140.09 105.83,139.42C104.9,138.75 104.68,137.45 105.35,136.51L107.08,134.08L104.31,133.17C103.22,132.82 102.62,131.64 102.98,130.55C103.34,129.45 104.51,128.86 105.61,129.21L108.34,130.11V127.19C108.34,126.04 109.28,125.11 110.43,125.11Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M29.17,69.75C29.17,68.6 30.1,67.67 31.25,67.67L114.58,67.67C115.73,67.67 116.67,68.6 116.67,69.75C116.67,70.9 115.73,71.83 114.58,71.83L31.25,71.83C30.1,71.83 29.17,70.9 29.17,69.75Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M170.22,62.03C171.04,62.84 171.04,64.16 170.22,64.97L157.72,77.47C156.91,78.29 155.59,78.29 154.78,77.47L148.53,71.22C147.71,70.41 147.71,69.09 148.53,68.28C149.34,67.46 150.66,67.46 151.47,68.28L156.25,73.05L167.28,62.03C168.09,61.21 169.41,61.21 170.22,62.03Z" />
|
||||
<path
|
||||
android:name="accent"
|
||||
android:fillColor="#FFBF00"
|
||||
android:pathData="M191.67,132.25C191.67,146.06 180.47,157.25 166.67,157.25C152.86,157.25 141.67,146.06 141.67,132.25C141.67,118.44 152.86,107.25 166.67,107.25C180.47,107.25 191.67,118.44 191.67,132.25Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M166.67,153.08C178.17,153.08 187.5,143.76 187.5,132.25C187.5,120.74 178.17,111.42 166.67,111.42C155.16,111.42 145.83,120.74 145.83,132.25C145.83,143.76 155.16,153.08 166.67,153.08ZM166.67,157.25C180.47,157.25 191.67,146.06 191.67,132.25C191.67,118.44 180.47,107.25 166.67,107.25C152.86,107.25 141.67,118.44 141.67,132.25C141.67,146.06 152.86,157.25 166.67,157.25Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M178.56,124.53C179.37,125.34 179.37,126.66 178.56,127.47L163.97,142.06C163.16,142.87 161.84,142.87 161.03,142.06L154.78,135.81C153.96,134.99 153.96,133.67 154.78,132.86C155.59,132.05 156.91,132.05 157.72,132.86L162.5,137.64L175.61,124.53C176.42,123.71 177.74,123.71 178.56,124.53Z" />
|
||||
<path
|
||||
android:name="accent"
|
||||
android:fillColor="#FFBF00"
|
||||
android:pathData="M191.67,69.75C191.67,83.56 180.47,94.75 166.67,94.75C152.86,94.75 141.67,83.56 141.67,69.75C141.67,55.94 152.86,44.75 166.67,44.75C180.47,44.75 191.67,55.94 191.67,69.75Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M166.67,90.58C178.17,90.58 187.5,81.26 187.5,69.75C187.5,58.24 178.17,48.92 166.67,48.92C155.16,48.92 145.83,58.24 145.83,69.75C145.83,81.26 155.16,90.58 166.67,90.58ZM166.67,94.75C180.47,94.75 191.67,83.56 191.67,69.75C191.67,55.94 180.47,44.75 166.67,44.75C152.86,44.75 141.67,55.94 141.67,69.75C141.67,83.56 152.86,94.75 166.67,94.75Z" />
|
||||
<path
|
||||
android:name="outline"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M178.56,62.03C179.37,62.84 179.37,64.16 178.56,64.97L163.97,79.56C163.16,80.37 161.84,80.37 161.03,79.56L154.78,73.31C153.96,72.49 153.96,71.17 154.78,70.36C155.59,69.55 156.91,69.55 157.72,70.36L162.5,75.14L175.61,62.03C176.42,61.21 177.74,61.21 178.56,62.03Z" />
|
||||
</vector>
|
||||
@@ -78,6 +78,7 @@
|
||||
<string name="bitwarden_autofill_service">Bitwarden Autofill Service</string>
|
||||
<string name="change_master_password">Change master password</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="close_app">Close app</string>
|
||||
<string name="continue_text">Continue</string>
|
||||
<string name="create_account">Create account</string>
|
||||
<string name="create_an_account">Create an account</string>
|
||||
@@ -612,6 +613,7 @@ select Add TOTP to store the key safely</string>
|
||||
<string name="forwarded_email_description">Generate an email alias with an external forwarding service.</string>
|
||||
<string name="accessibility_service_disclosure">Accessibility Service Disclosure</string>
|
||||
<string name="accessibility_disclosure_text">Bitwarden uses the Accessibility Service to search for login fields in apps and websites, then establish the appropriate field IDs for entering a username & password when a match for the app or site is found. We do not store any of the information presented to us by the service, nor do we make any attempt to control any on-screen elements beyond text entry of credentials.</string>
|
||||
<string name="accessibility_disclosure_start_up_text">Bitwarden offers an optional autofill method that uses Android’s Accessibility Service to detect login fields in apps and websites. If you choose to enable it, Bitwarden will identify the appropriate fields and enter your credentials when a match is found. We do not store any information observed by the service, and we do not control any on-screen elements beyond credential entry.</string>
|
||||
<string name="accept">Accept</string>
|
||||
<string name="decline">Decline</string>
|
||||
<string name="login_request_has_already_expired">Login request has already expired.</string>
|
||||
|
||||
Reference in New Issue
Block a user