From af24bd2e381e449fc9bb80d98e87c2f0e9c43d85 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Tue, 7 Oct 2025 15:09:36 +0200 Subject: [PATCH] feat: CountryData & Selector Utilities --- .../ui/auth/compose/data/Countries.kt | 260 +++++++++++++ .../ui/auth/compose/data/CountryData.kt | 62 ++++ .../ui/auth/compose/data/CountryUtils.kt | 117 ++++++ .../ui/auth/compose/data/CountryDataTest.kt | 347 ++++++++++++++++++ 4 files changed, 786 insertions(+) create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/data/Countries.kt create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/data/CountryData.kt create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/data/CountryUtils.kt create mode 100644 auth/src/test/java/com/firebase/ui/auth/compose/data/CountryDataTest.kt diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/data/Countries.kt b/auth/src/main/java/com/firebase/ui/auth/compose/data/Countries.kt new file mode 100644 index 000000000..3ba0dc497 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/data/Countries.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.compose.data + +/** + * Complete list of countries with their dial codes and ISO country codes. + * Auto-generated from ISO 3166-1 standard. + */ +val ALL_COUNTRIES: List = listOf( + CountryData("Afghanistan", "+93", "AF", countryCodeToFlagEmoji("AF")), + CountryData("Albania", "+355", "AL", countryCodeToFlagEmoji("AL")), + CountryData("Algeria", "+213", "DZ", countryCodeToFlagEmoji("DZ")), + CountryData("American Samoa", "+684", "AS", countryCodeToFlagEmoji("AS")), + CountryData("Andorra", "+376", "AD", countryCodeToFlagEmoji("AD")), + CountryData("Angola", "+244", "AO", countryCodeToFlagEmoji("AO")), + CountryData("Anguilla", "+264", "AI", countryCodeToFlagEmoji("AI")), + CountryData("Antigua and Barbuda", "+268", "AG", countryCodeToFlagEmoji("AG")), + CountryData("Argentina", "+54", "AR", countryCodeToFlagEmoji("AR")), + CountryData("Armenia", "+374", "AM", countryCodeToFlagEmoji("AM")), + CountryData("Aruba", "+297", "AW", countryCodeToFlagEmoji("AW")), + CountryData("Australia", "+61", "AU", countryCodeToFlagEmoji("AU")), + CountryData("Austria", "+43", "AT", countryCodeToFlagEmoji("AT")), + CountryData("Azerbaijan", "+994", "AZ", countryCodeToFlagEmoji("AZ")), + CountryData("Bahamas", "+242", "BS", countryCodeToFlagEmoji("BS")), + CountryData("Bahrain", "+973", "BH", countryCodeToFlagEmoji("BH")), + CountryData("Bangladesh", "+880", "BD", countryCodeToFlagEmoji("BD")), + CountryData("Barbados", "+246", "BB", countryCodeToFlagEmoji("BB")), + CountryData("Belarus", "+375", "BY", countryCodeToFlagEmoji("BY")), + CountryData("Belgium", "+32", "BE", countryCodeToFlagEmoji("BE")), + CountryData("Belize", "+501", "BZ", countryCodeToFlagEmoji("BZ")), + CountryData("Benin", "+229", "BJ", countryCodeToFlagEmoji("BJ")), + CountryData("Bermuda", "+441", "BM", countryCodeToFlagEmoji("BM")), + CountryData("Bhutan", "+975", "BT", countryCodeToFlagEmoji("BT")), + CountryData("Bolivia", "+591", "BO", countryCodeToFlagEmoji("BO")), + CountryData("Bosnia and Herzegovina", "+387", "BA", countryCodeToFlagEmoji("BA")), + CountryData("Botswana", "+267", "BW", countryCodeToFlagEmoji("BW")), + CountryData("Brazil", "+55", "BR", countryCodeToFlagEmoji("BR")), + CountryData("British Indian Ocean Territory", "+246", "IO", countryCodeToFlagEmoji("IO")), + CountryData("Brunei", "+673", "BN", countryCodeToFlagEmoji("BN")), + CountryData("Bulgaria", "+359", "BG", countryCodeToFlagEmoji("BG")), + CountryData("Burkina Faso", "+226", "BF", countryCodeToFlagEmoji("BF")), + CountryData("Burundi", "+257", "BI", countryCodeToFlagEmoji("BI")), + CountryData("Cambodia", "+855", "KH", countryCodeToFlagEmoji("KH")), + CountryData("Cameroon", "+237", "CM", countryCodeToFlagEmoji("CM")), + CountryData("Canada", "+1", "CA", countryCodeToFlagEmoji("CA")), + CountryData("Cape Verde", "+238", "CV", countryCodeToFlagEmoji("CV")), + CountryData("Cayman Islands", "+345", "KY", countryCodeToFlagEmoji("KY")), + CountryData("Central African Republic", "+236", "CF", countryCodeToFlagEmoji("CF")), + CountryData("Chad", "+235", "TD", countryCodeToFlagEmoji("TD")), + CountryData("Chile", "+56", "CL", countryCodeToFlagEmoji("CL")), + CountryData("China", "+86", "CN", countryCodeToFlagEmoji("CN")), + CountryData("Colombia", "+57", "CO", countryCodeToFlagEmoji("CO")), + CountryData("Comoros", "+269", "KM", countryCodeToFlagEmoji("KM")), + CountryData("Congo", "+242", "CG", countryCodeToFlagEmoji("CG")), + CountryData("Congo (DRC)", "+243", "CD", countryCodeToFlagEmoji("CD")), + CountryData("Cook Islands", "+682", "CK", countryCodeToFlagEmoji("CK")), + CountryData("Costa Rica", "+506", "CR", countryCodeToFlagEmoji("CR")), + CountryData("Côte d'Ivoire", "+225", "CI", countryCodeToFlagEmoji("CI")), + CountryData("Croatia", "+385", "HR", countryCodeToFlagEmoji("HR")), + CountryData("Cuba", "+53", "CU", countryCodeToFlagEmoji("CU")), + CountryData("Curaçao", "+599", "CW", countryCodeToFlagEmoji("CW")), + CountryData("Cyprus", "+357", "CY", countryCodeToFlagEmoji("CY")), + CountryData("Czech Republic", "+420", "CZ", countryCodeToFlagEmoji("CZ")), + CountryData("Denmark", "+45", "DK", countryCodeToFlagEmoji("DK")), + CountryData("Djibouti", "+253", "DJ", countryCodeToFlagEmoji("DJ")), + CountryData("Dominica", "+767", "DM", countryCodeToFlagEmoji("DM")), + CountryData("Dominican Republic", "+809", "DO", countryCodeToFlagEmoji("DO")), + CountryData("Ecuador", "+593", "EC", countryCodeToFlagEmoji("EC")), + CountryData("Egypt", "+20", "EG", countryCodeToFlagEmoji("EG")), + CountryData("El Salvador", "+503", "SV", countryCodeToFlagEmoji("SV")), + CountryData("Equatorial Guinea", "+240", "GQ", countryCodeToFlagEmoji("GQ")), + CountryData("Eritrea", "+291", "ER", countryCodeToFlagEmoji("ER")), + CountryData("Estonia", "+372", "EE", countryCodeToFlagEmoji("EE")), + CountryData("Ethiopia", "+251", "ET", countryCodeToFlagEmoji("ET")), + CountryData("Falkland Islands", "+500", "FK", countryCodeToFlagEmoji("FK")), + CountryData("Faroe Islands", "+298", "FO", countryCodeToFlagEmoji("FO")), + CountryData("Fiji", "+679", "FJ", countryCodeToFlagEmoji("FJ")), + CountryData("Finland", "+358", "FI", countryCodeToFlagEmoji("FI")), + CountryData("France", "+33", "FR", countryCodeToFlagEmoji("FR")), + CountryData("French Guiana", "+594", "GF", countryCodeToFlagEmoji("GF")), + CountryData("French Polynesia", "+689", "PF", countryCodeToFlagEmoji("PF")), + CountryData("Gabon", "+241", "GA", countryCodeToFlagEmoji("GA")), + CountryData("Gambia", "+220", "GM", countryCodeToFlagEmoji("GM")), + CountryData("Georgia", "+995", "GE", countryCodeToFlagEmoji("GE")), + CountryData("Germany", "+49", "DE", countryCodeToFlagEmoji("DE")), + CountryData("Ghana", "+233", "GH", countryCodeToFlagEmoji("GH")), + CountryData("Gibraltar", "+350", "GI", countryCodeToFlagEmoji("GI")), + CountryData("Greece", "+30", "GR", countryCodeToFlagEmoji("GR")), + CountryData("Greenland", "+299", "GL", countryCodeToFlagEmoji("GL")), + CountryData("Grenada", "+473", "GD", countryCodeToFlagEmoji("GD")), + CountryData("Guadeloupe", "+590", "GP", countryCodeToFlagEmoji("GP")), + CountryData("Guam", "+671", "GU", countryCodeToFlagEmoji("GU")), + CountryData("Guatemala", "+502", "GT", countryCodeToFlagEmoji("GT")), + CountryData("Guernsey", "+1481", "GG", countryCodeToFlagEmoji("GG")), + CountryData("Guinea", "+224", "GN", countryCodeToFlagEmoji("GN")), + CountryData("Guinea-Bissau", "+245", "GW", countryCodeToFlagEmoji("GW")), + CountryData("Guyana", "+592", "GY", countryCodeToFlagEmoji("GY")), + CountryData("Haiti", "+509", "HT", countryCodeToFlagEmoji("HT")), + CountryData("Honduras", "+504", "HN", countryCodeToFlagEmoji("HN")), + CountryData("Hong Kong", "+852", "HK", countryCodeToFlagEmoji("HK")), + CountryData("Hungary", "+36", "HU", countryCodeToFlagEmoji("HU")), + CountryData("Iceland", "+354", "IS", countryCodeToFlagEmoji("IS")), + CountryData("India", "+91", "IN", countryCodeToFlagEmoji("IN")), + CountryData("Indonesia", "+62", "ID", countryCodeToFlagEmoji("ID")), + CountryData("Iran", "+98", "IR", countryCodeToFlagEmoji("IR")), + CountryData("Iraq", "+964", "IQ", countryCodeToFlagEmoji("IQ")), + CountryData("Ireland", "+353", "IE", countryCodeToFlagEmoji("IE")), + CountryData("Isle of Man", "+44", "IM", countryCodeToFlagEmoji("IM")), + CountryData("Israel", "+972", "IL", countryCodeToFlagEmoji("IL")), + CountryData("Italy", "+39", "IT", countryCodeToFlagEmoji("IT")), + CountryData("Jamaica", "+876", "JM", countryCodeToFlagEmoji("JM")), + CountryData("Japan", "+81", "JP", countryCodeToFlagEmoji("JP")), + CountryData("Jersey", "+44", "JE", countryCodeToFlagEmoji("JE")), + CountryData("Jordan", "+962", "JO", countryCodeToFlagEmoji("JO")), + CountryData("Kazakhstan", "+7", "KZ", countryCodeToFlagEmoji("KZ")), + CountryData("Kenya", "+254", "KE", countryCodeToFlagEmoji("KE")), + CountryData("Kiribati", "+686", "KI", countryCodeToFlagEmoji("KI")), + CountryData("Kosovo", "+383", "XK", countryCodeToFlagEmoji("XK")), + CountryData("Kuwait", "+965", "KW", countryCodeToFlagEmoji("KW")), + CountryData("Kyrgyzstan", "+996", "KG", countryCodeToFlagEmoji("KG")), + CountryData("Laos", "+856", "LA", countryCodeToFlagEmoji("LA")), + CountryData("Latvia", "+371", "LV", countryCodeToFlagEmoji("LV")), + CountryData("Lebanon", "+961", "LB", countryCodeToFlagEmoji("LB")), + CountryData("Lesotho", "+266", "LS", countryCodeToFlagEmoji("LS")), + CountryData("Liberia", "+231", "LR", countryCodeToFlagEmoji("LR")), + CountryData("Libya", "+218", "LY", countryCodeToFlagEmoji("LY")), + CountryData("Liechtenstein", "+423", "LI", countryCodeToFlagEmoji("LI")), + CountryData("Lithuania", "+370", "LT", countryCodeToFlagEmoji("LT")), + CountryData("Luxembourg", "+352", "LU", countryCodeToFlagEmoji("LU")), + CountryData("Macao", "+853", "MO", countryCodeToFlagEmoji("MO")), + CountryData("Macedonia", "+389", "MK", countryCodeToFlagEmoji("MK")), + CountryData("Madagascar", "+261", "MG", countryCodeToFlagEmoji("MG")), + CountryData("Malawi", "+265", "MW", countryCodeToFlagEmoji("MW")), + CountryData("Malaysia", "+60", "MY", countryCodeToFlagEmoji("MY")), + CountryData("Maldives", "+960", "MV", countryCodeToFlagEmoji("MV")), + CountryData("Mali", "+223", "ML", countryCodeToFlagEmoji("ML")), + CountryData("Malta", "+356", "MT", countryCodeToFlagEmoji("MT")), + CountryData("Marshall Islands", "+692", "MH", countryCodeToFlagEmoji("MH")), + CountryData("Martinique", "+596", "MQ", countryCodeToFlagEmoji("MQ")), + CountryData("Mauritania", "+222", "MR", countryCodeToFlagEmoji("MR")), + CountryData("Mauritius", "+230", "MU", countryCodeToFlagEmoji("MU")), + CountryData("Mayotte", "+262", "YT", countryCodeToFlagEmoji("YT")), + CountryData("Mexico", "+52", "MX", countryCodeToFlagEmoji("MX")), + CountryData("Micronesia", "+691", "FM", countryCodeToFlagEmoji("FM")), + CountryData("Moldova", "+373", "MD", countryCodeToFlagEmoji("MD")), + CountryData("Monaco", "+377", "MC", countryCodeToFlagEmoji("MC")), + CountryData("Mongolia", "+976", "MN", countryCodeToFlagEmoji("MN")), + CountryData("Montenegro", "+382", "ME", countryCodeToFlagEmoji("ME")), + CountryData("Montserrat", "+664", "MS", countryCodeToFlagEmoji("MS")), + CountryData("Morocco", "+212", "MA", countryCodeToFlagEmoji("MA")), + CountryData("Mozambique", "+258", "MZ", countryCodeToFlagEmoji("MZ")), + CountryData("Myanmar", "+95", "MM", countryCodeToFlagEmoji("MM")), + CountryData("Namibia", "+264", "NA", countryCodeToFlagEmoji("NA")), + CountryData("Nauru", "+674", "NR", countryCodeToFlagEmoji("NR")), + CountryData("Nepal", "+977", "NP", countryCodeToFlagEmoji("NP")), + CountryData("Netherlands", "+31", "NL", countryCodeToFlagEmoji("NL")), + CountryData("New Caledonia", "+687", "NC", countryCodeToFlagEmoji("NC")), + CountryData("New Zealand", "+64", "NZ", countryCodeToFlagEmoji("NZ")), + CountryData("Nicaragua", "+505", "NI", countryCodeToFlagEmoji("NI")), + CountryData("Niger", "+227", "NE", countryCodeToFlagEmoji("NE")), + CountryData("Nigeria", "+234", "NG", countryCodeToFlagEmoji("NG")), + CountryData("Niue", "+683", "NU", countryCodeToFlagEmoji("NU")), + CountryData("Norfolk Island", "+672", "NF", countryCodeToFlagEmoji("NF")), + CountryData("North Korea", "+850", "KP", countryCodeToFlagEmoji("KP")), + CountryData("Northern Mariana Islands", "+670", "MP", countryCodeToFlagEmoji("MP")), + CountryData("Norway", "+47", "NO", countryCodeToFlagEmoji("NO")), + CountryData("Oman", "+968", "OM", countryCodeToFlagEmoji("OM")), + CountryData("Pakistan", "+92", "PK", countryCodeToFlagEmoji("PK")), + CountryData("Palau", "+680", "PW", countryCodeToFlagEmoji("PW")), + CountryData("Palestine", "+970", "PS", countryCodeToFlagEmoji("PS")), + CountryData("Panama", "+507", "PA", countryCodeToFlagEmoji("PA")), + CountryData("Papua New Guinea", "+675", "PG", countryCodeToFlagEmoji("PG")), + CountryData("Paraguay", "+595", "PY", countryCodeToFlagEmoji("PY")), + CountryData("Peru", "+51", "PE", countryCodeToFlagEmoji("PE")), + CountryData("Philippines", "+63", "PH", countryCodeToFlagEmoji("PH")), + CountryData("Poland", "+48", "PL", countryCodeToFlagEmoji("PL")), + CountryData("Portugal", "+351", "PT", countryCodeToFlagEmoji("PT")), + CountryData("Puerto Rico", "+787", "PR", countryCodeToFlagEmoji("PR")), + CountryData("Qatar", "+974", "QA", countryCodeToFlagEmoji("QA")), + CountryData("Réunion", "+262", "RE", countryCodeToFlagEmoji("RE")), + CountryData("Romania", "+40", "RO", countryCodeToFlagEmoji("RO")), + CountryData("Russia", "+7", "RU", countryCodeToFlagEmoji("RU")), + CountryData("Rwanda", "+250", "RW", countryCodeToFlagEmoji("RW")), + CountryData("Saint Barthélemy", "+590", "BL", countryCodeToFlagEmoji("BL")), + CountryData("Saint Helena", "+290", "SH", countryCodeToFlagEmoji("SH")), + CountryData("Saint Kitts and Nevis", "+869", "KN", countryCodeToFlagEmoji("KN")), + CountryData("Saint Lucia", "+758", "LC", countryCodeToFlagEmoji("LC")), + CountryData("Saint Martin", "+590", "MF", countryCodeToFlagEmoji("MF")), + CountryData("Saint Pierre and Miquelon", "+508", "PM", countryCodeToFlagEmoji("PM")), + CountryData("Saint Vincent and the Grenadines", "+784", "VC", countryCodeToFlagEmoji("VC")), + CountryData("Samoa", "+685", "WS", countryCodeToFlagEmoji("WS")), + CountryData("San Marino", "+378", "SM", countryCodeToFlagEmoji("SM")), + CountryData("Sao Tome and Principe", "+239", "ST", countryCodeToFlagEmoji("ST")), + CountryData("Saudi Arabia", "+966", "SA", countryCodeToFlagEmoji("SA")), + CountryData("Senegal", "+221", "SN", countryCodeToFlagEmoji("SN")), + CountryData("Serbia", "+381", "RS", countryCodeToFlagEmoji("RS")), + CountryData("Seychelles", "+248", "SC", countryCodeToFlagEmoji("SC")), + CountryData("Sierra Leone", "+232", "SL", countryCodeToFlagEmoji("SL")), + CountryData("Singapore", "+65", "SG", countryCodeToFlagEmoji("SG")), + CountryData("Sint Maarten", "+599", "SX", countryCodeToFlagEmoji("SX")), + CountryData("Slovakia", "+421", "SK", countryCodeToFlagEmoji("SK")), + CountryData("Slovenia", "+386", "SI", countryCodeToFlagEmoji("SI")), + CountryData("Solomon Islands", "+677", "SB", countryCodeToFlagEmoji("SB")), + CountryData("Somalia", "+252", "SO", countryCodeToFlagEmoji("SO")), + CountryData("South Africa", "+27", "ZA", countryCodeToFlagEmoji("ZA")), + CountryData("South Korea", "+82", "KR", countryCodeToFlagEmoji("KR")), + CountryData("South Sudan", "+211", "SS", countryCodeToFlagEmoji("SS")), + CountryData("Spain", "+34", "ES", countryCodeToFlagEmoji("ES")), + CountryData("Sri Lanka", "+94", "LK", countryCodeToFlagEmoji("LK")), + CountryData("Sudan", "+249", "SD", countryCodeToFlagEmoji("SD")), + CountryData("Suriname", "+597", "SR", countryCodeToFlagEmoji("SR")), + CountryData("Swaziland", "+268", "SZ", countryCodeToFlagEmoji("SZ")), + CountryData("Sweden", "+46", "SE", countryCodeToFlagEmoji("SE")), + CountryData("Switzerland", "+41", "CH", countryCodeToFlagEmoji("CH")), + CountryData("Syria", "+963", "SY", countryCodeToFlagEmoji("SY")), + CountryData("Taiwan", "+886", "TW", countryCodeToFlagEmoji("TW")), + CountryData("Tajikistan", "+992", "TJ", countryCodeToFlagEmoji("TJ")), + CountryData("Tanzania", "+255", "TZ", countryCodeToFlagEmoji("TZ")), + CountryData("Thailand", "+66", "TH", countryCodeToFlagEmoji("TH")), + CountryData("Timor-Leste", "+670", "TL", countryCodeToFlagEmoji("TL")), + CountryData("Togo", "+228", "TG", countryCodeToFlagEmoji("TG")), + CountryData("Tokelau", "+690", "TK", countryCodeToFlagEmoji("TK")), + CountryData("Tonga", "+676", "TO", countryCodeToFlagEmoji("TO")), + CountryData("Trinidad and Tobago", "+868", "TT", countryCodeToFlagEmoji("TT")), + CountryData("Tunisia", "+216", "TN", countryCodeToFlagEmoji("TN")), + CountryData("Turkey", "+90", "TR", countryCodeToFlagEmoji("TR")), + CountryData("Turkmenistan", "+993", "TM", countryCodeToFlagEmoji("TM")), + CountryData("Turks and Caicos Islands", "+649", "TC", countryCodeToFlagEmoji("TC")), + CountryData("Tuvalu", "+688", "TV", countryCodeToFlagEmoji("TV")), + CountryData("Uganda", "+256", "UG", countryCodeToFlagEmoji("UG")), + CountryData("Ukraine", "+380", "UA", countryCodeToFlagEmoji("UA")), + CountryData("United Arab Emirates", "+971", "AE", countryCodeToFlagEmoji("AE")), + CountryData("United Kingdom", "+44", "GB", countryCodeToFlagEmoji("GB")), + CountryData("United States", "+1", "US", countryCodeToFlagEmoji("US")), + CountryData("Uruguay", "+598", "UY", countryCodeToFlagEmoji("UY")), + CountryData("Uzbekistan", "+998", "UZ", countryCodeToFlagEmoji("UZ")), + CountryData("Vanuatu", "+678", "VU", countryCodeToFlagEmoji("VU")), + CountryData("Vatican City", "+379", "VA", countryCodeToFlagEmoji("VA")), + CountryData("Venezuela", "+58", "VE", countryCodeToFlagEmoji("VE")), + CountryData("Vietnam", "+84", "VN", countryCodeToFlagEmoji("VN")), + CountryData("Virgin Islands (British)", "+284", "VG", countryCodeToFlagEmoji("VG")), + CountryData("Virgin Islands (U.S.)", "+340", "VI", countryCodeToFlagEmoji("VI")), + CountryData("Wallis and Futuna", "+681", "WF", countryCodeToFlagEmoji("WF")), + CountryData("Western Sahara", "+212", "EH", countryCodeToFlagEmoji("EH")), + CountryData("Yemen", "+967", "YE", countryCodeToFlagEmoji("YE")), + CountryData("Zambia", "+260", "ZM", countryCodeToFlagEmoji("ZM")), + CountryData("Zimbabwe", "+263", "ZW", countryCodeToFlagEmoji("ZW")) +) diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/data/CountryData.kt b/auth/src/main/java/com/firebase/ui/auth/compose/data/CountryData.kt new file mode 100644 index 000000000..7deb276b7 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/data/CountryData.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.compose.data + +/** + * Represents country information for phone number authentication. + * + * @property name The display name of the country (e.g., "United States"). + * @property dialCode The international dialing code (e.g., "+1"). + * @property countryCode The ISO 3166-1 alpha-2 country code (e.g., "US"). + * @property flagEmoji The flag emoji for the country (e.g., "🇺🇸"). + */ +data class CountryData( + val name: String, + val dialCode: String, + val countryCode: String, + val flagEmoji: String +) { + /** + * Returns a formatted display string combining flag emoji and country name. + */ + fun getDisplayName(): String = "$flagEmoji $name" + + /** + * Returns a formatted string with dial code. + */ + fun getDisplayNameWithDialCode(): String = "$flagEmoji $name ($dialCode)" +} + +/** + * Converts an ISO 3166-1 alpha-2 country code to its corresponding flag emoji. + * + * @param countryCode The two-letter country code (e.g., "US", "GB", "FR"). + * @return The flag emoji string, or an empty string if the code is invalid. + */ +fun countryCodeToFlagEmoji(countryCode: String): String { + if (countryCode.length != 2) return "" + + val uppercaseCode = countryCode.uppercase() + val baseCodePoint = 0x1F1E6 // Regional Indicator Symbol Letter A + val charCodeOffset = 'A'.code + + val firstChar = uppercaseCode[0].code + val secondChar = uppercaseCode[1].code + + val firstCodePoint = baseCodePoint + (firstChar - charCodeOffset) + val secondCodePoint = baseCodePoint + (secondChar - charCodeOffset) + + return String(intArrayOf(firstCodePoint, secondCodePoint), 0, 2) +} diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/data/CountryUtils.kt b/auth/src/main/java/com/firebase/ui/auth/compose/data/CountryUtils.kt new file mode 100644 index 000000000..e6ef16f06 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/data/CountryUtils.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.compose.data + +import java.text.Normalizer +import java.util.Locale + +/** + * Utility functions for searching and filtering countries. + */ +object CountryUtils { + + // Lazy-initialized maps for fast lookups + private val countryCodeMap: Map by lazy { + ALL_COUNTRIES.associateBy { it.countryCode.uppercase() } + } + + private val dialCodeMap: Map> by lazy { + ALL_COUNTRIES.groupBy { it.dialCode } + } + + /** + * Finds a country by its ISO 3166-1 alpha-2 country code. + * + * @param countryCode The two-letter country code (e.g., "US", "GB"). + * @return The CountryData or null if not found. + */ + fun findByCountryCode(countryCode: String): CountryData? { + return countryCodeMap[countryCode.uppercase()] + } + + /** + * Finds all countries with the given dial code. + * + * @param dialCode The international dialing code (e.g., "+1", "+44"). + * @return List of countries with that dial code, or empty list if none found. + */ + fun findByDialCode(dialCode: String): List { + return dialCodeMap[dialCode] ?: emptyList() + } + + /** + * Searches for countries by name. Supports partial matching and diacritic-insensitive search. + * + * @param query The search query. + * @return List of countries matching the query, or empty list if none found. + */ + fun searchByName(query: String): List { + val trimmedQuery = query.trim() + if (trimmedQuery.isEmpty()) return emptyList() + + val normalizedQuery = normalizeString(trimmedQuery) + + return ALL_COUNTRIES.filter { country -> + normalizeString(country.name).contains(normalizedQuery, ignoreCase = true) + } + } + + /** + * Filters countries by allowed country codes. + * + * @param allowedCountryCodes Set of allowed ISO 3166-1 alpha-2 country codes. + * @return List of countries that are in the allowed set. + */ + fun filterByAllowedCountries(allowedCountryCodes: Set): List { + if (allowedCountryCodes.isEmpty()) return ALL_COUNTRIES + + val uppercaseAllowed = allowedCountryCodes.map { it.uppercase() }.toSet() + return ALL_COUNTRIES.filter { it.countryCode.uppercase() in uppercaseAllowed } + } + + /** + * Gets the default country based on the device's locale. + * + * @return The CountryData for the device's country, or United States as fallback. + */ + fun getDefaultCountry(): CountryData { + val deviceCountryCode = Locale.getDefault().country + return findByCountryCode(deviceCountryCode) ?: findByCountryCode("US")!! + } + + /** + * Formats a phone number with the country's dial code. + * + * @param dialCode The country dial code (e.g., "+1"). + * @param phoneNumber The local phone number. + * @return The formatted international phone number. + */ + fun formatPhoneNumber(dialCode: String, phoneNumber: String): String { + val cleanNumber = phoneNumber.replace(Regex("[^0-9]"), "") + return "$dialCode$cleanNumber" + } + + /** + * Normalizes a string by removing diacritics and converting to lowercase. + * + * @param value The string to normalize. + * @return The normalized string. + */ + private fun normalizeString(value: String): String { + return Normalizer.normalize(value, Normalizer.Form.NFD) + .replace(Regex("\\p{M}"), "") + .lowercase() + } +} diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/data/CountryDataTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/data/CountryDataTest.kt new file mode 100644 index 000000000..8c7064f8f --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/compose/data/CountryDataTest.kt @@ -0,0 +1,347 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.compose.data + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Unit tests for [CountryData] and related utilities. + * + * @suppress Internal test class + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class CountryDataTest { + + // ============================================================================================= + // CountryData Tests + // ============================================================================================= + + @Test + fun `CountryData has correct properties`() { + val country = CountryData( + name = "United States", + dialCode = "+1", + countryCode = "US", + flagEmoji = "🇺🇸" + ) + + assertThat(country.name).isEqualTo("United States") + assertThat(country.dialCode).isEqualTo("+1") + assertThat(country.countryCode).isEqualTo("US") + assertThat(country.flagEmoji).isEqualTo("🇺🇸") + } + + @Test + fun `getDisplayName returns formatted name with flag`() { + val country = CountryData( + name = "United Kingdom", + dialCode = "+44", + countryCode = "GB", + flagEmoji = "🇬🇧" + ) + + assertThat(country.getDisplayName()).isEqualTo("🇬🇧 United Kingdom") + } + + @Test + fun `getDisplayNameWithDialCode returns formatted name with flag and dial code`() { + val country = CountryData( + name = "France", + dialCode = "+33", + countryCode = "FR", + flagEmoji = "🇫🇷" + ) + + assertThat(country.getDisplayNameWithDialCode()).isEqualTo("🇫🇷 France (+33)") + } + + // ============================================================================================= + // Flag Emoji Tests + // ============================================================================================= + + @Test + fun `countryCodeToFlagEmoji generates correct emoji for US`() { + val emoji = countryCodeToFlagEmoji("US") + assertThat(emoji).isEqualTo("🇺🇸") + } + + @Test + fun `countryCodeToFlagEmoji generates correct emoji for GB`() { + val emoji = countryCodeToFlagEmoji("GB") + assertThat(emoji).isEqualTo("🇬🇧") + } + + @Test + fun `countryCodeToFlagEmoji generates correct emoji for FR`() { + val emoji = countryCodeToFlagEmoji("FR") + assertThat(emoji).isEqualTo("🇫🇷") + } + + @Test + fun `countryCodeToFlagEmoji works with lowercase codes`() { + val emoji = countryCodeToFlagEmoji("de") + assertThat(emoji).isEqualTo("🇩🇪") + } + + @Test + fun `countryCodeToFlagEmoji returns empty string for invalid code length`() { + val emoji = countryCodeToFlagEmoji("USA") + assertThat(emoji).isEmpty() + } + + @Test + fun `countryCodeToFlagEmoji returns empty string for empty code`() { + val emoji = countryCodeToFlagEmoji("") + assertThat(emoji).isEmpty() + } + + @Test + fun `countryCodeToFlagEmoji returns empty string for single character`() { + val emoji = countryCodeToFlagEmoji("U") + assertThat(emoji).isEmpty() + } + + // ============================================================================================= + // Country List Tests + // ============================================================================================= + + @Test + fun `ALL_COUNTRIES contains expected number of countries`() { + assertThat(ALL_COUNTRIES).isNotEmpty() + assertThat(ALL_COUNTRIES.size).isGreaterThan(200) + } + + @Test + fun `ALL_COUNTRIES contains United States`() { + val us = ALL_COUNTRIES.find { it.countryCode == "US" } + assertThat(us).isNotNull() + assertThat(us?.name).isEqualTo("United States") + assertThat(us?.dialCode).isEqualTo("+1") + } + + @Test + fun `ALL_COUNTRIES contains United Kingdom`() { + val uk = ALL_COUNTRIES.find { it.countryCode == "GB" } + assertThat(uk).isNotNull() + assertThat(uk?.name).isEqualTo("United Kingdom") + assertThat(uk?.dialCode).isEqualTo("+44") + } + + @Test + fun `ALL_COUNTRIES contains France`() { + val france = ALL_COUNTRIES.find { it.countryCode == "FR" } + assertThat(france).isNotNull() + assertThat(france?.name).isEqualTo("France") + assertThat(france?.dialCode).isEqualTo("+33") + } + + @Test + fun `ALL_COUNTRIES has no duplicate country codes`() { + val countryCodes = ALL_COUNTRIES.map { it.countryCode } + val uniqueCodes = countryCodes.toSet() + assertThat(countryCodes.size).isEqualTo(uniqueCodes.size) + } + + @Test + fun `ALL_COUNTRIES all entries have valid flag emojis`() { + ALL_COUNTRIES.forEach { country -> + assertThat(country.flagEmoji).isNotEmpty() + } + } + + @Test + fun `ALL_COUNTRIES all entries have dial codes starting with plus`() { + ALL_COUNTRIES.forEach { country -> + assertThat(country.dialCode).startsWith("+") + } + } + + @Test + fun `ALL_COUNTRIES all entries have two-letter country codes`() { + ALL_COUNTRIES.forEach { country -> + assertThat(country.countryCode).hasLength(2) + } + } + + // ============================================================================================= + // CountryUtils - Lookup Tests + // ============================================================================================= + + @Test + fun `findByCountryCode returns correct country for US`() { + val country = CountryUtils.findByCountryCode("US") + assertThat(country).isNotNull() + assertThat(country?.name).isEqualTo("United States") + assertThat(country?.dialCode).isEqualTo("+1") + } + + @Test + fun `findByCountryCode is case insensitive`() { + val country = CountryUtils.findByCountryCode("us") + assertThat(country).isNotNull() + assertThat(country?.countryCode).isEqualTo("US") + } + + @Test + fun `findByCountryCode returns null for invalid code`() { + val country = CountryUtils.findByCountryCode("XX") + assertThat(country).isNull() + } + + @Test + fun `findByDialCode returns countries with +1 dial code`() { + val countries = CountryUtils.findByDialCode("+1") + assertThat(countries).isNotEmpty() + assertThat(countries.map { it.countryCode }).contains("US") + assertThat(countries.map { it.countryCode }).contains("CA") + } + + @Test + fun `findByDialCode returns countries with +44 dial code`() { + val countries = CountryUtils.findByDialCode("+44") + assertThat(countries).isNotEmpty() + val countryCodes = countries.map { it.countryCode } + assertThat(countryCodes).contains("GB") + } + + @Test + fun `findByDialCode returns empty list for non-existent dial code`() { + val countries = CountryUtils.findByDialCode("+9999") + assertThat(countries).isEmpty() + } + + // ============================================================================================= + // CountryUtils - Search Tests + // ============================================================================================= + + @Test + fun `searchByName finds United States`() { + val countries = CountryUtils.searchByName("United States") + assertThat(countries).isNotEmpty() + assertThat(countries[0].countryCode).isEqualTo("US") + } + + @Test + fun `searchByName finds countries with partial match`() { + val countries = CountryUtils.searchByName("United") + assertThat(countries).isNotEmpty() + val names = countries.map { it.name } + assertThat(names).contains("United States") + assertThat(names).contains("United Kingdom") + assertThat(names).contains("United Arab Emirates") + } + + @Test + fun `searchByName is case insensitive`() { + val countries = CountryUtils.searchByName("france") + assertThat(countries).isNotEmpty() + assertThat(countries[0].countryCode).isEqualTo("FR") + } + + @Test + fun `searchByName handles diacritics`() { + val countries = CountryUtils.searchByName("Cote d'Ivoire") + assertThat(countries).isNotEmpty() + assertThat(countries[0].countryCode).isEqualTo("CI") + } + + @Test + fun `searchByName returns empty list for empty query`() { + val countries = CountryUtils.searchByName("") + assertThat(countries).isEmpty() + } + + @Test + fun `searchByName returns empty list for whitespace query`() { + val countries = CountryUtils.searchByName(" ") + assertThat(countries).isEmpty() + } + + // ============================================================================================= + // CountryUtils - Filter Tests + // ============================================================================================= + + @Test + fun `filterByAllowedCountries returns only allowed countries`() { + val allowedCodes = setOf("US", "GB", "FR") + val filtered = CountryUtils.filterByAllowedCountries(allowedCodes) + + assertThat(filtered).hasSize(3) + val countryCodes = filtered.map { it.countryCode } + assertThat(countryCodes).containsExactly("US", "GB", "FR") + } + + @Test + fun `filterByAllowedCountries is case insensitive`() { + val allowedCodes = setOf("us", "gb") + val filtered = CountryUtils.filterByAllowedCountries(allowedCodes) + + assertThat(filtered).hasSize(2) + val countryCodes = filtered.map { it.countryCode } + assertThat(countryCodes).containsExactly("US", "GB") + } + + @Test + fun `filterByAllowedCountries returns all countries when set is empty`() { + val filtered = CountryUtils.filterByAllowedCountries(emptySet()) + assertThat(filtered).hasSize(ALL_COUNTRIES.size) + } + + @Test + fun `filterByAllowedCountries returns empty list for non-existent codes`() { + val allowedCodes = setOf("XX", "YY") + val filtered = CountryUtils.filterByAllowedCountries(allowedCodes) + assertThat(filtered).isEmpty() + } + + // ============================================================================================= + // CountryUtils - Default Country Tests + // ============================================================================================= + + @Test + fun `getDefaultCountry returns a valid country`() { + val country = CountryUtils.getDefaultCountry() + assertThat(country).isNotNull() + assertThat(country.countryCode).isNotEmpty() + assertThat(country.dialCode).isNotEmpty() + } + + // ============================================================================================= + // CountryUtils - Formatting Tests + // ============================================================================================= + + @Test + fun `formatPhoneNumber combines dial code and phone number`() { + val formatted = CountryUtils.formatPhoneNumber("+1", "5551234567") + assertThat(formatted).isEqualTo("+15551234567") + } + + @Test + fun `formatPhoneNumber removes non-numeric characters from phone number`() { + val formatted = CountryUtils.formatPhoneNumber("+1", "(555) 123-4567") + assertThat(formatted).isEqualTo("+15551234567") + } + + @Test + fun `formatPhoneNumber handles phone number with spaces`() { + val formatted = CountryUtils.formatPhoneNumber("+44", "20 1234 5678") + assertThat(formatted).isEqualTo("+442012345678") + } +}