mirror of
https://github.com/bitwarden/android.git
synced 2026-10-23 03:23:53 -05:00
Merge branch 'main' into QA-1126b/adding-native-sanity-test
This commit is contained in:
21 files changed
+575
-35
No files matched your search
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+26
-6
@@ -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,
|
||||
|
||||
+17
-4
@@ -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
|
||||
|
||||
+17
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
+20
-5
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+13
@@ -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(),
|
||||
|
||||
+1
@@ -881,4 +881,5 @@ private val DEFAULT_STATE = AddEditSendState(
|
||||
baseWebSendUrl = "https://vault.bitwarden.com/#/send/",
|
||||
policyDisablesSend = false,
|
||||
sendType = SendItemType.TEXT,
|
||||
isPremium = true,
|
||||
)
|
||||
+67
@@ -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),
|
||||
)
|
||||
+44
-2
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user