diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b52400dcf6..408d341ff0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,6 +76,9 @@ jobs: needs: - check runs-on: ubuntu-22.04 + strategy: + matrix: + variant: [ "aab", "apk" ] steps: - name: Checkout uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 @@ -169,6 +172,7 @@ jobs: shell: bash - name: Assemble Release APK + if: ${{ matrix.variant == 'apk' }} run: | bundle exec fastlane buildRelease \ storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \ @@ -178,11 +182,13 @@ jobs: shell: bash - name: Create checksum file for Release APK + if: ${{ matrix.variant == 'apk' }} run: | sha256sum "app/build/outputs/apk/release/com.bitwarden.authenticator-release.apk" \ > ./authenticator-android-apk-sha256.txt - name: Upload release APK to GitHub + if: ${{ matrix.variant == 'apk' }} uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: com.bitwarden.authenticator.apk @@ -190,6 +196,7 @@ jobs: if-no-files-found: error - name: Upload checksum file for Release .apk + if: ${{ matrix.variant == 'apk' }} uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: authenticator-android-apk-sha256.txt @@ -201,10 +208,61 @@ jobs: run: bundle exec fastlane add_plugin firebase_app_distribution - name: Publish release APK to Firebase - if: ${{ github.ref_name == 'main' }} + if: ${{ github.ref_name == 'main' && matrix.variant == 'apk' }} env: FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json run: | bundle exec fastlane distributeReleaseToFirebase \ serviceCredentialsFile:${{ env.FIREBASE_CREDS_PATH }} shell: bash + + - name: Bundle release AAB + if: ${{ matrix.variant == 'aab' }} + run: | + bundle exec fastlane bundleRelease \ + storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \ + storePassword:'${{ secrets.AAB_KEYSTORE_STORE_PASSWORD }}' \ + keyAlias:authenticatorupload \ + keyPassword:'${{ secrets.AAB_KEYSTORE_KEY_PASSWORD }}' + shell: bash + + - name: Create checksum file for Release AAB + if: ${{ matrix.variant == 'aab' }} + run: | + sha256sum "app/build/outputs/bundle/release/com.bitwarden.authenticator-release.aab" \ + > ./authenticator-android-aab-sha256.txt + shell: bash + + - name: Upload release AAB to GitHub + if: ${{ matrix.variant == 'aab' }} + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: com.bitwarden.authenticator.aab + path: app/build/outputs/bundle/release/com.bitwarden.authenticator-release.aab + if-no-files-found: error + + - name: Upload checksum file for Release .aab + if: ${{ matrix.variant == 'aab' }} + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: authenticator-android-aab-sha256.txt + path: ./authenticator-android-aab-sha256.txt + if-no-files-found: error + + - name: Publish release AAB to Firebase + if: ${{ github.ref_name == 'main' && matrix.variant == 'aab' }} + env: + FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json + run: | + bundle exec fastlane distributeReleaseBundleToFirebase \ + serviceCredentialsFile:${{ env.FIREBASE_CREDS_PATH }} + shell: bash + +# - name: Publish release AAB to Google Play Store +# if: ${{ github.ref_name == 'main' && matrix.variant == 'aab'}} +# env: +# PLAY_STORE_CREDS_FILE: ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json +# run: | +# bundle exec fastlane publishReleaseToGooglePlayStore \ +# serviceCredentialsFile:${{ env.PLAY_STORE_CREDS_FILE }} \ +# shell: bash diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 962c75e82a..1ea79754fe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -61,7 +61,7 @@ android { } packaging { resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" + excludes += "/META-INF/{AL2.0,LGPL2.1,LICENSE*.md}" } } } @@ -124,4 +124,6 @@ dependencies { testImplementation(libs.robolectric.robolectric) testImplementation(libs.square.okhttp.mockwebserver) testImplementation(libs.square.turbine) + + androidTestImplementation(libs.bundles.tests.instrumented) } diff --git a/app/src/androidTest/kotlin/com/x8bit/bitwarden/android/authenticator/ExampleInstrumentedTest.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/android/authenticator/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..7504cc9635 --- /dev/null +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/android/authenticator/ExampleInstrumentedTest.kt @@ -0,0 +1,136 @@ +package com.x8bit.bitwarden.android.authenticator + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme +import com.bitwarden.authenticator.ui.platform.feature.tutorial.TutorialScreen +import com.bitwarden.authenticator.ui.platform.feature.tutorial.TutorialViewModel +import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme +import org.junit.ClassRule +import org.junit.Rule +import org.junit.Test +import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.locale.LocaleTestRule + +class ExampleInstrumentedTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun screenshotTutorialSlides_Dark() { + val viewModel = TutorialViewModel() + + composeTestRule.setContent { + AuthenticatorTheme(theme = AppTheme.DARK) { + TutorialScreen( + viewModel = viewModel, + onTutorialFinished = {}, + ) + } + } + + composeTestRule.waitForIdle() + + composeTestRule + .onNodeWithText("Continue") + .isDisplayed() + + composeTestRule + .onNodeWithText("Secure your accounts with Bitwarden Authenticator") + .isDisplayed() + + Screengrab.screenshot("IntroSlide_Dark") + + composeTestRule + .onNodeWithText("Continue") + .performClick() + + composeTestRule + .onNodeWithText("Use your device camera to scan codes") + .assertIsDisplayed() + + Screengrab.screenshot("QrCodeSlide_Dark") + + composeTestRule + .onNodeWithText("Continue") + .performClick() + + composeTestRule + .onNodeWithText("Sign in using unique codes") + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Continue") + .assertDoesNotExist() + + composeTestRule + .onNodeWithText("Get started") + .isDisplayed() + + Screengrab.screenshot("UniqueCodesSlide_Dark") + } + + @Test + fun screenshotTutorialSlides_Light() { + val viewModel = TutorialViewModel() + + composeTestRule.setContent { + AuthenticatorTheme(theme = AppTheme.LIGHT) { + TutorialScreen( + viewModel = viewModel, + onTutorialFinished = {}, + ) + } + } + + composeTestRule.waitForIdle() + + composeTestRule + .onNodeWithText("Continue") + .isDisplayed() + + composeTestRule + .onNodeWithText("Secure your accounts with Bitwarden Authenticator") + .isDisplayed() + + Screengrab.screenshot("IntroSlide_Light") + + composeTestRule + .onNodeWithText("Continue") + .performClick() + + composeTestRule + .onNodeWithText("Use your device camera to scan codes") + .assertIsDisplayed() + + Screengrab.screenshot("QrCodeSlide_Light") + + composeTestRule + .onNodeWithText("Continue") + .performClick() + + composeTestRule + .onNodeWithText("Sign in using unique codes") + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Continue") + .assertDoesNotExist() + + composeTestRule + .onNodeWithText("Get started") + .isDisplayed() + + Screengrab.screenshot("UniqueCodesSlide_Light") + } + + companion object { + @JvmField + @ClassRule + val localeTestRule: LocaleTestRule = LocaleTestRule() + } +} diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index e1b3fd96c6..5ba84656bd 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -2,6 +2,14 @@ + + + + + + options[:storeFile], + "android.injected.signing.store.password" => options[:storePassword], + "android.injected.signing.key.alias" => options[:keyAlias], + "android.injected.signing.key.password" => options[:keyPassword] + }, + print_command: false, + ) + end + desc "Publish release to Firebase" - lane :distributeReleaseToFirebase do |options| + lane :distributeReleaseToFirebase do |options| + release_notes = changelog_from_git_commits( + commits_count: 1, + pretty: "- %s" + ) + + puts "Release notes #{release_notes}" + + firebase_app_distribution( + app: "1:867301491091:android:50b626dba42a361651e866", + android_artifact_type: "APK", + android_artifact_path: "app/build/outputs/apk/release/com.bitwarden.authenticator-release.apk", + service_credentials_file: options[:serviceCredentialsFile], + groups: "internal-prod-group", + release_notes: release_notes, + ) + end + + desc "Publish release AAB to Firebase" + lane :distributeReleaseBundleToFirebase do |options| release_notes = changelog_from_git_commits( commits_count: 1, pretty: "- %s" @@ -105,11 +139,21 @@ platform :android do firebase_app_distribution( app: "1:867301491091:android:50b626dba42a361651e866", - android_artifact_type: "APK", - android_artifact_path: "app/build/outputs/apk/release/com.bitwarden.authenticator-release.apk", + android_artifact_type: "AAB", + android_artifact_path: "app/build/outputs/bundle/release/com.bitwarden.authenticator-release.aab", service_credentials_file: options[:serviceCredentialsFile], groups: "internal-prod-group", release_notes: release_notes, ) end + + desc "Publish release to Google Play Store" + lane :publishReleaseToGooglePlayStore do |options| + upload_to_play_store( + json_key: options[:serviceCredentialsFile], + track: "internal", + aab: "app/build/outputs/bundle/release/com.bitwarden.authenticator-release.aab", + mapping: "app/build/outputs/mapping/release/mapping.txt", + ) + end end diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1531b6c3f..e85c0df685 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ androidXBiometrics = "1.2.0-alpha05" androidxBrowser = "1.8.0" androidxCamera = "1.3.2" androidxComposeBom = "2024.04.01" +androidxComposeUiTest = "1.6.6" androidxCore = "1.12.0" androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.7.0" @@ -20,11 +21,14 @@ androidxNavigation = "2.7.7" androidxRoom = "2.6.1" androidXSecurityCrypto = "1.1.0-alpha06" androidxSplash = "1.1.0-rc01" +androidxTest = "1.5.0" androidXAppCompat = "1.6.1" androdixAutofill = "1.1.0" androidxWork = "2.9.0" bitwardenSdk = "0.4.0-20240314.115913-173" crashlytics = "2.9.9" +espresso = "3.5.1" +fastlaneScreengrab = "2.1.1" firebaseBom = "32.8.0" glide = "1.0.0-beta01" googleServices = "4.4.1" @@ -67,7 +71,7 @@ androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } -androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidxComposeUiTest" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } @@ -82,9 +86,13 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" } androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidXSecurityCrypto" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTest" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTest" } +androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" } bitwarden-sdk = { module = "com.bitwarden:sdk-android", version.ref = "bitwardenSdk" } bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" } +fastlane-screengrab = { module = "tools.fastlane:screengrab", version.ref = "fastlaneScreengrab"} google-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } google-firebase-cloud-messaging = { module = "com.google.firebase:firebase-messaging-ktx" } google-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } @@ -98,6 +106,7 @@ kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collec kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } mockk-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } robolectric-robolectric = { module = "org.robolectric:robolectric", version.ref = "roboelectric" } square-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } @@ -117,3 +126,15 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kotlinxKover" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } + +[bundles] +tests-instrumented = [ + "androidx-compose-ui-test", + "androidx-test-espresso", + "androidx-test-runner", + "androidx-test-rules", + "fastlane-screengrab", + "junit-junit5", + "junit-vintage", + "mockk-android", +]