diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml new file mode 100644 index 0000000..f84df88 --- /dev/null +++ b/.github/workflows/detekt.yml @@ -0,0 +1,28 @@ +name: Detekt + +on: + merge_group: + workflow_dispatch: + push: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + detekt: + name: Detekt + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup build environment + uses: ./.github/actions/setup + + - name: Run Detekt + run: ./gradlew detekt --continue --console=plain --no-daemon \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4b04427..7d1378f 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ configure(project(':universal_components')) { detekt { buildUponDefaultConfig = true parallel = true + config.setFrom(files("$rootDir/config/detekt/detekt.yml")) source.setFrom(files('src/main/java', 'src/main/kotlin', 'src/test/java', 'src/test/kotlin')) } diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 65a045d..78fe92d 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -30,6 +30,10 @@ formatting: active: true NoWildcardImports: active: true + # authenticator_methods package is public API; renaming would break consumers. + PackageName: + active: true + excludes: [ '**/mfa/authenticator_methods/**' ] complexity: active: true @@ -65,6 +69,11 @@ naming: TopLevelPropertyNaming: active: true constantPattern: '[A-Z][_A-Z0-9]*' + # The authenticator_methods package is part of the published public API. + # Renaming it to drop the underscore would be a breaking change for consumers. + PackageNaming: + active: true + excludes: [ '**/mfa/authenticator_methods/**' ] style: active: true diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/Auth0UniversalComponents.kt b/universal_components/src/main/java/com/auth0/universalcomponents/Auth0UniversalComponents.kt index 4f1b69a..dfaf869 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/Auth0UniversalComponents.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/Auth0UniversalComponents.kt @@ -10,9 +10,11 @@ import java.util.concurrent.atomic.AtomicBoolean /** * Configuration for managing passkey authentication. * - * @param credentialManager User application's existing [CredentialManager] instance. Pass the same instance you use elsewhere in the app to handle passkey operations consistently. + * @param credentialManager User application's existing [CredentialManager] instance. Pass the + * same instance you use elsewhere in the app to handle passkey operations consistently. * @param connection The Auth0 DB connection name to use for passkey enrollment and authentication. - * @param userIdentity Unique identifier of the current user's identity. Needed if the user logged in with a [linked account](https://auth0.com/docs/manage-users/user-accounts/user-account-linking) + * @param userIdentity Unique identifier of the current user's identity. Needed if the user + * logged in with a [linked account](https://auth0.com/docs/manage-users/user-accounts/user-account-linking) */ data class PasskeyConfiguration( val credentialManager: CredentialManager? = null, @@ -80,7 +82,7 @@ public object Auth0UniversalComponents { private fun assertInitialized() { if (!initialized.get()) { - throw IllegalStateException("Auth0UniversalComponents must be initialized first.") + error("Auth0UniversalComponents must be initialized first.") } } } diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/data/MyAccountProvider.kt b/universal_components/src/main/java/com/auth0/universalcomponents/data/MyAccountProvider.kt index 734da36..5b465a4 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/data/MyAccountProvider.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/data/MyAccountProvider.kt @@ -8,7 +8,7 @@ import com.auth0.universalcomponents.BuildConfig /** * Provider class that creates and provides instances of MyAccount from the Auth0 Android SDK. */ -class MyAccountProvider() { +class MyAccountProvider { /** * Creates and returns a MyAccountAPIClient instance configured with the current access token. diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/data/mapper/ErrorMapper.kt b/universal_components/src/main/java/com/auth0/universalcomponents/data/mapper/ErrorMapper.kt index a663d32..2baae0c 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/data/mapper/ErrorMapper.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/data/mapper/ErrorMapper.kt @@ -8,6 +8,8 @@ import com.auth0.universalcomponents.domain.error.Auth0Error internal object ErrorMapper { + private const val HTTP_STATUS_SERVER_ERROR = 500 + /** * Maps any exception to an Auth0Error * Handles AuthenticationException, MyAccountException, and other exceptions @@ -150,7 +152,7 @@ internal object ErrorMapper { ) } - exception.statusCode >= 500 -> Auth0Error.ServerError( + exception.statusCode >= HTTP_STATUS_SERVER_ERROR -> Auth0Error.ServerError( message = "Server error, please try again later", statusCode = exception.statusCode, cause = exception diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/data/network/WithErrorMapping.kt b/universal_components/src/main/java/com/auth0/universalcomponents/data/network/WithErrorMapping.kt index eff75e5..e504e62 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/data/network/WithErrorMapping.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/data/network/WithErrorMapping.kt @@ -11,6 +11,8 @@ import kotlin.coroutines.cancellation.CancellationException * @throws [CancellationException] if the coroutine is cancelled * @throws [com.auth0.universalcomponents.domain.error.Auth0Error] */ +// Intentionally catches all throwables to map them to Auth0Error; CancellationException is rethrown first. +@Suppress("TooGenericExceptionCaught") internal suspend inline fun withErrorMapping( scope: String? = null, execute: suspend () -> T diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/navigation/AuthenticatorSettingsNavigationHost.kt b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/navigation/AuthenticatorSettingsNavigationHost.kt index 6bc3d27..c499544 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/navigation/AuthenticatorSettingsNavigationHost.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/navigation/AuthenticatorSettingsNavigationHost.kt @@ -1,6 +1,7 @@ package com.auth0.universalcomponents.presentation.navigation import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -15,10 +16,12 @@ import com.auth0.universalcomponents.presentation.ui.passkeys.PasskeyEnableScree @Composable internal fun AuthenticatorSettingsNavigationHost( navController: NavHostController, + modifier: Modifier = Modifier, ) { NavHost( navController = navController, - startDestination = AuthenticatorRoute.AuthenticatorMethodList + startDestination = AuthenticatorRoute.AuthenticatorMethodList, + modifier = modifier ) { composable { AuthenticatorMethodsScreen( diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/UiState.kt b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/UiError.kt similarity index 100% rename from universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/UiState.kt rename to universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/UiError.kt diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/components/SnackBar.kt b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/components/SnackBar.kt index e10c935..2b80b7b 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/components/SnackBar.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/components/SnackBar.kt @@ -18,11 +18,11 @@ public fun SnackBar( SnackbarHost( hostState = snackbarHostState, - modifier = Modifier + modifier = modifier .padding(sizes.padding) ) - LaunchedEffect(Unit) { - snackbarHostState.showSnackbar("Copied to clipboard") + LaunchedEffect(message) { + snackbarHostState.showSnackbar(message) } } diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/menu/MenuItem.kt b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/menu/MenuItem.kt index 0ee9bef..4ed951f 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/menu/MenuItem.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/menu/MenuItem.kt @@ -1,7 +1,5 @@ package com.auth0.universalcomponents.presentation.ui.menu -// TODO: Move this to the correct package based on usage - sealed interface MenuAction { object Remove : MenuAction } diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/AuthenticatorSettingsComponent.kt b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/AuthenticatorSettingsComponent.kt index d1e5f73..4d77b0a 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/AuthenticatorSettingsComponent.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/AuthenticatorSettingsComponent.kt @@ -49,7 +49,8 @@ public fun AuthenticatorSettingsComponent( Auth0Theme(configuration = themeConfiguration) { AuthenticatorSettingsNavigationHost( - navController + navController = navController, + modifier = modifier ) } } diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/QREnrollmentScreen.kt b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/QREnrollmentScreen.kt index e5846a2..19df094 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/QREnrollmentScreen.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/QREnrollmentScreen.kt @@ -1,6 +1,7 @@ package com.auth0.universalcomponents.presentation.ui.mfa import android.graphics.Bitmap +import android.util.Log import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -70,6 +71,7 @@ import com.auth0.universalcomponents.presentation.viewmodel.EnrollmentUiState import com.auth0.universalcomponents.presentation.viewmodel.EnrollmentViewModel import com.auth0.universalcomponents.theme.Auth0Theme import com.google.zxing.BarcodeFormat +import com.google.zxing.WriterException import com.google.zxing.qrcode.QRCodeWriter @Composable @@ -529,22 +531,33 @@ private fun generateQRCode( return try { val writer = QRCodeWriter() val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size) - val width = bitMatrix.width - val height = bitMatrix.height - val bitmap = createBitmap(width, height) - - for (x in 0 until width) { - for (y in 0 until height) { - bitmap[x, y] = if (bitMatrix[x, y]) { - qrCodeColor.toArgb() - } else { - qrBackgroundColor.toArgb() - } + createQRBitmap(bitMatrix, qrCodeColor, qrBackgroundColor) + } catch (e: WriterException) { + Log.e("TAG", "Failed to generate QR code", e) + null + } +} + +/** + * Creates a bitmap from a QR code bit matrix with specified colors. + */ +private fun createQRBitmap( + bitMatrix: com.google.zxing.common.BitMatrix, + qrCodeColor: Color, + qrBackgroundColor: Color +): Bitmap { + val width = bitMatrix.width + val height = bitMatrix.height + val bitmap = createBitmap(width, height) + + for (x in 0 until width) { + for (y in 0 until height) { + bitmap[x, y] = if (bitMatrix[x, y]) { + qrCodeColor.toArgb() + } else { + qrBackgroundColor.toArgb() } } - bitmap - } catch (e: Exception) { - e.printStackTrace() - null } + return bitmap } diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/phone/CountryData.kt b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/phone/Country.kt similarity index 100% rename from universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/phone/CountryData.kt rename to universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/mfa/phone/Country.kt diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/passkeys/PasskeyViewModel.kt b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/passkeys/PasskeyViewModel.kt index a0c7d2d..cde85fd 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/passkeys/PasskeyViewModel.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/presentation/ui/passkeys/PasskeyViewModel.kt @@ -97,7 +97,7 @@ class PasskeyViewModel( _uiState.update { PasskeyUiState.EnrollingPasskey } - val result = myAccountRepository.verifyPasskey( + myAccountRepository.verifyPasskey( publicKeyCredentials, challenge, SCOPE diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/theme/Auth0Typography.kt b/universal_components/src/main/java/com/auth0/universalcomponents/theme/Auth0Typography.kt index 2ec0496..a027c63 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/theme/Auth0Typography.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/theme/Auth0Typography.kt @@ -40,9 +40,11 @@ data class Auth0Typography( // Other styles - /** Helper text for hints, validation messages, and supplementary information. Inter Regular 13sp / 18sp line height / 0.2sp tracking. */ + /** Helper text for hints, validation messages, and supplementary information. + * Inter Regular 13sp / 18sp line height / 0.2sp tracking. */ val helper: TextStyle, - /** Overline text for tags, categories, and small annotations. Inter Regular 11sp / 16sp line height / 0.77sp tracking. */ + /** Overline text for tags, categories, and small annotations. + * Inter Regular 11sp / 16sp line height / 0.77sp tracking. */ val overline: TextStyle, ) { companion object { diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/utils/CredentialManagerUtil.kt b/universal_components/src/main/java/com/auth0/universalcomponents/utils/CredentialManagerUtil.kt index ccc0e84..5d958c7 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/utils/CredentialManagerUtil.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/utils/CredentialManagerUtil.kt @@ -15,6 +15,6 @@ suspend fun createCredential( val request = CreatePublicKeyCredentialRequest(authParamsJson) val response = credentialsManager.createCredential(context, request) val publicKeyResponse = response as? CreatePublicKeyCredentialResponse - ?: throw IllegalStateException("Unexpected credential response type: ${response::class.java.name}") + ?: error("Unexpected credential response type: ${response::class.java.name}") return publicKeyResponse.registrationResponseJson } diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/utils/DateUtil.kt b/universal_components/src/main/java/com/auth0/universalcomponents/utils/DateUtil.kt index ad62ffe..2d71477 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/utils/DateUtil.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/utils/DateUtil.kt @@ -2,9 +2,12 @@ package com.auth0.universalcomponents.utils import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException object DateUtil { + private const val ISO_DATE_PREFIX_LENGTH = 10 + /** * Formats ISO8601 date string to M/dd/yy format */ @@ -12,8 +15,8 @@ object DateUtil { return try { val dateTime = LocalDateTime.parse(isoDate, DateTimeFormatter.ISO_DATE_TIME) dateTime.format(DateTimeFormatter.ofPattern("M/dd/yy")) - } catch (e: Exception) { - isoDate.take(10) + } catch (e: DateTimeParseException) { + isoDate.take(ISO_DATE_PREFIX_LENGTH) } } } diff --git a/universal_components/src/main/java/com/auth0/universalcomponents/utils/ValidationUtil.kt b/universal_components/src/main/java/com/auth0/universalcomponents/utils/ValidationUtil.kt index ff48b4d..99e57b1 100644 --- a/universal_components/src/main/java/com/auth0/universalcomponents/utils/ValidationUtil.kt +++ b/universal_components/src/main/java/com/auth0/universalcomponents/utils/ValidationUtil.kt @@ -8,6 +8,8 @@ import android.util.Patterns */ object ValidationUtil { + private const val MIN_PHONE_NUMBER_LENGTH = 6 + /** * Validates if an email address is in a valid format * Uses Android's built-in email pattern matcher @@ -27,7 +29,7 @@ object ValidationUtil { */ fun isValidPhoneNumber(phoneNumber: String): Boolean { val digitsOnly = phoneNumber.replace(Regex("[^0-9]"), "") - return digitsOnly.length >= 6 + return digitsOnly.length >= MIN_PHONE_NUMBER_LENGTH } /** diff --git a/universal_components/src/test/java/com/auth0/universalcomponents/ExampleUnitTest.kt b/universal_components/src/test/java/com/auth0/universalcomponents/ExampleUnitTest.kt index 0b9d58a..c4b94cd 100644 --- a/universal_components/src/test/java/com/auth0/universalcomponents/ExampleUnitTest.kt +++ b/universal_components/src/test/java/com/auth0/universalcomponents/ExampleUnitTest.kt @@ -1,6 +1,6 @@ package com.auth0.universalcomponents -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.Test /**