Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
Expand Down Expand Up @@ -84,7 +83,10 @@ android {
}
}

@Suppress("UNCHECKED_CAST")
fun getVersionName(): String = (project.extra["getVersionName"] as () -> String)()

@Suppress("UNCHECKED_CAST")
fun getVersionCode(): Int = (project.extra["getVersionCode"] as () -> Int)()

dependencies {
Expand Down Expand Up @@ -114,7 +116,6 @@ dependencies {

androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.ui.test.junit4.android)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
Expand Down
17 changes: 6 additions & 11 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ androidCoreKtx = "1.17.0"
androidLifecycleRuntimeKtx = "2.9.4"
appcompat = "1.7.1"
bouncycastle = "1.79"
compose = "1.7.6"
composeBom = "2025.09.00"
compose-material3 = "1.3.1"
composePayButton = "1.1.0"
Expand All @@ -16,7 +15,7 @@ coilNetworkOkhttp = "3.3.0"
coilTest = "3.3.0"
datastorePreferences = "1.1.7"
dokka = "2.0.0"
espressoCore = "3.5.0"
espressoCore = "3.7.0"
firebaseBom = "34.4.0"
google-services-plugin = "4.4.4"
io-mockk = "1.14.6"
Expand All @@ -31,8 +30,6 @@ kotlinxSerializationJson = "1.9.0"
playServicesWallet = "19.4.0"
slf4jApi = "2.0.17"
sonarqube-plugin = "6.3.1.5724"
uiTooling = "1.9.1"
uiTestJunit4Android = "1.9.3"
runtime = "1.0.0-alpha06"
junitKtx = "1.3.0"

Expand All @@ -49,10 +46,10 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidLifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidLifecycleRuntimeKtx" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material3" }
androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "compose" }
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", version.ref = "compose" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk15to18", version.ref = "bouncycastle" }
Expand All @@ -68,14 +65,13 @@ kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-cor
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
play-services-wallet = { module = "com.google.android.gms:play-services-wallet", version.ref = "playServicesWallet" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" }
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
androidx-ui-test-junit4-android = { group = "androidx.compose.ui", name = "ui-test-junit4-android", version.ref = "uiTestJunit4Android" }
androidx-runtime = { group = "androidx.xr.runtime", name = "runtime", version.ref = "runtime" }
androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
io-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
io-coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilNetworkOkhttp" }
io-coil-test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coilTest" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
Expand All @@ -86,5 +82,4 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-parcelize = { id = "kotlin-parcelize" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube-plugin" }

sonarqube = { id = "org.sonarqube", version.ref = "sonarqube-plugin" }
19 changes: 15 additions & 4 deletions monext/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,14 @@ android {
}
}

// Fonction pour récupérer la clé API de manière sécurisée
// Force la résolution d'espresso-core en 3.7.0 même si une lib transitive
// tente de le tirer en 3.6.x (incompatible avec Android 15+).
configurations.all {
resolutionStrategy {
force("androidx.test.espresso:espresso-core:3.7.0")
}
}

fun getApiKey(): String {
// 1. Variable d'environnement (CI/CD)
System.getenv("THREEDS_API_ACCESS_KEY")?.let {
Expand Down Expand Up @@ -469,18 +476,22 @@ dependencies {
// Voir la documentation : https://3dss.netcetera.com/3dssdk/doc/2.25.0/android-integration
implementation(files("libs/netcetera-3ds-sdk-2.5.3.2-classes.jar"))

// Tests
// Tests JVM
testImplementation(libs.junit.jupiter)
testImplementation(kotlin("test"))
testImplementation(libs.io.mockk)
testImplementation(libs.jetbrains.kotlinx.test)
testImplementation(libs.slf4j.api)

debugImplementation(libs.ui.tooling)
// Tooling Compose (Preview en debug)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)

// Tests instrumentés
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.ui.test.junit4.android)
androidTestImplementation(libs.androidx.ui.test.junit4)
androidTestImplementation(libs.io.mockk)
androidTestImplementation(libs.io.mockk.agent)
androidTestImplementation(libs.io.mockk.android)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.monext.sdk.internal.security

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.monext.sdk.internal.security.check.BinaryCheck
import com.monext.sdk.internal.security.check.BuildCheck
import com.monext.sdk.internal.security.check.DebuggerCheck
import com.monext.sdk.internal.security.check.FridaCheck
import com.monext.sdk.internal.security.check.PackageCheck
import com.monext.sdk.internal.security.check.ProcessCheck
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class RootDetectionRealDeviceTest {

private val context = InstrumentationRegistry.getInstrumentation().targetContext

@Test
fun buildCheck_shouldTriggerOnEmulator() {
assertTrue(BuildCheck().check())
}

@Test
fun binaryCheck_shouldNotTriggerOnCleanEmulator() {
assertTrue(BinaryCheck().check())
}

@Test
fun packageCheck_shouldNotTriggerOnCleanEmulator() {
assertFalse(PackageCheck(context.packageManager).check())
}

@Test
fun processCheck_shouldNotTriggerOnCleanEmulator() {
assertFalse(ProcessCheck(context).check())
}

@Test
fun fridaCheck_shouldNotTriggerOnCleanEmulator() {
assertFalse(FridaCheck().check())
}

@Test
fun debuggerCheck_jdwpOnly_shouldNotTrigger() {
assertFalse(DebuggerCheck().detectJavaDebugger())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.monext.sdk.internal.security.ui

import android.os.StrictMode
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.monext.sdk.FakeTestActivity
import io.mockk.MockKAnnotations
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class SecurityAlertDialogTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<FakeTestActivity>()

@Before
fun setup() {
MockKAnnotations.init(this)
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX)
StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX)
}

private fun setDialog(onDismiss: () -> Unit) {
composeTestRule.activity.setTestComposable {
CompositionLocalProvider(
LocalActivity provides composeTestRule.activity
) {
SecurityAlertDialog(onDismiss = onDismiss)
}
}
composeTestRule.waitForIdle()
}

@Test
fun securityDialog_whenShowing_displaysTitle() {
setDialog(onDismiss = {})

composeTestRule
.onNodeWithText("Appareil non sécurisé")
.assertIsDisplayed()
}

@Test
fun securityDialog_whenShowing_displaysMessage() {
setDialog(onDismiss = {})

composeTestRule
.onNodeWithText(
"Votre appareil ne répond pas aux exigences de sécurité " +
"requises pour effectuer un paiement."
)
.assertIsDisplayed()
}

@Test
fun securityDialog_whenShowing_displaysCloseButton() {
setDialog(onDismiss = {})

composeTestRule
.onNodeWithText("Fermer")
.assertIsDisplayed()
}

@Test
fun securityDialog_whenCloseButtonClicked_callsOnDismiss() {
var dismissed = false

setDialog(onDismiss = { dismissed = true })

composeTestRule
.onNodeWithText("Fermer")
.performClick()

composeTestRule.waitUntil(5000) { dismissed }
assertTrue(dismissed)
}
}
19 changes: 19 additions & 0 deletions monext/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<!--
ContentProvider auto-init de RootDetector.

Fusionné automatiquement dans le manifest de l'app intégratrice.
L'authority utilise ${applicationId} pour garantir l'unicité
entre plusieurs apps qui intègrent le SDK.

android:exported="false" → jamais accessible depuis l'extérieur.
android:multiprocess="false" → instancié une seule fois dans le process principal.
-->
<provider
android:name="com.monext.sdk.internal.security.RootDetectorInitializer"
android:authorities="${applicationId}.monext.security.init"
android:exported="false"
android:multiprocess="false" />

</application>

</manifest>
16 changes: 15 additions & 1 deletion monext/src/main/kotlin/com/monext/sdk/PaymentBox.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.monext.sdk.internal.security.RootDetector
import com.monext.sdk.internal.security.ui.SecurityAlertDialog

/**
* This composable handles payment sheet presentation upon user interaction.
Expand All @@ -28,10 +30,15 @@ fun PaymentBox(
) {

var showPaymentSheet by rememberSaveable { mutableStateOf(false) }
var showSecurityAlert by rememberSaveable { mutableStateOf(false) }

val onClick: () -> Unit = {
if (sessionToken != null) {
showPaymentSheet = true
if (RootDetector.isCompromised()) {
showSecurityAlert = true
} else {
showPaymentSheet = true
}
}
}

Expand All @@ -44,6 +51,13 @@ fun PaymentBox(
sdkContext = sdkContext,
onResult = onResult,
onIsShowingChange = { showPaymentSheet = it })


if (showSecurityAlert) {
SecurityAlertDialog(
onDismiss = { showSecurityAlert = false }
)
}
}
}

Expand Down
Loading
Loading