diff --git a/monext/src/androidTest/kotlin/com/monext/sdk/presentation/PaymentContainerTest.kt b/monext/src/androidTest/kotlin/com/monext/sdk/presentation/PaymentContainerTest.kt
index a7917f3..17eb94b 100644
--- a/monext/src/androidTest/kotlin/com/monext/sdk/presentation/PaymentContainerTest.kt
+++ b/monext/src/androidTest/kotlin/com/monext/sdk/presentation/PaymentContainerTest.kt
@@ -7,24 +7,15 @@ import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.monext.sdk.Appearance
-import com.monext.sdk.FakeTestActivity
-import com.monext.sdk.LocalAppearance
-import com.monext.sdk.PaymentOverlayToggle
-import com.monext.sdk.PaymentResult
+import com.monext.sdk.*
import com.monext.sdk.internal.api.model.response.SessionState
import com.monext.sdk.internal.api.model.response.SessionStateType
-import com.monext.sdk.internal.data.LocalSessionStateRepo
-import com.monext.sdk.internal.data.SessionStateRepository
-import com.monext.sdk.internal.data.sessionstate.ActiveWaiting
-import com.monext.sdk.internal.data.sessionstate.CustomMessage
-import com.monext.sdk.internal.data.sessionstate.PaymentOnholdPartner
+import com.monext.sdk.internal.data.FormData
+import com.monext.sdk.internal.data.sessionstate.*
+import com.monext.sdk.internal.presentation.PaymentAttempt
import com.monext.sdk.internal.presentation.PaymentContainer
import com.monext.sdk.internal.preview.PreviewSamples.Companion.buildSessionState
-import io.mockk.impl.annotations.RelaxedMockK
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
+import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -157,6 +148,78 @@ class PaymentContainerTest {
"back_button" to null))
}
+ @Test
+ fun withPaymentRedirectWithJavascript_triggersMakePaymentForLoadedMethod() {
+ // Given une réponse PAYMENT_REDIRECT_WITH_JAVASCRIPT contenant un moyen de paiement
+ // avec un script à exécuter (ex : empreinte device PayPal).
+ val paymentMethodData = PaymentMethodData(
+ cardCode = "PAYPAL_APIREST",
+ contractNumber = "PAYPAL_APIREST",
+ disabled = false,
+ hasForm = true,
+ form = PaymentForm(
+ displayButton = true,
+ formScript = FormScript(
+ content = "console.log('fingerprint');",
+ wrapIntoScriptTag = true,
+ formScriptEnum = "CUSTOM"
+ ),
+ formType = "CUSTOM"
+ ),
+ hasLogo = false,
+ logo = null,
+ isIsolated = false,
+ options = emptyList(),
+ paymentMethodAction = 0,
+ additionalData = null,
+ requestContext = null,
+ shouldBeInTopPosition = false,
+ state = "AVAILABLE"
+ )
+ val sessionState = SessionState(
+ token = "fake_token",
+ type = SessionStateType.PAYMENT_REDIRECT_WITH_JAVASCRIPT,
+ creationDate = "Tue Mar 25 12:33:22 CET 2025",
+ cancelUrl = "https://www.payline.com",
+ pointOfSale = "POS_Fake",
+ language = "fr",
+ returnUrl = "https://www.monext.fr",
+ automaticRedirectAtSessionsEnd = false,
+ isSandbox = true,
+ paymentMethodsList = PaymentMethodsList(
+ isOriginalCreditTransfer = false,
+ needsDeviceFingerprint = true,
+ paymentMethodsData = listOf(paymentMethodData),
+ scoringNeeded = null,
+ sensitiveInputContentMasked = false,
+ shouldChangePaymentMethodPosition = false,
+ wallets = emptyList()
+ )
+ )
+
+ var capturedAttempt: PaymentAttempt? = null
+
+ // When
+ composeTestRule.activity.setTestComposable {
+ CompositionLocalProvider(LocalAppearance provides appearance) {
+ PaymentContainer(
+ sessionState,
+ { paymentMethodList, sessionInfo -> },
+ {},
+ { attempt -> capturedAttempt = attempt },
+ {},
+ { },
+ { }
+ ) { state -> stateHistory.add(state) }
+ }
+ }
+
+ // Then : le paiement est déclenché automatiquement pour le moyen de paiement chargé
+ composeTestRule.waitUntil(timeoutMillis = 10_000) { capturedAttempt != null }
+ assertEquals("PAYPAL_APIREST", capturedAttempt?.selectedPaymentMethod?.cardCode)
+ assertTrue(capturedAttempt?.paymentFormData is FormData.AlternativePaymentMethodForm)
+ }
+
private fun executeSessionStateTest(
sessionState: SessionState,
expectedTransactionState: PaymentResult.TransactionState,
@@ -172,6 +235,7 @@ class PaymentContainerTest {
{ paymentMethodList, sessionInfo -> },
{},
{},
+ {},
{ result ->
paymentResult = result
},
diff --git a/monext/src/main/AndroidManifest.xml b/monext/src/main/AndroidManifest.xml
index a5918e6..4b57a18 100644
--- a/monext/src/main/AndroidManifest.xml
+++ b/monext/src/main/AndroidManifest.xml
@@ -1,4 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/monext/src/main/kotlin/com/monext/sdk/PaymentSheet.kt b/monext/src/main/kotlin/com/monext/sdk/PaymentSheet.kt
index 8a25636..1b76413 100644
--- a/monext/src/main/kotlin/com/monext/sdk/PaymentSheet.kt
+++ b/monext/src/main/kotlin/com/monext/sdk/PaymentSheet.kt
@@ -96,8 +96,8 @@ fun PaymentSheet(isShowing: Boolean, sessionToken: String, sdkContext: MnxtSDKCo
)
)
if (!sheetState.isVisible) {
- onIsShowingChange?.invoke(false)
- }
+ onIsShowingChange?.invoke(false)
+ }
},
modifier = Modifier.statusBarsPadding().testTag("payment_bottom_sheet"),
sheetState = sheetState,
@@ -192,6 +192,9 @@ fun PaymentSheet(isShowing: Boolean, sessionToken: String, sdkContext: MnxtSDKCo
)
},
onRedirectionComplete = { viewModel.updateSessionState(sessionToken) },
+ onMakePayment = { attempt ->
+ viewModel.makePayment(attempt, context) { showingOverlay = it }
+ },
onRetry = { /* TODO: implement RETRY */ },
onResult = onResult,
onIsShowingChange = isShowingChangeWrapper,
diff --git a/monext/src/main/kotlin/com/monext/sdk/internal/api/PaymentAPI.kt b/monext/src/main/kotlin/com/monext/sdk/internal/api/PaymentAPI.kt
index 5bfed3a..1b716e1 100644
--- a/monext/src/main/kotlin/com/monext/sdk/internal/api/PaymentAPI.kt
+++ b/monext/src/main/kotlin/com/monext/sdk/internal/api/PaymentAPI.kt
@@ -21,7 +21,7 @@ import java.net.URL
internal interface PaymentAPI {
@Throws(NetworkError::class)
- suspend fun stateCurrent(sessionToken: String): SessionState
+ suspend fun stateCurrent(sessionToken: String, merchantReturnUrl: String): SessionState
@Throws(NetworkError::class)
suspend fun payment(sessionToken: String, params: PaymentRequest): SessionState
@@ -78,10 +78,20 @@ internal class PaymentAPIImpl(
* GET /token/{token}/state/current
*/
@Throws(NetworkError::class)
- override suspend fun stateCurrent(sessionToken: String): SessionState {
+ override suspend fun stateCurrent(sessionToken: String, merchantReturnUrl: String): SessionState {
val baseUrl = buildBaseUrl()
val url = appendPath(baseUrl, sessionToken, "state", "current")
- val httpRequest = buildHttpRequest(url, HttpMethod.GET)
+
+ val uriWithQuery = URI(
+ url.protocol,
+ url.authority,
+ url.path,
+ "merchantReturnUrl=$merchantReturnUrl",
+ null
+ )
+ val finalUrl = uriWithQuery.toURL()
+
+ val httpRequest = buildHttpRequest(finalUrl, HttpMethod.GET)
val sessionStateResponse = makeRequest(httpRequest)
// on récupère la configuration pour le remoteLogger
diff --git a/monext/src/main/kotlin/com/monext/sdk/internal/api/model/response/SessionStateResponse.kt b/monext/src/main/kotlin/com/monext/sdk/internal/api/model/response/SessionStateResponse.kt
index 78c97b2..faee27c 100644
--- a/monext/src/main/kotlin/com/monext/sdk/internal/api/model/response/SessionStateResponse.kt
+++ b/monext/src/main/kotlin/com/monext/sdk/internal/api/model/response/SessionStateResponse.kt
@@ -46,6 +46,7 @@ internal enum class SessionStateType {
PAYMENT_METHODS_LIST,
PAYMENT_REDIRECT_NO_RESPONSE,
+ PAYMENT_REDIRECT_WITH_JAVASCRIPT, // Redirection après avoir executé le Javascript
PAYMENT_SUCCESS,
PAYMENT_FAILURE,
PAYMENT_ONHOLD_PARTNER,
@@ -59,7 +60,7 @@ internal enum class SessionStateType {
fun toTransactionState(): PaymentResult.TransactionState? =
when (this) {
- PAYMENT_METHODS_LIST, PAYMENT_REDIRECT_NO_RESPONSE, SDK_CHALLENGE, ACTIVE_WAITING -> PaymentResult.TransactionState.PAYMENT_INCOMPLETE
+ PAYMENT_METHODS_LIST, PAYMENT_REDIRECT_NO_RESPONSE, PAYMENT_REDIRECT_WITH_JAVASCRIPT, SDK_CHALLENGE, ACTIVE_WAITING -> PaymentResult.TransactionState.PAYMENT_INCOMPLETE
PAYMENT_SUCCESS -> PaymentResult.TransactionState.PAYMENT_SUCCESS
PAYMENT_FAILURE, PAYMENT_FAILURE_WITH_RETRY -> PaymentResult.TransactionState.PAYMENT_FAILURE
PAYMENT_CANCELED -> PaymentResult.TransactionState.PAYMENT_CANCELED
diff --git a/monext/src/main/kotlin/com/monext/sdk/internal/data/SessionStateRepository.kt b/monext/src/main/kotlin/com/monext/sdk/internal/data/SessionStateRepository.kt
index 274b17b..df3e4f6 100644
--- a/monext/src/main/kotlin/com/monext/sdk/internal/data/SessionStateRepository.kt
+++ b/monext/src/main/kotlin/com/monext/sdk/internal/data/SessionStateRepository.kt
@@ -55,7 +55,7 @@ internal class SessionStateRepository(
suspend fun updateSessionState(token: String) {
makeRequest {
- val sState = paymentAPI.stateCurrent(token)
+ val sState = paymentAPI.stateCurrent(sessionToken = token, merchantReturnUrl = this.returnURLString)
this.token = sState.token
sState
}
diff --git a/monext/src/main/kotlin/com/monext/sdk/internal/data/sessionstate/PaymentMethodsList.kt b/monext/src/main/kotlin/com/monext/sdk/internal/data/sessionstate/PaymentMethodsList.kt
index 1f2757c..dd3e318 100644
--- a/monext/src/main/kotlin/com/monext/sdk/internal/data/sessionstate/PaymentMethodsList.kt
+++ b/monext/src/main/kotlin/com/monext/sdk/internal/data/sessionstate/PaymentMethodsList.kt
@@ -96,9 +96,32 @@ internal data class PaymentForm(
val description: String? = null,
val buttonText: String? = null,
val formFields: List = emptyList(),
+ val formScript: FormScript? = null,
val formType: String? = null
) : Parcelable
+@Parcelize
+@Serializable
+internal data class FormScript(
+ val content: String? = null,
+ val wrapIntoScriptTag: Boolean? = null,
+ val formScriptEnum: String? = null
+) : Parcelable {
+
+ /**
+ * Retourne le contenu HTML prêt à être chargé dans la WebView.
+ * Si [wrapIntoScriptTag] vaut true, le contenu est entouré d'une balise .
+ */
+ fun htmlContent(): String? {
+ val script = content ?: return null
+ return if (wrapIntoScriptTag == true) {
+ ""
+ } else {
+ script
+ }
+ }
+}
+
@Parcelize
@Serializable
internal data class PaymentMethodFormField(
diff --git a/monext/src/main/kotlin/com/monext/sdk/internal/presentation/PaymentContainer.kt b/monext/src/main/kotlin/com/monext/sdk/internal/presentation/PaymentContainer.kt
index 91aa748..b19f2b4 100644
--- a/monext/src/main/kotlin/com/monext/sdk/internal/presentation/PaymentContainer.kt
+++ b/monext/src/main/kotlin/com/monext/sdk/internal/presentation/PaymentContainer.kt
@@ -1,6 +1,7 @@
package com.monext.sdk.internal.presentation
import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
import com.monext.sdk.PaymentOverlayToggle
import com.monext.sdk.PaymentResult
import com.monext.sdk.PaymentResult.PaymentCompleted
@@ -17,9 +18,11 @@ import com.monext.sdk.internal.presentation.status.LoadingSection
import com.monext.sdk.internal.presentation.status.PaymentCanceledScreen
import com.monext.sdk.internal.presentation.status.PaymentFailureScreen
import com.monext.sdk.internal.presentation.status.PaymentPendingScreen
+import com.monext.sdk.internal.presentation.status.PaymentJavascriptRedirectionScreen
import com.monext.sdk.internal.presentation.status.PaymentRedirectionScreen
import com.monext.sdk.internal.presentation.status.PaymentSuccessScreen
import com.monext.sdk.internal.presentation.status.TokenExpiredScreen
+import com.monext.sdk.internal.service.PaymentForegroundService
import com.monext.sdk.internal.threeds.view.PaymentSdkChallengeScreen
internal data class PaymentAttempt(
@@ -38,13 +41,17 @@ internal fun PaymentContainer(
sessionState: SessionState?,
paymentMethodsScreen: @Composable (PaymentMethodsList, SessionInfo) -> Unit,
onRedirectionComplete: () -> Unit,
+ onMakePayment: (PaymentAttempt) -> Unit,
onRetry: () -> Unit,
onResult: (PaymentResult) -> Unit,
onIsShowingChange: ((Boolean) -> Unit)? = null,
showOverlay: (PaymentOverlayToggle) -> Unit
) {
+ val context = LocalContext.current
+
if (sessionState?.type?.isFinalState() == true) {
+ PaymentForegroundService.stop(context)
when(sessionState.type) {
SessionStateType.PAYMENT_SUCCESS -> onResult(
PaymentCompleted(
@@ -92,6 +99,30 @@ internal fun PaymentContainer(
} ?: LoadingSection()
}
+ SessionStateType.PAYMENT_REDIRECT_WITH_JAVASCRIPT -> {
+ // On a les memes data que "paymentMethodsList".
+ // On exécute le Javascript dans une WebView invisible (seul un spinner est affiché),
+ // puis on déclenche automatiquement le paiement du moyen de paiement chargé,
+ // sans afficher le bouton ni demander d'interaction à l'acheteur.
+ val paymentMethod = sessionState.paymentMethodsList?.paymentMethods?.firstOrNull()
+ val formScript = paymentMethod?.data?.form?.formScript
+ if (paymentMethod != null && formScript != null) {
+ PaymentJavascriptRedirectionScreen(formScript) {
+ onMakePayment(
+ PaymentAttempt(
+ selectedPaymentMethod = paymentMethod,
+ paymentFormData = FormData.AlternativePaymentMethodForm(saveCard = false),
+ selectedWallet = null,
+ walletFormData = null
+ )
+ )
+ }
+ } else {
+ // Ne peut normalement pas arriver car si le backend renvoit ce type de réponse, c'est qu'il a controlé les données.
+ LoadingSection()
+ }
+ }
+
SessionStateType.SDK_CHALLENGE -> {
sessionState.paymentSdkChallenge?.sdkChallengeData?.let { data ->
PaymentSdkChallengeScreen(sdkChallengeData = data, showOverlay)
diff --git a/monext/src/main/kotlin/com/monext/sdk/internal/presentation/status/PaymentJavascriptRedirectionScreen.kt b/monext/src/main/kotlin/com/monext/sdk/internal/presentation/status/PaymentJavascriptRedirectionScreen.kt
new file mode 100644
index 0000000..155490c
--- /dev/null
+++ b/monext/src/main/kotlin/com/monext/sdk/internal/presentation/status/PaymentJavascriptRedirectionScreen.kt
@@ -0,0 +1,94 @@
+package com.monext.sdk.internal.presentation.status
+
+import android.annotation.SuppressLint
+import android.view.ViewGroup
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Alignment
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import com.monext.sdk.internal.data.sessionstate.FormScript
+import kotlinx.coroutines.delay
+import kotlin.time.Duration.Companion.milliseconds
+
+/**
+ * Écran utilisé pour l'état [com.monext.sdk.internal.api.model.response.SessionStateType.PAYMENT_REDIRECT_WITH_JAVASCRIPT].
+ *
+ * Cas d'usage : exécuter un script Javascript fourni par le backend (ex : empreinte device PayPal)
+ * SANS aucune interaction de l'acheteur. On affiche uniquement un spinner, on charge le script dans
+ * une WebView invisible, on attend un court délai, puis on déclenche automatiquement le paiement.
+ */
+const val JAVASCRIPT_EXECUTION_DELAY_MS = 500L
+
+@SuppressLint("SetJavaScriptEnabled")
+@Composable
+internal fun PaymentJavascriptRedirectionScreen(
+ formScript: FormScript,
+ onScriptExecuted: () -> Unit
+) {
+ val htmlContent = remember(formScript) { formScript.htmlContent() }
+ var pageFinished by remember { mutableStateOf(false) }
+ val currentOnScriptExecuted by rememberUpdatedState(onScriptExecuted)
+
+ // Si le script est absent, on déclenche directement le paiement
+ LaunchedEffect(htmlContent) {
+ if (htmlContent == null) {
+ currentOnScriptExecuted()
+ }
+ }
+
+ // Une fois la page chargée, on attend un court délai pour laisser le JS s'exécuter,
+ // puis on déclenche le paiement programmatiquement.
+ LaunchedEffect(pageFinished) {
+ if (pageFinished) {
+ delay(JAVASCRIPT_EXECUTION_DELAY_MS.milliseconds)
+ currentOnScriptExecuted()
+ }
+ }
+
+ // Spinner affiché à l'acheteur, centré au-dessus de la WebView (invisible) pendant
+ // l'exécution du Javascript (seul élément visible pour l'acheteur).
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ if (htmlContent != null) {
+ // WebView invisible (taille 0) : on ne veut rien afficher à l'acheteur.
+ AndroidView(
+ factory = { context ->
+ WebView(context).apply {
+ layoutParams = ViewGroup.LayoutParams(0, 0)
+ settings.javaScriptEnabled = true
+ webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView?, url: String?) {
+ super.onPageFinished(view, url)
+ pageFinished = true
+ }
+ }
+ }
+ },
+ update = { webView ->
+ webView.loadDataWithBaseURL(
+ null,
+ htmlContent,
+ "text/html",
+ "UTF-8",
+ null
+ )
+ }
+ )
+ }
+
+ // Loader circulaire affiché pendant l'exécution du Javascript.
+ LoadingSection()
+ }
+}
diff --git a/monext/src/main/kotlin/com/monext/sdk/internal/preview/PaymentAPIPreview.kt b/monext/src/main/kotlin/com/monext/sdk/internal/preview/PaymentAPIPreview.kt
index f24033d..d0fd248 100644
--- a/monext/src/main/kotlin/com/monext/sdk/internal/preview/PaymentAPIPreview.kt
+++ b/monext/src/main/kotlin/com/monext/sdk/internal/preview/PaymentAPIPreview.kt
@@ -13,7 +13,7 @@ import com.monext.sdk.internal.threeds.response.DirectoryServerSdkKey
import com.monext.sdk.internal.threeds.response.DirectoryServerSdkKeyResponse
internal object PaymentAPIPreviewSuccess: PaymentAPI {
- override suspend fun stateCurrent(sessionToken: String): SessionState = PreviewSamples.sessionStatePaymentMethodsList
+ override suspend fun stateCurrent(sessionToken: String, merchantReturnUrl: String): SessionState = PreviewSamples.sessionStatePaymentMethodsList
override suspend fun payment(sessionToken: String, params: PaymentRequest): SessionState = PreviewSamples.sessionStateSuccess
override suspend fun securedPayment(sessionToken: String, params: SecuredPaymentRequest): SessionState = PreviewSamples.sessionStateSuccess
override suspend fun walletPayment(sessionToken: String, params: WalletPaymentRequest): SessionState = PreviewSamples.sessionStateSuccess
diff --git a/monext/src/main/kotlin/com/monext/sdk/internal/service/PaymentForegroundService.kt b/monext/src/main/kotlin/com/monext/sdk/internal/service/PaymentForegroundService.kt
new file mode 100644
index 0000000..b162d0f
--- /dev/null
+++ b/monext/src/main/kotlin/com/monext/sdk/internal/service/PaymentForegroundService.kt
@@ -0,0 +1,107 @@
+package com.monext.sdk.internal.service
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import androidx.core.content.ContextCompat
+
+class PaymentForegroundService : Service() {
+
+ companion object {
+ const val ACTION_START = "ACTION_START"
+ const val ACTION_STOP = "ACTION_STOP"
+
+ private const val NOTIFICATION_ID = 1001
+ private const val CHANNEL_ID = "payment_channel"
+
+ // Helper pour démarrer le service depuis n'importe quel contexte
+ fun start(context: Context) {
+ val intent = Intent(context, PaymentForegroundService::class.java)
+ .apply { action = ACTION_START }
+ ContextCompat.startForegroundService(context, intent)
+ }
+
+ // Helper pour arrêter le service
+ fun stop(context: Context) {
+ val intent = Intent(context, PaymentForegroundService::class.java)
+ .apply { action = ACTION_STOP }
+ context.startService(intent)
+ }
+ }
+
+ // Evite les appels multiples à makeForeground()
+ private var isStarted = false
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d("PaymentFGS", "onStartCommand called with action: ${intent?.action}")
+ when (intent?.action) {
+ ACTION_START -> {
+ if (!isStarted) {
+ makeForeground()
+ isStarted = true
+ }
+ }
+ ACTION_STOP -> {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+ }
+ // START_NOT_STICKY : ne pas redémarrer automatiquement si le process est tué
+ return START_NOT_STICKY
+ }
+
+ private fun makeForeground() {
+ val notification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Paiement en cours")
+ .setContentText("Veuillez finaliser votre authentification bancaire")
+ .setSmallIcon(android.R.drawable.ic_lock_lock)
+ .setOngoing(true)
+ .setShowWhen(false)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .build()
+
+ // ServiceCompat gère automatiquement les différences de versions
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ServiceCompat.startForeground(
+ this,
+ NOTIFICATION_ID,
+ notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ )
+ }
+ }
+
+ private fun createNotificationChannel() {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "Paiement",
+ NotificationManager.IMPORTANCE_LOW // Pas de son, pas de popup
+ ).apply {
+ description = "Maintient le paiement actif pendant l'authentification 3DS"
+ setShowBadge(false)
+ }
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(channel)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ isStarted = false
+ }
+
+ // Pas de binding nécessaire pour ce cas d'usage
+ override fun onBind(intent: Intent?): IBinder? = null
+}
\ No newline at end of file
diff --git a/monext/src/test/kotlin/com/monext/sdk/internal/api/PaymentAPIImplTest.kt b/monext/src/test/kotlin/com/monext/sdk/internal/api/PaymentAPIImplTest.kt
index ce43988..d47f09a 100644
--- a/monext/src/test/kotlin/com/monext/sdk/internal/api/PaymentAPIImplTest.kt
+++ b/monext/src/test/kotlin/com/monext/sdk/internal/api/PaymentAPIImplTest.kt
@@ -38,6 +38,7 @@ class PaymentAPIImplTest {
private val testDispatcher = StandardTestDispatcher()
private val testEnvironment = MnxtEnvironment.Custom("test.example.com/api/v1")
private val sessionToken = "test-session-token-123"
+ private val merchantReturnUrl = "https://www.monext.fr"
// allow to capture parameter with non nullable type `Double`
val captureHttpRequest = slot()
@@ -73,14 +74,14 @@ class PaymentAPIImplTest {
mockHttpClientResponse(responseBody = RESPONSE_CONTEXT_SUCCESS)
// When
- val result = paymentApi.stateCurrent(sessionToken)
+ val result = paymentApi.stateCurrent(sessionToken, merchantReturnUrl)
// Then
checkResponseSuccess(expectedSessionState, result)
// On check aussi la request envoyée
checkHttpRequest(
- url = "https://test.example.com/api/v1/services/token/test-session-token-123/state/current",
+ url = "https://test.example.com/api/v1/services/token/test-session-token-123/state/current?merchantReturnUrl=$merchantReturnUrl",
method = "GET"
)
}
@@ -100,14 +101,14 @@ class PaymentAPIImplTest {
mockHttpClientResponse(responseBody = RESPONSE_CONTEXT_SUCCESS)
// When
- val result = paymentApi.stateCurrent(sessionToken)
+ val result = paymentApi.stateCurrent(sessionToken, merchantReturnUrl)
// Then
checkResponseSuccess(expectedSessionState, result)
// On check aussi la request envoyée
checkHttpRequest(
- url = "https://homologation-payment.payline.com/services/token/test-session-token-123/state/current",
+ url = "https://homologation-payment.payline.com/services/token/test-session-token-123/state/current?merchantReturnUrl=$merchantReturnUrl",
method = "GET",
origin = "homologation-payment.payline.com"
)
@@ -128,14 +129,14 @@ class PaymentAPIImplTest {
mockHttpClientResponse(responseBody = RESPONSE_CONTEXT_SUCCESS)
// When
- val result = paymentApi.stateCurrent(sessionToken)
+ val result = paymentApi.stateCurrent(sessionToken, merchantReturnUrl)
// Then
checkResponseSuccess(expectedSessionState, result)
// On check aussi la request envoyée
checkHttpRequest(
- url = "https://payment.payline.com/services/token/test-session-token-123/state/current",
+ url = "https://payment.payline.com/services/token/test-session-token-123/state/current?merchantReturnUrl=$merchantReturnUrl",
method = "GET",
origin = "payment.payline.com"
)
diff --git a/monext/src/test/kotlin/com/monext/sdk/internal/api/model/response/SessionStateTypeTest.kt b/monext/src/test/kotlin/com/monext/sdk/internal/api/model/response/SessionStateTypeTest.kt
new file mode 100644
index 0000000..9181525
--- /dev/null
+++ b/monext/src/test/kotlin/com/monext/sdk/internal/api/model/response/SessionStateTypeTest.kt
@@ -0,0 +1,80 @@
+package com.monext.sdk.internal.api.model.response
+
+import com.monext.sdk.PaymentResult
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class SessionStateTypeTest {
+
+ @Test
+ fun toTransactionState_paymentRedirectWithJavascript_returnsPaymentIncomplete() {
+ // Given
+ val type = SessionStateType.PAYMENT_REDIRECT_WITH_JAVASCRIPT
+
+ // When
+ val result = type.toTransactionState()
+
+ // Then
+ assertEquals(PaymentResult.TransactionState.PAYMENT_INCOMPLETE, result)
+ }
+
+ @Test
+ fun toTransactionState_paymentMethodsList_returnsPaymentIncomplete() {
+ assertEquals(
+ PaymentResult.TransactionState.PAYMENT_INCOMPLETE,
+ SessionStateType.PAYMENT_METHODS_LIST.toTransactionState()
+ )
+ }
+
+ @Test
+ fun toTransactionState_paymentRedirectNoResponse_returnsPaymentIncomplete() {
+ assertEquals(
+ PaymentResult.TransactionState.PAYMENT_INCOMPLETE,
+ SessionStateType.PAYMENT_REDIRECT_NO_RESPONSE.toTransactionState()
+ )
+ }
+
+ @Test
+ fun toTransactionState_paymentSuccess_returnsPaymentSuccess() {
+ assertEquals(
+ PaymentResult.TransactionState.PAYMENT_SUCCESS,
+ SessionStateType.PAYMENT_SUCCESS.toTransactionState()
+ )
+ }
+
+ @Test
+ fun toTransactionState_paymentFailure_returnsPaymentFailure() {
+ assertEquals(
+ PaymentResult.TransactionState.PAYMENT_FAILURE,
+ SessionStateType.PAYMENT_FAILURE.toTransactionState()
+ )
+ }
+
+ @Test
+ fun toTransactionState_paymentCanceled_returnsPaymentCanceled() {
+ assertEquals(
+ PaymentResult.TransactionState.PAYMENT_CANCELED,
+ SessionStateType.PAYMENT_CANCELED.toTransactionState()
+ )
+ }
+
+ @Test
+ fun toTransactionState_tokenExpired_returnsTokenExpired() {
+ assertEquals(
+ PaymentResult.TransactionState.TOKEN_EXPIRED,
+ SessionStateType.TOKEN_EXPIRED.toTransactionState()
+ )
+ }
+
+ @Test
+ fun toTransactionState_unknown_returnsNull() {
+ assertNull(SessionStateType.UNKNOWN.toTransactionState())
+ }
+
+ @Test
+ fun isFinalState_paymentRedirectWithJavascript_returnsFalse() {
+ // Given le flux Javascript est un état intermédiaire (spinner + exécution du JS)
+ assertEquals(false, SessionStateType.PAYMENT_REDIRECT_WITH_JAVASCRIPT.isFinalState())
+ }
+}
diff --git a/monext/src/test/kotlin/com/monext/sdk/internal/data/SessionStateRepositoryTest.kt b/monext/src/test/kotlin/com/monext/sdk/internal/data/SessionStateRepositoryTest.kt
index 7ef8c76..0031949 100644
--- a/monext/src/test/kotlin/com/monext/sdk/internal/data/SessionStateRepositoryTest.kt
+++ b/monext/src/test/kotlin/com/monext/sdk/internal/data/SessionStateRepositoryTest.kt
@@ -7,14 +7,12 @@ import com.monext.sdk.MnxtSDKContext
import com.monext.sdk.SdkTestHelper
import com.monext.sdk.internal.api.PaymentAPI
import com.monext.sdk.internal.api.configuration.InternalSDKContext
-import com.monext.sdk.internal.api.model.response.SessionState
import com.monext.sdk.internal.preview.PreviewSamples
import com.monext.sdk.internal.preview.PreviewSamples.Companion.sessionStateActiveWaiting
import com.monext.sdk.internal.threeds.ThreeDSManager
import com.monext.sdk.internal.threeds.model.ChallengeUseCaseCallback
import io.mockk.coEvery
import io.mockk.coVerify
-import io.mockk.every
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit5.MockKExtension
import io.mockk.mockk
@@ -52,7 +50,7 @@ class SessionStateRepositoryTest {
underTest = spyk(SessionStateRepository(paymentAPI, internalSDKContext, threeDSManager))
// Mock
- coEvery { paymentAPI.stateCurrent(any()) } returns sessionStateMethodList
+ coEvery { paymentAPI.stateCurrent(any(), any()) } returns sessionStateMethodList
}
@Test
@@ -60,7 +58,7 @@ class SessionStateRepositoryTest {
underTest.updateSessionState(token)
- coVerify(exactly = 1) { paymentAPI.stateCurrent(token) }
+ coVerify(exactly = 1) { paymentAPI.stateCurrent(token, any()) }
assertEquals(sessionStateMethodList, underTest.sessionState.value)
}
@@ -86,7 +84,7 @@ class SessionStateRepositoryTest {
fun clearSession() = runTest(testDispatcher) {
// Init data
underTest.updateSessionState(token)
- coVerify(exactly = 1) { paymentAPI.stateCurrent(token) }
+ coVerify(exactly = 1) { paymentAPI.stateCurrent(token, any()) }
assertEquals(sessionStateMethodList, underTest.sessionState.value)
// Test
@@ -180,7 +178,7 @@ class SessionStateRepositoryTest {
// On met à jour
underTest.updateSessionState(token)
- coVerify(exactly = 1) { paymentAPI.stateCurrent(token) }
+ coVerify(exactly = 1) { paymentAPI.stateCurrent(token, any()) }
// Il ne doit plus etre null
assertEquals(sessionStateMethodList, underTest.sessionState.value)
}
@@ -264,4 +262,4 @@ class SessionStateRepositoryTest {
coVerify { underTest.updateSessionState(token) }
}
-}
\ No newline at end of file
+}
diff --git a/monext/src/test/kotlin/com/monext/sdk/internal/data/sessionstate/FormScriptTest.kt b/monext/src/test/kotlin/com/monext/sdk/internal/data/sessionstate/FormScriptTest.kt
new file mode 100644
index 0000000..1f588be
--- /dev/null
+++ b/monext/src/test/kotlin/com/monext/sdk/internal/data/sessionstate/FormScriptTest.kt
@@ -0,0 +1,73 @@
+package com.monext.sdk.internal.data.sessionstate
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class FormScriptTest {
+
+ @Test
+ fun htmlContent_whenWrapIntoScriptTagTrue_wrapsContentInScriptTag() {
+ // Given
+ val formScript = FormScript(
+ content = "console.log('hello');",
+ wrapIntoScriptTag = true,
+ formScriptEnum = "CUSTOM"
+ )
+
+ // When
+ val result = formScript.htmlContent()
+
+ // Then
+ assertEquals("", result)
+ }
+
+ @Test
+ fun htmlContent_whenWrapIntoScriptTagFalse_returnsContentAsIs() {
+ // Given
+ val rawContent = ""
+ val formScript = FormScript(
+ content = rawContent,
+ wrapIntoScriptTag = false,
+ formScriptEnum = "CUSTOM"
+ )
+
+ // When
+ val result = formScript.htmlContent()
+
+ // Then
+ assertEquals(rawContent, result)
+ }
+
+ @Test
+ fun htmlContent_whenWrapIntoScriptTagNull_returnsContentAsIs() {
+ // Given
+ val formScript = FormScript(
+ content = "alert('hi');",
+ wrapIntoScriptTag = null,
+ formScriptEnum = "CUSTOM"
+ )
+
+ // When
+ val result = formScript.htmlContent()
+
+ // Then
+ assertEquals("alert('hi');", result)
+ }
+
+ @Test
+ fun htmlContent_whenContentNull_returnsNull() {
+ // Given
+ val formScript = FormScript(
+ content = null,
+ wrapIntoScriptTag = true,
+ formScriptEnum = "CUSTOM"
+ )
+
+ // When
+ val result = formScript.htmlContent()
+
+ // Then
+ assertNull(result)
+ }
+}
diff --git a/monext/src/test/kotlin/com/monext/sdk/internal/preview/PaymentAPIPreviewSuccessTest.kt b/monext/src/test/kotlin/com/monext/sdk/internal/preview/PaymentAPIPreviewSuccessTest.kt
index d92e8c3..4a37589 100644
--- a/monext/src/test/kotlin/com/monext/sdk/internal/preview/PaymentAPIPreviewSuccessTest.kt
+++ b/monext/src/test/kotlin/com/monext/sdk/internal/preview/PaymentAPIPreviewSuccessTest.kt
@@ -3,7 +3,6 @@ package com.monext.sdk.internal.preview
import com.monext.sdk.MnxtEnvironment
import com.monext.sdk.MnxtSDKContext
import com.monext.sdk.SdkTestHelper
-import com.monext.sdk.internal.api.AvailableCardNetworksRequest
import com.monext.sdk.internal.api.configuration.InternalSDKContext
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.*
@@ -28,7 +27,7 @@ class PaymentAPIPreviewSuccessTest {
@Test
fun stateCurrent() = runBlocking {
- assertEquals(PreviewSamples.sessionStatePaymentMethodsList, underTest.stateCurrent("aa"))
+ assertEquals(PreviewSamples.sessionStatePaymentMethodsList, underTest.stateCurrent("aa", "https://www.monext.fr"))
}
@Test