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)