diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c458d7..0d28982 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,25 +14,26 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.6" composeBom = "2024.09.03" +startupRuntime = "1.2.0" [libraries] androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } -androidx-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" } -androidx-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" } -kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } -libphonenumber-jvm = { module = "com.googlecode.libphonenumber:libphonenumber", version.ref = "libphonenumberJvm" } -libphonenumber-android = { module = "io.michaelrocks:libphonenumber-android", version.ref = "libphonenumberAndroid" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startupRuntime" } +androidx-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } +libphonenumber-jvm = { module = "com.googlecode.libphonenumber:libphonenumber", version.ref = "libphonenumberJvm" } +libphonenumber-android = { module = "io.michaelrocks:libphonenumber-android", version.ref = "libphonenumberAndroid" } [plugins] cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 63c36fb..4d0adcc 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -18,6 +18,10 @@ plugins { } kotlin { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } // cocoapods { // version = "1.0.0" // summary = "Yet Another Kotlin COmpose Validation library" @@ -106,7 +110,8 @@ kotlin { androidMain.dependencies { compileOnly(compose.uiTooling) implementation(libs.androidx.activityCompose) - api(libs.libphonenumber.android) + implementation(libs.libphonenumber.android) + implementation(libs.androidx.startup.runtime) } jvmMain.dependencies { @@ -154,6 +159,9 @@ android { systemImageSource = "aosp" } } + unitTests { + isIncludeAndroidResources = true + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/library/src/androidInstrumentedTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.android.kt b/library/src/androidInstrumentedTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.android.kt new file mode 100644 index 0000000..de864f0 --- /dev/null +++ b/library/src/androidInstrumentedTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.android.kt @@ -0,0 +1,3 @@ +package com.chrisjenx.yakcov + +actual fun initPhoneNumberUtil() = Unit \ No newline at end of file diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml index fa57d87..95869d5 100644 --- a/library/src/androidMain/AndroidManifest.xml +++ b/library/src/androidMain/AndroidManifest.xml @@ -1,2 +1,17 @@ - \ No newline at end of file + + + + + + + + + diff --git a/library/src/androidMain/kotlin/com/chrisjenx/yakcov/Regex.android.kt b/library/src/androidMain/kotlin/com/chrisjenx/yakcov/Regex.android.kt index 6192b35..066cbf6 100644 --- a/library/src/androidMain/kotlin/com/chrisjenx/yakcov/Regex.android.kt +++ b/library/src/androidMain/kotlin/com/chrisjenx/yakcov/Regex.android.kt @@ -1,10 +1,15 @@ package com.chrisjenx.yakcov +import android.content.Context +import androidx.startup.Initializer import io.michaelrocks.libphonenumber.android.PhoneNumberUtil -import io.michaelrocks.libphonenumber.android.metadata.init.ClassPathResourceMetadataLoader +import io.michaelrocks.libphonenumber.android.metadata.source.AssetsMetadataLoader +import org.jetbrains.annotations.VisibleForTesting -private val phoneUtil = PhoneNumberUtil.createInstance(ClassPathResourceMetadataLoader()) +@VisibleForTesting +internal lateinit var phoneUtil: PhoneNumberUtil + /** * Check if is phone number to best ability of each platform. @@ -19,3 +24,20 @@ actual fun String?.isPhoneNumber(defaultRegion: String?): Boolean { false } } + +/** + * Will init the Android phone number util at app startup using androidx.startup, see manifest. + */ +class PhoneNumberUtilInitializer : Initializer { + override fun create(context: Context): PhoneNumberUtil { + val assetManager = AssetsMetadataLoader(context.applicationContext.assets) + val instance = PhoneNumberUtil.createInstance(assetManager) + phoneUtil = instance // Set global + return instance + } + + override fun dependencies(): List>> { + return emptyList() + } +} + diff --git a/library/src/androidUnitTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.android.kt b/library/src/androidUnitTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.android.kt new file mode 100644 index 0000000..3491d7d --- /dev/null +++ b/library/src/androidUnitTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.android.kt @@ -0,0 +1,4 @@ +package com.chrisjenx.yakcov + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IOSIgnore \ No newline at end of file diff --git a/library/src/androidUnitTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.android.kt b/library/src/androidUnitTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.android.kt new file mode 100644 index 0000000..52dc48c --- /dev/null +++ b/library/src/androidUnitTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.android.kt @@ -0,0 +1,8 @@ +package com.chrisjenx.yakcov + +import io.michaelrocks.libphonenumber.android.PhoneNumberUtil +import io.michaelrocks.libphonenumber.android.metadata.init.ClassPathResourceMetadataLoader + +actual fun initPhoneNumberUtil() { + phoneUtil = PhoneNumberUtil.createInstance(ClassPathResourceMetadataLoader()) +} diff --git a/library/src/androidUnitTest/resources/io/michaelrocks/libphonenumber/android/data/PhoneNumberMetadataProto_GB b/library/src/androidUnitTest/resources/io/michaelrocks/libphonenumber/android/data/PhoneNumberMetadataProto_GB new file mode 100644 index 0000000..77dfe79 Binary files /dev/null and b/library/src/androidUnitTest/resources/io/michaelrocks/libphonenumber/android/data/PhoneNumberMetadataProto_GB differ diff --git a/library/src/commonMain/kotlin/com/chrisjenx/yakcov/RegexExt.kt b/library/src/commonMain/kotlin/com/chrisjenx/yakcov/RegexExt.kt index 92117b1..00e6b0e 100644 --- a/library/src/commonMain/kotlin/com/chrisjenx/yakcov/RegexExt.kt +++ b/library/src/commonMain/kotlin/com/chrisjenx/yakcov/RegexExt.kt @@ -21,4 +21,4 @@ fun String?.isEmail(): Boolean { * @param defaultRegion The default region to use if the number is not in international format. * it's two digits country code. e.g. "US", "GB", "ES" */ -expect fun String?.isPhoneNumber(defaultRegion: String? = null): Boolean +expect fun String?.isPhoneNumber(defaultRegion: String? = "US"): Boolean diff --git a/library/src/commonTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.kt b/library/src/commonTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.kt new file mode 100644 index 0000000..996a854 --- /dev/null +++ b/library/src/commonTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.kt @@ -0,0 +1,4 @@ +package com.chrisjenx.yakcov + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +expect annotation class IOSIgnore() diff --git a/library/src/commonTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.kt b/library/src/commonTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.kt index 027df76..88fa646 100644 --- a/library/src/commonTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.kt +++ b/library/src/commonTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.kt @@ -1,11 +1,17 @@ package com.chrisjenx.yakcov +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue class PhoneNumberTest { + @BeforeTest + fun setUp() { + initPhoneNumberUtil() + } + @Test fun isPhoneNumber_fail() { assertFalse("43435".isPhoneNumber()) @@ -21,5 +27,6 @@ class PhoneNumberTest { assertTrue("6508991234".isPhoneNumber()) } - } + +expect fun initPhoneNumberUtil() diff --git a/library/src/commonTest/kotlin/com/chrisjenx/yakcov/strings/PhoneRuleTest.kt b/library/src/commonTest/kotlin/com/chrisjenx/yakcov/strings/PhoneRuleTest.kt index a350c00..9ed5a38 100644 --- a/library/src/commonTest/kotlin/com/chrisjenx/yakcov/strings/PhoneRuleTest.kt +++ b/library/src/commonTest/kotlin/com/chrisjenx/yakcov/strings/PhoneRuleTest.kt @@ -1,11 +1,19 @@ package com.chrisjenx.yakcov.strings +import com.chrisjenx.yakcov.IOSIgnore import com.chrisjenx.yakcov.ValidationResult.Outcome +import com.chrisjenx.yakcov.initPhoneNumberUtil +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals class PhoneRuleTest { + @BeforeTest + fun setUp() { + initPhoneNumberUtil() + } + @Test fun phoneNumber_invalid() { assertEquals(Outcome.ERROR, Phone().validate("43435").outcome()) @@ -17,6 +25,7 @@ class PhoneRuleTest { } @Test + @IOSIgnore fun phoneNumber_wrongRegion() { // This is a UK number should error for US assertEquals(Outcome.ERROR, Phone("US").validate("07745973912").outcome()) @@ -27,5 +36,6 @@ class PhoneRuleTest { fun phoneNumber_withRegion() { assertEquals(Outcome.SUCCESS, Phone().validate("+16508991234").outcome()) } + } diff --git a/library/src/iosTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.ios.kt b/library/src/iosTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.ios.kt new file mode 100644 index 0000000..bc6d4ad --- /dev/null +++ b/library/src/iosTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.ios.kt @@ -0,0 +1,3 @@ +package com.chrisjenx.yakcov + +actual typealias IOSIgnore = kotlin.test.Ignore \ No newline at end of file diff --git a/library/src/iosTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.ios.kt b/library/src/iosTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.ios.kt new file mode 100644 index 0000000..de864f0 --- /dev/null +++ b/library/src/iosTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.ios.kt @@ -0,0 +1,3 @@ +package com.chrisjenx.yakcov + +actual fun initPhoneNumberUtil() = Unit \ No newline at end of file diff --git a/library/src/jsTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.js.kt b/library/src/jsTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.js.kt new file mode 100644 index 0000000..3491d7d --- /dev/null +++ b/library/src/jsTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.js.kt @@ -0,0 +1,4 @@ +package com.chrisjenx.yakcov + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IOSIgnore \ No newline at end of file diff --git a/library/src/jsTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.js.kt b/library/src/jsTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.js.kt new file mode 100644 index 0000000..de864f0 --- /dev/null +++ b/library/src/jsTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.js.kt @@ -0,0 +1,3 @@ +package com.chrisjenx.yakcov + +actual fun initPhoneNumberUtil() = Unit \ No newline at end of file diff --git a/library/src/jvmTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.jvm.kt b/library/src/jvmTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.jvm.kt new file mode 100644 index 0000000..3491d7d --- /dev/null +++ b/library/src/jvmTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.jvm.kt @@ -0,0 +1,4 @@ +package com.chrisjenx.yakcov + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IOSIgnore \ No newline at end of file diff --git a/library/src/jvmTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.jvm.kt b/library/src/jvmTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.jvm.kt new file mode 100644 index 0000000..de864f0 --- /dev/null +++ b/library/src/jvmTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.jvm.kt @@ -0,0 +1,3 @@ +package com.chrisjenx.yakcov + +actual fun initPhoneNumberUtil() = Unit \ No newline at end of file diff --git a/library/src/wasmJsTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.wasmJs.kt b/library/src/wasmJsTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.wasmJs.kt new file mode 100644 index 0000000..3491d7d --- /dev/null +++ b/library/src/wasmJsTest/kotlin/com/chrisjenx/yakcov/IOSIgnore.wasmJs.kt @@ -0,0 +1,4 @@ +package com.chrisjenx.yakcov + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +actual annotation class IOSIgnore \ No newline at end of file diff --git a/library/src/wasmJsTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.wasmJs.kt b/library/src/wasmJsTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.wasmJs.kt new file mode 100644 index 0000000..de864f0 --- /dev/null +++ b/library/src/wasmJsTest/kotlin/com/chrisjenx/yakcov/PhoneNumberTest.wasmJs.kt @@ -0,0 +1,3 @@ +package com.chrisjenx.yakcov + +actual fun initPhoneNumberUtil() = Unit \ No newline at end of file diff --git a/sample/src/main/java/com/chrisjenx/yakcov/sample/SampleActivity.kt b/sample/src/main/java/com/chrisjenx/yakcov/sample/SampleActivity.kt index 235cc9b..2f369e1 100644 --- a/sample/src/main/java/com/chrisjenx/yakcov/sample/SampleActivity.kt +++ b/sample/src/main/java/com/chrisjenx/yakcov/sample/SampleActivity.kt @@ -13,8 +13,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button @@ -27,14 +29,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.chrisjenx.yakcov.generic.IsChecked import com.chrisjenx.yakcov.generic.ListNotEmpty @@ -43,6 +43,7 @@ import com.chrisjenx.yakcov.sample.ui.theme.YakcovTheme import com.chrisjenx.yakcov.strings.Email import com.chrisjenx.yakcov.strings.MinLength import com.chrisjenx.yakcov.strings.PasswordMatches +import com.chrisjenx.yakcov.strings.Phone import com.chrisjenx.yakcov.strings.Required import com.chrisjenx.yakcov.strings.rememberTextFieldValueValidator import com.chrisjenx.yakcov.validate @@ -57,6 +58,7 @@ class SampleActivity : ComponentActivity() { Column( modifier = Modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding(innerPadding) .padding(16.dp) ) { @@ -87,6 +89,29 @@ class SampleActivity : ComponentActivity() { Spacer(modifier = Modifier.height(16.dp)) + val phoneValidator = rememberTextFieldValueValidator( + rules = listOf(Required, Phone()), + ) + with(phoneValidator) { + OutlinedTextField( + value = value, + label = { Text(text = "Phone") }, + modifier = Modifier + .validationConfig(validateOnFocusLost = true) + .fillMaxWidth(), + onValueChange = ::onValueChange, + isError = isError(), + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Phone, + ), + singleLine = true, + supportingText = supportingText() + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "Password", style = MaterialTheme.typography.headlineSmall) // Password example val passwordValidator = rememberTextFieldValueValidator( @@ -255,6 +280,7 @@ class SampleActivity : ComponentActivity() { onClick = { listOf( emailValidator, + phoneValidator, passwordValidator, passwordMatchesValidator, requiredValidator, @@ -274,19 +300,3 @@ class SampleActivity : ComponentActivity() { } } } - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - YakcovTheme { - Greeting("Android") - } -} \ No newline at end of file