From 61163def41d106e1b17cfbf3bc8d56efb623802c Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 16 Sep 2026 16:55:21 -0400 Subject: [PATCH 1/4] chore(deps): bump both contract packages for the runtime metadata --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f753cb8e99..385fcc4aaa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,8 +68,8 @@ protovalidate-kt = "0.1.2" # cadence and there is nothing to keep aligned. # 0.3.0 is the first release of either package to ship R8 keep rules for its generated # messages, which is what lets proguard-rules.pro drop its own. -ocp-client-protocol = "0.4.0" -flipcash2-client-protocol = "0.8.0" +ocp-client-protocol = "0.5.0" +flipcash2-client-protocol = "0.9.0" # The Android port is the ONLY libphonenumber this app depends on, deliberately. Google's # `com.googlecode` artifact used to sit alongside it; the two ship separate copies of the metadata, From 24348b7cf62a480b599e2da60f0f1e6ee45b45d8 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 16 Sep 2026 16:58:04 -0400 Subject: [PATCH 2/4] feat(lab): read the contract version and proto pin from both packages --- apps/flipcash/features/lab/build.gradle.kts | 7 ++ .../app/lab/internal/ContractInfoRow.kt | 70 +++++++++++++++++++ .../app/lab/internal/ContractInfoTest.kt | 37 ++++++++++ 3 files changed, 114 insertions(+) create mode 100644 apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/ContractInfoRow.kt create mode 100644 apps/flipcash/features/lab/src/test/kotlin/com/flipcash/app/lab/internal/ContractInfoTest.kt diff --git a/apps/flipcash/features/lab/build.gradle.kts b/apps/flipcash/features/lab/build.gradle.kts index a712d78c8b..4187a26e8b 100644 --- a/apps/flipcash/features/lab/build.gradle.kts +++ b/apps/flipcash/features/lab/build.gradle.kts @@ -10,4 +10,11 @@ dependencies { implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":apps:flipcash:shared:userflags")) implementation(project(":libs:messaging")) + + implementation(libs.ocp.client.protocol) + implementation(libs.flipcash2.client.protocol) + + testImplementation(kotlin("test")) + testImplementation(libs.bundles.unit.testing) + testImplementation(libs.bundles.compose.ui.testing) } diff --git a/apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/ContractInfoRow.kt b/apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/ContractInfoRow.kt new file mode 100644 index 0000000000..bd25d4f4ca --- /dev/null +++ b/apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/ContractInfoRow.kt @@ -0,0 +1,70 @@ +package com.flipcash.app.lab.internal + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.codeinc.flipcash.gen.Flipcash2ContractInfo +import com.codeinc.opencode.gen.OcpContractInfo +import com.getcode.theme.CodeTheme + +/** + * One backend contract package, as the build carries it. + * + * [commit] is the upstream proto commit the package was generated from, already shortened by the + * package itself so both platforms truncate the same way. A package built from a local proto sync + * reports `LOCAL` instead of a SHA, which is what [isLocal] flags -- that build's contract is not + * reproducible from anything published, so it is worth seeing at a glance. + */ +internal data class ContractInfo( + val name: String, + val version: String, + val commit: String, + val isLocal: Boolean, +) { + val detail: String get() = "$version · $commit" +} + +internal fun contractInfo(): List = listOf( + ContractInfo( + name = "ocp", + version = OcpContractInfo.VERSION, + commit = OcpContractInfo.shortProtoCommit, + isLocal = OcpContractInfo.isLocal, + ), + ContractInfo( + name = "flipcash2", + version = Flipcash2ContractInfo.VERSION, + commit = Flipcash2ContractInfo.shortProtoCommit, + isLocal = Flipcash2ContractInfo.isLocal, + ), +) + +@Composable +internal fun ContractInfoRow( + info: ContractInfo, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = CodeTheme.dimens.inset, vertical = CodeTheme.dimens.grid.x2), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = info.name, + style = CodeTheme.typography.textMedium, + color = CodeTheme.colors.textMain, + ) + Text( + text = info.detail, + style = CodeTheme.typography.textSmall, + color = if (info.isLocal) CodeTheme.colors.warning else CodeTheme.colors.textSecondary, + ) + } +} diff --git a/apps/flipcash/features/lab/src/test/kotlin/com/flipcash/app/lab/internal/ContractInfoTest.kt b/apps/flipcash/features/lab/src/test/kotlin/com/flipcash/app/lab/internal/ContractInfoTest.kt new file mode 100644 index 0000000000..93a5c0803e --- /dev/null +++ b/apps/flipcash/features/lab/src/test/kotlin/com/flipcash/app/lab/internal/ContractInfoTest.kt @@ -0,0 +1,37 @@ +package com.flipcash.app.lab.internal + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ContractInfoTest { + + @Test + fun `both contracts are listed, in a stable order`() { + val names = contractInfo().map { it.name } + assertEquals(listOf("ocp", "flipcash2"), names) + } + + @Test + fun `detail joins the version and the short commit`() { + val row = ContractInfo(name = "ocp", version = "0.5.0", commit = "82202912", isLocal = false) + assertEquals("0.5.0 · 82202912", row.detail) + assertFalse(row.isLocal) + } + + @Test + fun `a local contract keeps LOCAL in the detail and flags itself`() { + val row = ContractInfo(name = "ocp", version = "0.5.0", commit = "LOCAL", isLocal = true) + assertEquals("0.5.0 · LOCAL", row.detail) + assertTrue(row.isLocal) + } + + @Test + fun `the real packages report a non-empty version and commit`() { + contractInfo().forEach { row -> + assertTrue(row.version.isNotEmpty(), "${row.name} has no version") + assertTrue(row.commit.isNotEmpty(), "${row.name} has no commit") + } + } +} From fb0020412402a11cd335cd75023d54386bb4b2dd Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 16 Sep 2026 17:01:02 -0400 Subject: [PATCH 3/4] feat(lab): show the contract versions and proto pins under Developer --- .../core/src/main/res/values/strings.xml | 1 + .../app/lab/internal/LabsScreenContent.kt | 11 +++++ .../app/lab/internal/ContractInfoRowTest.kt | 44 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 apps/flipcash/features/lab/src/test/kotlin/com/flipcash/app/lab/internal/ContractInfoRowTest.kt diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index 640e4e339f..bd6af86443 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -662,6 +662,7 @@ Features Developer + Contracts User Flags Account User Flags diff --git a/apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/LabsScreenContent.kt b/apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/LabsScreenContent.kt index a2188bf67f..1956a365af 100644 --- a/apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/LabsScreenContent.kt +++ b/apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/internal/LabsScreenContent.kt @@ -189,6 +189,17 @@ internal fun LabsScreenContent(viewModel: LabsScreenViewModel, onboarding: Boole navigator.navigate(AppRoute.UserFlags) } } + + item(contentType = "section_header") { + SectionHeader( + modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset), + title = stringResource(R.string.title_settingsSectionContracts) + ) + } + + items(contractInfo(), key = { it.name }, contentType = { "contract_info" }) { info -> + ContractInfoRow(info) + } } } } diff --git a/apps/flipcash/features/lab/src/test/kotlin/com/flipcash/app/lab/internal/ContractInfoRowTest.kt b/apps/flipcash/features/lab/src/test/kotlin/com/flipcash/app/lab/internal/ContractInfoRowTest.kt new file mode 100644 index 0000000000..01ffda4969 --- /dev/null +++ b/apps/flipcash/features/lab/src/test/kotlin/com/flipcash/app/lab/internal/ContractInfoRowTest.kt @@ -0,0 +1,44 @@ +package com.flipcash.app.lab.internal + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import com.getcode.theme.DesignSystem +import org.junit.Rule +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.Test + +@RunWith(RobolectricTestRunner::class) +class ContractInfoRowTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `shows the package name and the version-commit detail`() { + composeTestRule.setContent { + DesignSystem { + ContractInfoRow( + ContractInfo(name = "ocp", version = "0.5.0", commit = "82202912", isLocal = false) + ) + } + } + + composeTestRule.onNodeWithText("ocp").assertIsDisplayed() + composeTestRule.onNodeWithText("0.5.0 · 82202912").assertIsDisplayed() + } + + @Test + fun `a local contract shows LOCAL in place of a commit`() { + composeTestRule.setContent { + DesignSystem { + ContractInfoRow( + ContractInfo(name = "flipcash2", version = "0.9.0", commit = "LOCAL", isLocal = true) + ) + } + } + + composeTestRule.onNodeWithText("0.9.0 · LOCAL").assertIsDisplayed() + } +} From 3852ed950538c49dee8a13d757101ad67855abad Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 16 Sep 2026 17:06:00 -0400 Subject: [PATCH 4/4] feat(logging): name both contract packages in the log file header --- libs/logging/build.gradle.kts | 3 +++ .../logging/src/main/kotlin/com/getcode/utils/FileTree.kt | 4 ++++ .../src/test/kotlin/com/getcode/utils/FileTreeTest.kt | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/libs/logging/build.gradle.kts b/libs/logging/build.gradle.kts index ea03686de0..6baf81d2b1 100644 --- a/libs/logging/build.gradle.kts +++ b/libs/logging/build.gradle.kts @@ -31,4 +31,7 @@ dependencies { implementation(libs.androidx.annotation) implementation(libs.grpc.kotlin) implementation(project(":libs:messaging")) + + implementation(libs.ocp.client.protocol) + implementation(libs.flipcash2.client.protocol) } diff --git a/libs/logging/src/main/kotlin/com/getcode/utils/FileTree.kt b/libs/logging/src/main/kotlin/com/getcode/utils/FileTree.kt index 7cb3415db2..19f9437eec 100644 --- a/libs/logging/src/main/kotlin/com/getcode/utils/FileTree.kt +++ b/libs/logging/src/main/kotlin/com/getcode/utils/FileTree.kt @@ -4,6 +4,8 @@ import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.util.Log +import com.codeinc.flipcash.gen.Flipcash2ContractInfo +import com.codeinc.opencode.gen.OcpContractInfo import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow @@ -146,6 +148,8 @@ private fun buildDeviceHeader(context: Context): String { appendLine("=".repeat(60)) appendLine("App Version: $versionName ($versionCode)") appendLine("Package: ${context.packageName}") + appendLine("OCP Contract: ${OcpContractInfo.VERSION} (${OcpContractInfo.shortProtoCommit})") + appendLine("FC2 Contract: ${Flipcash2ContractInfo.VERSION} (${Flipcash2ContractInfo.shortProtoCommit})") appendLine("User ID: ${userId ?: "not set"}") appendLine("Device: ${Build.MANUFACTURER} ${Build.MODEL}") appendLine("Android: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})") diff --git a/libs/logging/src/test/kotlin/com/getcode/utils/FileTreeTest.kt b/libs/logging/src/test/kotlin/com/getcode/utils/FileTreeTest.kt index 1cfe67876b..d97b9e6675 100644 --- a/libs/logging/src/test/kotlin/com/getcode/utils/FileTreeTest.kt +++ b/libs/logging/src/test/kotlin/com/getcode/utils/FileTreeTest.kt @@ -263,4 +263,12 @@ class FileTreeTest { assertTrue(content.contains("Android:")) assertTrue(content.contains("Exported:")) } + + @Test + fun `exported log file names both contract packages`() { + Timber.d("contract check") + val content = fileTree.getLogFile()!!.readText() + assertTrue(content.contains("OCP Contract:"), "header has no OCP contract line") + assertTrue(content.contains("FC2 Contract:"), "header has no flipcash2 contract line") + } }