Merge branch 'main' into QA-1126b/adding-native-sanity-test

This commit is contained in:
ifernandezdiaz authored and GitHub committed 2025-07-21 15:19:39 -03:00
commit be325eb43c
21 files changed
+575 -35

No files matched your search

+137 -7
View File
@@ -1,16 +1,146 @@
name: Publish
name: Publish to Google Play
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
on:
workflow_dispatch:
inputs:
product:
description: "Which app is being released."
type: choice
options:
- Password Manager
- Authenticator
version-name:
description: "Version name to promote to production ex 2025.1.1"
type: string
version-code:
description: "Build number to promote to production."
required: true
type: string
rollout-percentage:
description: "Percentage of users who will receive this version update."
required: true
type: choice
options:
- 10%
- 30%
- 50%
- 100%
default: 10%
release-notes:
description: "Change notes to be included with this release."
type: string
default: "Bug fixes."
required: true
track-from:
description: "Track to promote from."
type: choice
options:
- internal
- Fastlane Automation Source
required: true
default: "internal"
track-target:
description: "Track to promote to."
type: choice
options:
- production
- Fastlane Automation Target
required: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
permissions: {}
permissions:
contents: read
packages: read
jobs:
publish:
promote:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
steps:
- name: TEST STEP
run: exit 0
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/app/src/standardRelease
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
- name: Format Release Notes
run: |
FORMATTED_MESSAGE="$(echo "${{ inputs.release-notes }}" | sed 's/ /\n/g')"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Promote Play Store version to production
env:
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_BETA_KEYSTORE_PASSWORD }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_BETA_KEY_PASSWORD }}
VERSION_CODE_INPUT: ${{ inputs.version-code }}
VERSION_NAME: ${{inputs.version-name}}
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
PRODUCT: ${{ inputs.product }}
TRACK_FROM: ${{ inputs.track-from }}
TRACK_TARGET: ${{ inputs.track-target }}
run: |
if [ "$PRODUCT" = "Password Manager" ]; then
PACKAGE_NAME="com.x8bit.bitwarden"
elif [ "$PRODUCT" = "Authenticator" ]; then
PACKAGE_NAME="com.bitwarden.authenticator"
else
echo "Unsupported product: $PRODUCT"
exit 1
fi
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
bundle exec fastlane updateReleaseNotes \
releaseNotes:"$RELEASE_NOTES" \
versionCode:"$VERSION_CODE"
bundle exec fastlane promoteToProduction \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME" \
rolloutPercentage:"$decimal" \
packageName:"$PACKAGE_NAME" \
releaseNotes:"$RELEASE_NOTES" \
track:"$TRACK_FROM" \
trackPromoteTo:"$TRACK_TARGET"
+6 -6
View File
@@ -11,8 +11,8 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1125.0)
aws-sdk-core (3.226.2)
aws-partitions (1.1131.0)
aws-sdk-core (3.226.3)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -22,7 +22,7 @@ GEM
aws-sdk-kms (1.106.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.192.0)
aws-sdk-s3 (1.193.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -167,13 +167,13 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.12.2)
json (2.13.0)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multi_json (1.17.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
@@ -181,7 +181,7 @@ GEM
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
ostruct (0.6.1)
ostruct (0.6.3)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.3.0)
+6 -1
View File
@@ -67,7 +67,12 @@ android {
buildConfigField(
type = "String",
name = "CI_INFO",
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}",
value = "${ciProperties.getOrDefault("ci.info", "\"\uD83D\uDCBB local\"")}",
)
buildConfigField(
type = "String",
name = "SDK_VERSION",
value = "\"${libs.versions.bitwardenSdk.get()}\"",
)
}
@@ -73,7 +73,7 @@ class MainActivity : AppCompatActivity() {
var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
super.onCreate(savedInstanceState)
window.decorView.filterTouchesWhenObscured = true
if (savedInstanceState == null) {
mainViewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = intent))
}
@@ -21,6 +21,11 @@ val isDevBuild: Boolean
val versionData: String
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
/**
* A string that represents a displayable SDK version.
*/
val sdkData: String get() = BuildConfig.SDK_VERSION
/**
* A string that represents device data.
*/
@@ -340,6 +340,8 @@ private fun AboutScreenContent_preview() {
AboutScreenContent(
state = AboutState(
version = "Version: 1.0.0 (1)".asText(),
sdkVersion = "\uD83E\uDD80 SDK: 1.0.0-20250708.105256-238".asText(),
serverData = "\uD83C\uDF29 Server: 2025.7.1 @ US".asText(),
deviceData = "device_data".asText(),
ciData = "ci_data".asText(),
isSubmitCrashLogsEnabled = false,
@@ -3,12 +3,12 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.about
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.concat
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.util.ciBuildInfo
import com.x8bit.bitwarden.data.platform.util.deviceData
import com.x8bit.bitwarden.data.platform.util.isFdroid
import com.x8bit.bitwarden.data.platform.util.sdkData
import com.x8bit.bitwarden.data.platform.util.versionData
import com.x8bit.bitwarden.ui.platform.feature.settings.about.util.getStopsLoggingStringForActiveLog
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -43,10 +44,22 @@ class AboutViewModel @Inject constructor(
private val logsManager: LogsManager,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
serverConfigRepository: ServerConfigRepository,
) : BaseViewModel<AboutState, AboutEvent, AboutAction>(
initialState = savedStateHandle[KEY_STATE]
?: AboutState(
version = R.string.version.asText().concat(": $versionData".asText()),
initialState = savedStateHandle[KEY_STATE] ?: run {
val serverData = serverConfigRepository.serverConfigStateFlow.value?.serverData
AboutState(
version = "Version: $versionData".asText(),
sdkVersion = "\uD83E\uDD80 SDK: $sdkData".asText(),
serverData = StringBuilder()
.append("\uD83C\uDF29 Server:")
.apply {
serverData?.server?.name?.let { append(" $it") }
serverData?.version?.let { append(" $it") }
serverData?.environment?.cloudRegion?.let { append(" @ $it") }
}
.toString()
.asText(),
deviceData = deviceData.asText(),
ciData = ciBuildInfo?.let { "\n$it" }.orEmpty().asText(),
isSubmitCrashLogsEnabled = logsManager.isEnabled,
@@ -58,7 +71,8 @@ class AboutViewModel @Inject constructor(
.flightRecorderData
.getStopsLoggingStringForActiveLog(clock = clock),
copyrightInfo = "© Bitwarden Inc. 2015-${Year.now(clock).value}".asText(),
),
)
},
) {
init {
stateFlow
@@ -136,7 +150,11 @@ class AboutViewModel @Inject constructor(
.concat(state.version)
.concat("\n".asText())
.concat(state.deviceData)
.concat(state.ciData),
.concat(state.ciData)
.concat("\n".asText())
.concat(state.sdkVersion)
.concat("\n".asText())
.concat(state.serverData),
)
}
@@ -171,6 +189,8 @@ class AboutViewModel @Inject constructor(
@Parcelize
data class AboutState(
val version: Text,
val sdkVersion: Text,
val serverData: Text,
val deviceData: Text,
val ciData: Text,
val isSubmitCrashLogsEnabled: Boolean,
@@ -119,7 +119,7 @@ class IntentManagerImpl(
try {
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
.apply {
data = Uri.parse("package:${context.packageName}")
data = "package:${context.packageName}".toUri()
}
context.startActivity(intent)
true
@@ -129,7 +129,7 @@ class IntentManagerImpl(
override fun startApplicationDetailsSettingsActivity() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:" + context.packageName)
intent.data = "package:${context.packageName}".toUri()
startActivity(intent = intent)
}
@@ -210,8 +210,21 @@ class IntentManagerImpl(
return if (uri != null) getLocalFileData(uri) else getCameraFileData()
}
override fun getFileDataFromIntent(intent: Intent): IntentManager.FileData? =
intent.clipData?.getItemAt(0)?.uri?.let { getLocalFileData(it) }
override fun getFileDataFromIntent(
intent: Intent,
): IntentManager.FileData? = intent
.clipData
?.getItemAt(0)
?.uri
?.takeUnless { uri ->
val uriString = uri.toString()
context
.packageManager
.getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_PROVIDERS)
.providers
?.any { uriString.contains(other = it.authority) } == true
}
?.let { getLocalFileData(it) }
override fun getShareDataFromIntent(intent: Intent): IntentManager.ShareData? {
if (intent.action != Intent.ACTION_SEND) return null
@@ -15,6 +15,7 @@ import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.concat
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
@@ -65,6 +66,7 @@ private const val MAX_FILE_SIZE_BYTES: Long = 100 * 1024 * 1024
@HiltViewModel
class AddEditSendViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
authRepo: AuthRepository,
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val environmentRepo: EnvironmentRepository,
@@ -131,6 +133,7 @@ class AddEditSendViewModel @Inject constructor(
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(),
isPremium = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
)
},
) {
@@ -499,6 +502,19 @@ class AddEditSendViewModel @Inject constructor(
}
(content.selectedType as? AddEditSendState.ViewState.Content.SendType.File)
?.let { fileType ->
if (!state.isPremium) {
// We should never get here without a premium account, but we do one last
// check just in case.
mutableStateFlow.update {
it.copy(
dialogState = AddEditSendState.DialogState.Error(
title = R.string.send.asText(),
message = R.string.send_file_premium_required.asText(),
),
)
}
return@onContent
}
if (fileType.name.isNullOrBlank()) {
mutableStateFlow.update {
it.copy(
@@ -686,6 +702,7 @@ data class AddEditSendState(
val isShared: Boolean,
val baseWebSendUrl: String,
val policyDisablesSend: Boolean,
val isPremium: Boolean,
) : Parcelable {
/**
@@ -838,11 +838,26 @@ class VaultItemListingViewModel @Inject constructor(
}
is VaultItemListingState.ItemListingType.Send -> {
sendEvent(
VaultItemListingEvent.NavigateToAddSendItem(
sendType = itemListingType.toSendItemType(),
),
)
when (val sendType = itemListingType.toSendItemType()) {
SendItemType.FILE -> {
if (state.isPremium) {
sendEvent(VaultItemListingEvent.NavigateToAddSendItem(sendType))
} else {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Error(
title = R.string.send.asText(),
message = R.string.send_file_premium_required.asText(),
),
)
}
}
}
SendItemType.TEXT -> {
sendEvent(VaultItemListingEvent.NavigateToAddSendItem(sendType))
}
}
}
}
}
-1
View File
@@ -68,7 +68,6 @@
<string name="value_has_been_copied">%1$s copied</string>
<string name="verify_master_password">Verify master password</string>
<string name="verify_pin">Verify PIN</string>
<string name="version">Version</string>
<string name="view">View</string>
<string name="yes">Yes</string>
<string name="account">Account</string>
@@ -324,6 +324,8 @@ class AboutScreenTest : BitwardenComposeTest() {
private val DEFAULT_STATE = AboutState(
version = "Version: 1.0.0 (1)".asText(),
sdkVersion = "\uD83E\uDD80 SDK: 1.0.0-20250708.105256-238".asText(),
serverData = "\uD83C\uDF29 Server: 2025.7.1 @ US".asText(),
deviceData = "device_data".asText(),
ciData = "ci_data".asText(),
isSubmitCrashLogsEnabled = false,
@@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.about
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.data.datasource.disk.model.ServerConfig
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.util.asText
@@ -45,6 +47,10 @@ class AboutViewModelTest : BaseViewModelTest() {
every { flightRecorderDataFlow } returns mutableFlightRecorderFlow
every { flightRecorderData } returns FlightRecorderDataSet(emptySet())
}
private val mutableServerConfigStateFlow = MutableStateFlow<ServerConfig?>(null)
private val serverConfigRepository: ServerConfigRepository = mockk {
every { serverConfigStateFlow } returns mutableServerConfigStateFlow
}
@AfterEach
fun tearDown() {
@@ -185,6 +191,10 @@ class AboutViewModelTest : BaseViewModelTest() {
.concat("\n".asText())
.concat(state.deviceData)
.concat(state.ciData)
.concat("\n".asText())
.concat(state.sdkVersion)
.concat("\n".asText())
.concat(state.serverData)
every { clipboardManager.setText(expectedText, true, null) } just runs
@@ -222,6 +232,7 @@ class AboutViewModelTest : BaseViewModelTest() {
environmentRepository = environmentRepository,
logsManager = logsManager,
settingsRepository = settingsRepository,
serverConfigRepository = serverConfigRepository,
)
}
@@ -231,6 +242,8 @@ private val FIXED_CLOCK = Clock.fixed(
)
private val DEFAULT_ABOUT_STATE: AboutState = AboutState(
version = "Version: <version_name> (<version_code>)".asText(),
sdkVersion = "\uD83E\uDD80 SDK: 1.0.0-20250708.105256-238".asText(),
serverData = "\uD83C\uDF29 Server: 2025.7.1".asText(),
deviceData = "<device_data>".asText(),
ciData = "<ci_info>".asText(),
copyrightInfo = "© Bitwarden Inc. 2015-${Year.now(FIXED_CLOCK).value}".asText(),
@@ -881,4 +881,5 @@ private val DEFAULT_STATE = AddEditSendState(
baseWebSendUrl = "https://vault.bitwarden.com/#/send/",
policyDisablesSend = false,
sendType = SendItemType.TEXT,
isPremium = true,
)
@@ -12,10 +12,14 @@ import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
@@ -66,6 +70,10 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
private val clipboardManager: BitwardenClipboardManager = mockk {
every { setText(any<String>(), toastDescriptorOverride = any<Text>()) } just runs
}
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val authRepository = mockk<AuthRepository> {
every { userStateFlow } returns mutableUserStateFlow
}
private val environmentRepository: EnvironmentRepository = mockk {
every { environment } returns Environment.Us
}
@@ -386,6 +394,38 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `SaveClick for file without premium show error dialog`() {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(DEFAULT_ACCOUNT.copy(isPremium = false)),
)
val initialState = DEFAULT_STATE.copy(
isPremium = false,
viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(name = "test"),
selectedType = AddEditSendState.ViewState.Content.SendType.File(
uri = null,
name = null,
displaySize = null,
sizeBytes = null,
),
),
)
val viewModel = createViewModel(initialState)
viewModel.trySendAction(AddEditSendAction.SaveClick)
assertEquals(
initialState.copy(
dialogState = AddEditSendState.DialogState.Error(
title = R.string.send.asText(),
message = R.string.send_file_premium_required.asText(),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `SaveClick with file missing should show error dialog`() {
val initialState = DEFAULT_STATE.copy(
@@ -966,6 +1006,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
toAddEditSendArgs()
} returns AddEditSendArgs(sendType = sendType, addEditSendType = addEditSendType)
},
authRepo = authRepository,
environmentRepo = environmentRepository,
specialCircumstanceManager = specialCircumstanceManager,
clock = clock,
@@ -1013,4 +1054,30 @@ private val DEFAULT_STATE = AddEditSendState(
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
policyDisablesSend = false,
sendType = SendItemType.TEXT,
isPremium = true,
)
private val DEFAULT_ACCOUNT = UserState.Account(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
isBiometricsEnabled = false,
organizations = emptyList(),
needsMasterPassword = false,
trustedDevice = null,
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = FirstTimeState(showImportLoginsCard = true),
)
private val DEFAULT_USER_STATE = UserState(
activeUserId = "activeUserId",
accounts = listOf(DEFAULT_ACCOUNT),
)
@@ -1422,7 +1422,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
@Test
fun `AddVaultItemClick for send item should emit NavigateToAddVaultItem`() = runTest {
fun `AddVaultItemClick for text send item should emit NavigateToAddVaultItem`() = runTest {
val viewModel = createVaultItemListingViewModel(
createSavedStateHandleWithVaultItemListingType(VaultItemListingType.SendText),
)
@@ -1435,6 +1435,47 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `AddVaultItemClick for file send item with premium should emit NavigateToAddVaultItem`() =
runTest {
val viewModel = createVaultItemListingViewModel(
createSavedStateHandleWithVaultItemListingType(VaultItemListingType.SendFile),
)
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
assertEquals(
VaultItemListingEvent.NavigateToAddSendItem(sendType = SendItemType.FILE),
awaitItem(),
)
}
}
@Test
fun `AddVaultItemClick for file send item without premium should display error dialog`() =
runTest {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(DEFAULT_ACCOUNT.copy(isPremium = false)),
)
val viewModel = createVaultItemListingViewModel(
createSavedStateHandleWithVaultItemListingType(VaultItemListingType.SendFile),
)
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
expectNoEvents()
}
assertEquals(
createVaultItemListingState(
itemListingType = VaultItemListingState.ItemListingType.Send.SendFile,
dialogState = VaultItemListingState.DialogState.Error(
title = R.string.send.asText(),
message = R.string.send_file_premium_required.asText(),
),
isPremium = false,
),
viewModel.stateFlow.value,
)
}
@Test
fun `ItemTypeToAddSelected sends NavigateToAddFolder for folder selection`() = runTest {
val viewModel = createVaultItemListingViewModel(
@@ -5331,6 +5372,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
itemListingType: VaultItemListingState.ItemListingType = VaultItemListingState.ItemListingType.Vault.Login,
viewState: VaultItemListingState.ViewState = VaultItemListingState.ViewState.Loading,
dialogState: VaultItemListingState.DialogState? = null,
isPremium: Boolean = true,
): VaultItemListingState =
VaultItemListingState(
itemListingType = itemListingType,
@@ -5348,7 +5390,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
policyDisablesSend = false,
hasMasterPassword = true,
createCredentialRequest = null,
isPremium = true,
isPremium = isPremium,
isRefreshing = false,
restrictItemTypesPolicyOrgIds = persistentListOf(),
)
+73
View File
@@ -13,6 +13,9 @@
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
require_relative 'patches/supply_custom_promote_config'
require_relative 'patches/supply_custom_promote'
default_platform(:android)
platform :android do
@@ -418,4 +421,74 @@ platform :android do
mapping: "authenticator/build/outputs/mapping/release/mapping.txt",
)
end
desc "Retrieve build from Github releases"
lane :retrieveBuildFromGithub do |options|
version_codes = Actions.lane_context[SharedValues::GOOGLE_PLAY_TRACK_VERSION_CODES]
UI.message("Version codes in beta track: #{version_codes}")
end
desc "Update release notes for all locales."
lane :updateReleaseNotes do |options|
changelog = options[:releaseNotes]
version_code = options[:versionCode]
auth_locales = ["en-US"]
pw_manager_locales = ["ca", "cs-CZ", "da-DK", "de-DE", "en-US", "es-ES", "et", "fr-FR", "hr", "hu-HU", "id", "it-IT", "iw-IL", "ja-JP", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro", "ru-RU", "sk", "sv-SE", "tr-TR", "uk", "vi", "zh-CN", "zh-TW"]
if options[:packageName] == "com.bitwarden.authenticator"
locales = auth_locales
else
locales = pw_manager_locales
end
locales.each do |locale|
dir = "metadata/android/#{locale}/changelogs"
FileUtils.mkdir_p(dir)
File.write("#{dir}/#{version_code}.txt", changelog)
end
end
desc "Promote to production."
lane :promoteToProduction do |options|
release_options = {
package_name: options[:packageName],
version_code: options[:versionCode].to_i,
version_name: options[:versionName],
track: options[:track],
track_promote_to: options[:trackPromoteTo],
skip_release_verification: true,
skip_upload_apk: true,
skip_upload_aab: true,
}
if options[:releaseNotes].nil? or options[:releaseNotes].to_s.empty?
release_options[:skip_upload_metadata] = true
else
release_options[:skip_upload_metadata] = false
end
if options[:rolloutPercentage].to_f < 1
release_options[:track_promote_release_status] = "inProgress"
release_options[:rollout] = options[:rolloutPercentage]
else
release_options[:release_status] = "completed"
end
begin
UI.message("🚀 Starting release to #{options[:trackPromoteTo]}...")
supply(release_options)
rescue => error
message = error.to_s
if message.include?("You cannot rollout this release because it does not allow any existing users to upgrade to the newly added APKs")
UI.error("❌ App Store Error: Cannot rollout release because no existing users can upgrade to the new build.")
UI.error("This might mean the version is older than what is currently available on the track, pick a newer build.")
else
UI.error("❌ Unexpected error during release: #{message}")
end
raise
end
end
end
+97
View File
@@ -0,0 +1,97 @@
# Patch Description:
# Fixes issue where Fastlane 'Supply' doesn't recognize previous builds
# when promoting to another track.
#
# Source: https://github.com/artsy/eigen/pull/10262
# Author: Brian Beckerle (@brainbicycle)
#
module Supply
class Uploader
alias_method :original_promote_track, :promote_track
def promote_track
if Supply.config[:skip_release_verification]
custom_promote_track
else
original_promote_track
end
end
def custom_promote_track
UI.message("Using custom promotion logic")
track_from = client.tracks(Supply.config[:track]).first
unless track_from
UI.user_error!("Cannot promote from track '#{Supply.config[:track]}' - track doesn't exist")
end
releases = track_from.releases
UI.message("Track contents: #{track_from.to_json}")
version_code = Supply.config[:version_code].to_s
if !Supply.config[:skip_release_verification]
if version_code != ""
releases = releases.select do |release|
release.version_codes.include?(version_code)
end
else
releases = releases.select do |release|
release.status == Supply.config[:release_status]
end
end
if releases.size == 0
if version_code != ""
UI.user_error!("Cannot find release with version code '#{version_code}' to promote in track '#{Supply.config[:track]}'")
else
UI.user_error!("Track '#{Supply.config[:track]}' doesn't have any releases")
end
elsif releases.size > 1
UI.user_error!("Track '#{Supply.config[:track]}' has more than one release - use :version_code to filter the release to promote")
end
else
UI.message("Skipping release verification as per configuration.")
if version_code == ""
UI.user_error!("Must provide a version code when release verification is skipped.")
end
if Supply.config[:version_name].nil?
UI.user_error!("To force promote a :version_code, it is mandatory to enter the :version_name")
end
release = AndroidPublisher::TrackRelease.new(
name: Supply.config[:version_name],
version_codes: [version_code],
status: Supply.config[:track_promote_release_status] || Supply::ReleaseStatus::COMPLETED
)
end
release = releases.first unless Supply.config[:skip_release_verification]
track_to = client.tracks(Supply.config[:track_promote_to]).first || AndroidPublisher::Track.new(
track: Supply.config[:track_promote_to],
releases: []
)
rollout = (Supply.config[:rollout] || 0).to_f
if rollout > 0 && rollout < 1
release.status = Supply::ReleaseStatus::IN_PROGRESS
release.user_fraction = rollout
else
release.status = Supply.config[:track_promote_release_status]
release.user_fraction = nil
end
if track_to
# Its okay to set releases to an array containing the newest release
# Google Play will keep previous releases there this release is a partial rollout
track_to.releases = [release]
else
track_to = AndroidPublisher::Track.new(
track: Supply.config[:track_promote_to],
releases: [release]
)
end
client.update_track(Supply.config[:track_promote_to], track_to)
UI.message("confirmed that update_track was reached: #{Supply.config[:track_promote_to]} #{release}")
end
end
end
@@ -0,0 +1,38 @@
# Patch Description:
# Fixes issue where Fastlane 'Supply' doesn't recognize previous builds
# when promoting to another track.
#
# Source: https://github.com/artsy/eigen/pull/10262
# Author: Brian Beckerle (@brainbicycle)
#
module Supply
class Options
class << self
alias_method :original_available_options, :available_options
def available_options
original_options = original_available_options
custom_options = [
FastlaneCore::ConfigItem.new(
key: :skip_release_verification,
env_name: "SUPPLY_SKIP_RELEASE_VERIFICATION",
description: "If set to true, skips checking if the version code exists in the track before promoting",
type: Boolean,
default_value: false,
optional: true
)
]
# Only add custom options if they aren't already present
custom_options.each do |custom_option|
unless original_options.any? { |option| option.key == custom_option.key }
original_options << custom_option
end
end
original_options
end
end
end
end
@@ -45,14 +45,14 @@ fun String?.orZeroWidthSpace(): String = this.orNullIfBlank() ?: ZERO_WIDTH_CHAR
*
* This validates that the email is valid by asserting that:
* * The string starts with a string of characters including periods, underscores, percent symbols,
* plus's, minus's, forward slash's, and alphanumeric characters.
* plus's, minus's, forward slash's, asterisks, and alphanumeric characters.
* * Followed by an '@' symbol.
* * Followed by a string of characters including periods, minus's, and alphanumeric characters.
* * Followed by a period.
* * Followed by at least 2 more alphanumeric characters.
*/
fun String.isValidEmail(): Boolean =
this.matches(regex = "^[A-Za-z0-9._%+-/]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex())
this.matches(regex = "^[A-Za-z0-9._%+-/*]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex())
/**
* Returns `true` if the given [String] is a non-blank, valid URI and `false` otherwise.
@@ -28,6 +28,7 @@ class StringExtensionsTest {
"test.test@test.test.com" to true,
"test/test@test.test.com" to true,
"test.test@test/test.com" to false,
"test*test@test.com" to true,
)
invalidEmails.forEach {
assertEquals(it.first.isValidEmail(), it.second)