From e5c73c56e3fb41ae1d2b86250ffc533943953c19 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 20 Aug 2026 11:01:16 -0400 Subject: [PATCH] chore(deps): drop Google's libphonenumber, use the Android port everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app depended on two libphonenumber artifacts: the Android port (io.michaelrocks:libphonenumber-android) and Google's desktop artifact (com.googlecode.libphonenumber:libphonenumber). Each ships its own copy of the metadata, so any version skew between them meant the two could disagree on whether a number is valid — and since the port lags upstream, it always set the ceiling and blocked routine bumps of the Google artifact. Three of the four declarations were dead: :services:flipcash, :services:flipcash-compose and :services:opencode declared the Google artifact but had no Kotlin imports and no references to i18n/phonenumbers in their compiled output. Removed. The one real consumer, PhoneRegion in :shared:onramp:coinbase, is ported onto the Android artifact. That artifact loads its metadata from the AAR's assets rather than exposing a static instance, so it needs a Context — regionFromPhone becomes an injectable PhoneRegionResolver, mirroring PhoneUtils in :shared:phone (including the lazy PhoneNumberUtil so the metadata blob is parsed on first use rather than during construction on the main thread). The test moves to Robolectric for the Context. Its 13 cases and their expected values are unchanged on purpose: they are the parity check that the port agrees with Google's artifact on every number covered. With the Google artifact gone, the dependabot group rule that kept the two aliases in lockstep is no longer needed. --- .github/dependabot.yml | 17 ----- .../shared/onramp/coinbase/build.gradle.kts | 2 +- .../com/flipcash/app/onramp/PhoneRegion.kt | 69 ++++++++++++------- .../flipcash/app/onramp/PhoneRegionTest.kt | 43 ++++++++---- gradle/libs.versions.toml | 13 ++-- services/flipcash-compose/build.gradle.kts | 2 - services/flipcash/build.gradle.kts | 2 - services/opencode/build.gradle.kts | 2 - 8 files changed, 81 insertions(+), 69 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d7394f85a6..d43eeec884 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,23 +5,6 @@ updates: schedule: interval: weekly open-pull-requests-limit: 10 - groups: - # Both libphonenumber artifacts must move together — see the note in gradle/libs.versions.toml. - # Grouping them into one PR makes any version skew visible in a single diff rather than in two - # PRs that can land independently. - libphonenumber: - patterns: - - "*libphonenumber*" - ignore: - # Google's libphonenumber can only move when the Android port (io.michaelrocks) has a matching - # release — the two ship separate copies of the metadata and must stay on the same version, or - # they can disagree on whether a number is valid. See the note in gradle/libs.versions.toml. - # The port has no 9.0.37, so this specific version is held. Grouping alone does not prevent it: - # a group PR still gets opened with just the one artifact when only that artifact has an update, - # and closing such a PR does NOT suppress the version (Dependabot says so explicitly on close). - # Remove this entry once the port publishes 9.0.37 or later. - - dependency-name: "com.googlecode.libphonenumber:libphonenumber" - versions: ["9.0.37"] labels: - "dependencies" - "type: build" diff --git a/apps/flipcash/shared/onramp/coinbase/build.gradle.kts b/apps/flipcash/shared/onramp/coinbase/build.gradle.kts index 10ca5044b3..70a50cfdfc 100644 --- a/apps/flipcash/shared/onramp/coinbase/build.gradle.kts +++ b/apps/flipcash/shared/onramp/coinbase/build.gradle.kts @@ -18,7 +18,7 @@ dependencies { implementation(libs.androidx.localbroadcastmanager) implementation(libs.bundles.kotlinx.serialization) - implementation(libs.lib.phone.number.google) + implementation(libs.lib.phone.number.port) implementation(libs.play.services.wallet) implementation(libs.kotlinx.coroutines.play.services) implementation(project(":libs:messaging")) diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/PhoneRegion.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/PhoneRegion.kt index 1255d668e4..45978ca354 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/PhoneRegion.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/PhoneRegion.kt @@ -1,7 +1,11 @@ package com.flipcash.app.onramp -import com.google.i18n.phonenumbers.PhoneNumberUtil -import com.google.i18n.phonenumbers.NumberParseException +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.michaelrocks.libphonenumber.android.NumberParseException +import io.michaelrocks.libphonenumber.android.PhoneNumberUtil +import javax.inject.Inject +import javax.inject.Singleton /** * Region derived from a phone number for Coinbase buy-options lookups. @@ -22,33 +26,48 @@ data class PhoneRegion( */ private val NYC_AREA_CODES = setOf("212", "718", "917", "646", "347", "929", "586") -private val phoneUtil: PhoneNumberUtil = PhoneNumberUtil.getInstance() - /** - * Extracts the [PhoneRegion] from an E.164 phone number using libphonenumber - * for country detection and area code heuristics for US subdivision. + * Resolves the [PhoneRegion] for a phone number. * - * @return the detected region, or null if the number can't be parsed. + * Injectable rather than a top-level function because the Android libphonenumber port loads its + * metadata from the AAR's assets, so it needs a [Context] — unlike Google's desktop artifact, which + * exposes a static instance. See the note in `gradle/libs.versions.toml` for why the port is the + * only libphonenumber this app depends on. */ -fun regionFromPhone(phone: String): PhoneRegion? { - val parsed = try { - phoneUtil.parse(phone, null) - } catch (_: NumberParseException) { - return null - } +@Singleton +class PhoneRegionResolver @Inject constructor( + @ApplicationContext private val context: Context, +) { + // Lazy so libphonenumber's metadata blob is parsed on first *use* (a background caller), never + // during construction on the main thread. Same reasoning as PhoneUtils in :shared:phone. + private val phoneUtil by lazy { PhoneNumberUtil.createInstance(context) } - val country = phoneUtil.getRegionCodeForNumber(parsed) ?: return null - val subdivision = if (country == "US") { - subdivisionFromUsNumber(parsed.nationalNumber.toString()) - } else { - null - } + /** + * Extracts the [PhoneRegion] from an E.164 phone number using libphonenumber + * for country detection and area code heuristics for US subdivision. + * + * @return the detected region, or null if the number can't be parsed. + */ + fun regionFromPhone(phone: String): PhoneRegion? { + val parsed = try { + phoneUtil.parse(phone, null) + } catch (_: NumberParseException) { + return null + } - return PhoneRegion(country = country, subdivision = subdivision) -} + val country = phoneUtil.getRegionCodeForNumber(parsed) ?: return null + val subdivision = if (country == "US") { + subdivisionFromUsNumber(parsed.nationalNumber.toString()) + } else { + null + } + + return PhoneRegion(country = country, subdivision = subdivision) + } -private fun subdivisionFromUsNumber(nationalNumber: String): String? { - if (nationalNumber.length < 3) return null - val areaCode = nationalNumber.take(3) - return if (areaCode in NYC_AREA_CODES) "NY" else null + private fun subdivisionFromUsNumber(nationalNumber: String): String? { + if (nationalNumber.length < 3) return null + val areaCode = nationalNumber.take(3) + return if (areaCode in NYC_AREA_CODES) "NY" else null + } } diff --git a/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/PhoneRegionTest.kt b/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/PhoneRegionTest.kt index f0edd52a96..4590240120 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/PhoneRegionTest.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/PhoneRegionTest.kt @@ -1,41 +1,58 @@ package com.flipcash.app.onramp +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull +// Robolectric, because the Android libphonenumber port loads its metadata from the AAR's assets and +// so needs a real Context. These cases are unchanged from when this resolved via Google's desktop +// artifact — they are the parity check that the port agrees with it on every number here. +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) class PhoneRegionTest { + private lateinit var resolver: PhoneRegionResolver + + @BeforeTest + fun setup() { + resolver = PhoneRegionResolver(RuntimeEnvironment.getApplication()) + } + // region NYC area codes @Test fun `212 area code returns US-NY`() { - assertEquals(PhoneRegion("US", "NY"), regionFromPhone("+12125551234")) + assertEquals(PhoneRegion("US", "NY"), resolver.regionFromPhone("+12125551234")) } @Test fun `718 area code returns US-NY`() { - assertEquals(PhoneRegion("US", "NY"), regionFromPhone("+17185551234")) + assertEquals(PhoneRegion("US", "NY"), resolver.regionFromPhone("+17185551234")) } @Test fun `917 area code returns US-NY`() { - assertEquals(PhoneRegion("US", "NY"), regionFromPhone("+19175551234")) + assertEquals(PhoneRegion("US", "NY"), resolver.regionFromPhone("+19175551234")) } @Test fun `646 area code returns US-NY`() { - assertEquals(PhoneRegion("US", "NY"), regionFromPhone("+16465551234")) + assertEquals(PhoneRegion("US", "NY"), resolver.regionFromPhone("+16465551234")) } @Test fun `347 area code returns US-NY`() { - assertEquals(PhoneRegion("US", "NY"), regionFromPhone("+13475551234")) + assertEquals(PhoneRegion("US", "NY"), resolver.regionFromPhone("+13475551234")) } @Test fun `929 area code returns US-NY`() { - assertEquals(PhoneRegion("US", "NY"), regionFromPhone("+19295551234")) + assertEquals(PhoneRegion("US", "NY"), resolver.regionFromPhone("+19295551234")) } // endregion @@ -44,7 +61,7 @@ class PhoneRegionTest { @Test fun `non-NYC US area code returns US with no subdivision`() { - assertEquals(PhoneRegion("US", null), regionFromPhone("+14155551234")) // SF + assertEquals(PhoneRegion("US", null), resolver.regionFromPhone("+14155551234")) // SF } // endregion @@ -53,17 +70,17 @@ class PhoneRegionTest { @Test fun `Canadian number returns CA`() { - assertEquals(PhoneRegion("CA", null), regionFromPhone("+14165551234")) // Toronto + assertEquals(PhoneRegion("CA", null), resolver.regionFromPhone("+14165551234")) // Toronto } @Test fun `UK number returns GB`() { - assertEquals(PhoneRegion("GB", null), regionFromPhone("+442071234567")) + assertEquals(PhoneRegion("GB", null), resolver.regionFromPhone("+442071234567")) } @Test fun `German number returns DE`() { - assertEquals(PhoneRegion("DE", null), regionFromPhone("+4915112345678")) + assertEquals(PhoneRegion("DE", null), resolver.regionFromPhone("+4915112345678")) } // endregion @@ -72,17 +89,17 @@ class PhoneRegionTest { @Test fun `invalid number returns null`() { - assertNull(regionFromPhone("12345")) + assertNull(resolver.regionFromPhone("12345")) } @Test fun `empty string returns null`() { - assertNull(regionFromPhone("")) + assertNull(resolver.regionFromPhone("")) } @Test fun `non-numeric string returns null`() { - assertNull(regionFromPhone("not-a-phone")) + assertNull(resolver.regionFromPhone("not-a-phone")) } // endregion diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b15bedf5e0..8001287a60 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,13 +64,13 @@ protobuf = "4.35.1" protobuf-plugin = "0.10.0" protovalidate-kt = "0.1.1" -# These two must stay on the SAME version. `:shared:phone` parses and formats with the Android port -# while the services modules validate with Google's artifact, and each ships its own copy of the -# libphonenumber metadata — so a version skew means the two can disagree on whether a number is -# valid. The port lags upstream, so it sets the ceiling: only bump `-google` once the port has -# published a matching release. +# 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, +# so any version skew between them meant they could disagree on whether a number is valid — and the +# port lags upstream, so it always set the ceiling and blocked routine bumps. +# Don't reintroduce the Google artifact. The port exposes the same API; it just needs a Context, +# because it loads its metadata from the AAR's assets rather than from a static instance. lib-phone-number-port = "9.0.36" -lib-phone-number-google = "9.0.36" zxing = "3.5.4" androidx-benchmark-macro = "1.5.0-rc01" @@ -251,7 +251,6 @@ vico-compose = { module = "com.patrykandpatrick.vico:compose", version.ref = "vi # Phone number lib-phone-number-port = { module = "io.michaelrocks:libphonenumber-android", version.ref = "lib-phone-number-port" } -lib-phone-number-google = { module = "com.googlecode.libphonenumber:libphonenumber", version.ref = "lib-phone-number-google" } # QR / Scanning zxing = { module = "com.google.zxing:core", version.ref = "zxing" } diff --git a/services/flipcash-compose/build.gradle.kts b/services/flipcash-compose/build.gradle.kts index 9fbc39e99c..86dd09fe4c 100644 --- a/services/flipcash-compose/build.gradle.kts +++ b/services/flipcash-compose/build.gradle.kts @@ -50,8 +50,6 @@ dependencies { ksp(libs.androidx.room.compiler) implementation(libs.fingerprint.pro) - implementation(libs.lib.phone.number.google) - androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.junit) androidTestImplementation(libs.androidx.test.runner) diff --git a/services/flipcash/build.gradle.kts b/services/flipcash/build.gradle.kts index 5df8adfb03..50199a10cd 100644 --- a/services/flipcash/build.gradle.kts +++ b/services/flipcash/build.gradle.kts @@ -57,8 +57,6 @@ dependencies { implementation(libs.fingerprint.pro) - implementation(libs.lib.phone.number.google) - androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.junit) androidTestImplementation(libs.androidx.test.runner) diff --git a/services/opencode/build.gradle.kts b/services/opencode/build.gradle.kts index 9b8b70c617..e0eba2380a 100644 --- a/services/opencode/build.gradle.kts +++ b/services/opencode/build.gradle.kts @@ -76,8 +76,6 @@ dependencies { implementation(libs.fingerprint.pro) - implementation(libs.lib.phone.number.google) - androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.junit) androidTestImplementation(libs.androidx.test.runner)