From 2047bb439f4f42e5b86dfba5177010892b483daf Mon Sep 17 00:00:00 2001 From: ChiragKV-Juspay Date: Thu, 9 Oct 2025 12:58:22 +0530 Subject: [PATCH] feat: added AuthenticationSession and aliased universal3dsapis --- app/build.gradle | 3 +- .../authentication/AuthenticationSession.kt | 62 +++ .../hyperswitch/authentication/TypeAliases.kt | 58 ++ demo-app/build.gradle | 5 +- demo-app/src/main/AndroidManifest.xml | 10 +- .../io/hyperswitch/demoapp/MainActivity.kt | 6 + .../demoapp/ThreeDSTestActivity.kt | 496 ++++++++++++++++++ .../src/main/res/layout/main_activity.xml | 12 + .../res/layout/three_ds_test_activity.xml | 104 ++++ 9 files changed, 752 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/io/hyperswitch/authentication/AuthenticationSession.kt create mode 100644 app/src/main/java/io/hyperswitch/authentication/TypeAliases.kt create mode 100644 demo-app/src/main/java/io/hyperswitch/demoapp/ThreeDSTestActivity.kt create mode 100644 demo-app/src/main/res/layout/three_ds_test_activity.xml diff --git a/app/build.gradle b/app/build.gradle index 5fedc5e9..24cda459 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,6 +129,7 @@ dependencies { implementation("org.greenrobot:eventbus") implementation project(':hyperswitch-sdk-android-lite') implementation('com.squareup.okhttp3:okhttp') + compileOnly project(':react-native-hyperswitch-unified-3ds') constraints { implementation("com.squareup.okhttp3:okhttp") { @@ -136,7 +137,7 @@ dependencies { prefer '3.3.0' } } - + implementation("androidx.appcompat:appcompat") { version { prefer '1.6.1' diff --git a/app/src/main/java/io/hyperswitch/authentication/AuthenticationSession.kt b/app/src/main/java/io/hyperswitch/authentication/AuthenticationSession.kt new file mode 100644 index 00000000..3dc74111 --- /dev/null +++ b/app/src/main/java/io/hyperswitch/authentication/AuthenticationSession.kt @@ -0,0 +1,62 @@ +package io.hyperswitch.authentication + +import android.app.Activity +import io.hyperswitch.modular_3ds.api.ThreeDSAuthenticationSession + + +class AuthenticationSession( + private val activity: Activity, + private val publishKey: String +) { + companion object { + /** + * Check if the 3DS library is available at runtime + * + * @return true if modular_3ds library is available, false otherwise + */ + @JvmStatic + fun isAvailable(): Boolean { + return try { + Class.forName("io.hyperswitch.modular_3ds.api.ThreeDSAuthenticationSession") + true + } catch (e: ClassNotFoundException) { + false + } + } + } + + private val internalAuthSession: ThreeDSAuthenticationSession + + init { + if (!isAvailable()) { + throw IllegalStateException( + "3DS library not found. Please add the following dependency to your build.gradle:\n" + + "implementation 'io.hyperswitch:modular-3ds-api:x.x.x'\n" + + "You can check availability using AuthenticationSession.isAvailable() before instantiation." + ) + } + internalAuthSession = ThreeDSAuthenticationSession(activity, publishKey) + } + + /** + * Initialize a 3DS session + * + * @param authIntentClientSecret The authentication intent client secret + * @param configuration Authentication configuration (use AuthenticationConfiguration from this package) + * @param callback Callback to receive authentication result (use AuthenticationResult from this package) + * @return Session object if initialization is successful, null otherwise + */ + fun initThreeDsSession( + authIntentClientSecret: String, + configuration: AuthenticationConfiguration, + callback: (AuthenticationResult) -> Unit + ): Session? { + return internalAuthSession.initThreeDsSession( + authIntentClientSecret = authIntentClientSecret, + configuration = configuration + ) { modularResult -> + // Convert modular_3ds result to wrapper result + callback(AuthenticationResult.from(modularResult)) + } + } +} diff --git a/app/src/main/java/io/hyperswitch/authentication/TypeAliases.kt b/app/src/main/java/io/hyperswitch/authentication/TypeAliases.kt new file mode 100644 index 00000000..df03c3dd --- /dev/null +++ b/app/src/main/java/io/hyperswitch/authentication/TypeAliases.kt @@ -0,0 +1,58 @@ +package io.hyperswitch.authentication + +/** + * Type aliases and wrappers for 3DS authentication types + * + * These allow merchants to use io.hyperswitch.authentication.* namespace + * while the actual implementation comes from io.hyperswitch.modular_3ds + */ + +// Session and Transaction types +typealias Session = io.hyperswitch.modular_3ds.api.ThreeDSSession +typealias Transaction = io.hyperswitch.modular_3ds.api.ThreeDSTransaction + +// Configuration and Parameters +typealias AuthenticationConfiguration = io.hyperswitch.modular_3ds.api.AuthenticationConfiguration +typealias AuthenticationRequestParameters = io.hyperswitch.modular_3ds.api.AuthenticationRequestParameters +typealias ChallengeParameters = io.hyperswitch.modular_3ds.models.ChallengeParameters + +// Callbacks +typealias ChallengeStatusReceiver = io.hyperswitch.modular_3ds.api.ChallengeStatusReceiver + +// Events +typealias CompletionEvent = io.hyperswitch.modular_3ds.api.CompletionEvent +typealias ProtocolErrorEvent = io.hyperswitch.modular_3ds.api.ProtocolErrorEvent +typealias RuntimeErrorEvent = io.hyperswitch.modular_3ds.api.RuntimeErrorEvent +typealias ErrorMessage = io.hyperswitch.modular_3ds.api.ErrorMessage + +// Environment and Customization +typealias ThreeDSEnvironment = io.hyperswitch.modular_3ds.models.ThreeDSEnvironment +typealias UiCustomization = io.hyperswitch.modular_3ds.models.UiCustomization +typealias ToolbarCustomization = io.hyperswitch.modular_3ds.models.ToolbarCustomization + +// Provider Registration +typealias ProviderRegistry = io.hyperswitch.modular_3ds.provider.ProviderRegistry +typealias ProviderFactory = io.hyperswitch.modular_3ds.provider.ProviderFactory + +/** + * Wrapper for AuthenticationResult sealed class + * This is needed because type aliases don't work well with sealed classes and their nested types + */ +sealed class AuthenticationResult { + object Success : AuthenticationResult() + data class Error(val message: String) : AuthenticationResult() + object Challenge : AuthenticationResult() + + companion object { + /** + * Convert from modular_3ds AuthenticationResult to wrapper AuthenticationResult + */ + internal fun from(result: io.hyperswitch.modular_3ds.api.AuthenticationResult): AuthenticationResult { + return when (result) { + is io.hyperswitch.modular_3ds.api.AuthenticationResult.Success -> Success + is io.hyperswitch.modular_3ds.api.AuthenticationResult.Error -> Error(result.message) + is io.hyperswitch.modular_3ds.api.AuthenticationResult.Challenge -> Challenge + } + } + } +} diff --git a/demo-app/build.gradle b/demo-app/build.gradle index bf3b9578..382793ee 100644 --- a/demo-app/build.gradle +++ b/demo-app/build.gradle @@ -84,12 +84,13 @@ dependencies { implementation 'com.github.kittinunf.fuel:fuel-json:2.3.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' - implementation project(':app') + implementation project(':app') implementation ('com.wix:detox:+') androidTestImplementation('com.wix:detox:+') implementation 'androidx.appcompat:appcompat:1.1.0' implementation project(':hyperswitch-sdk-android-lite') - implementation(project(":hyperswitch-scan-card-lite")) + implementation project(':react-native-hyperswitch-unified-3ds') + implementation ('io.hyperswitch:modular-3ds-trident:1.0.0') compileOnly 'com.facebook.react:react-android' implementation 'org.slf4j:slf4j-nop:2.0.9' } \ No newline at end of file diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml index f500f4d4..4bb1e07a 100644 --- a/demo-app/src/main/AndroidManifest.xml +++ b/demo-app/src/main/AndroidManifest.xml @@ -1,6 +1,10 @@ + + + + - + + + \ No newline at end of file diff --git a/demo-app/src/main/java/io/hyperswitch/demoapp/MainActivity.kt b/demo-app/src/main/java/io/hyperswitch/demoapp/MainActivity.kt index 620f58f4..4425acf9 100644 --- a/demo-app/src/main/java/io/hyperswitch/demoapp/MainActivity.kt +++ b/demo-app/src/main/java/io/hyperswitch/demoapp/MainActivity.kt @@ -312,6 +312,12 @@ class MainActivity : Activity() { startActivity(intent) } + findViewById(R.id.launchAuthenticationButton).setOnClickListener{ + val intent = Intent(this, ThreeDSTestActivity::class.java) + intent.putExtra("publishKey", publishKey) + intent.putExtra("clientSecret", paymentIntentClientSecret) + startActivity(intent) + } } private fun setStatus(error: String) { diff --git a/demo-app/src/main/java/io/hyperswitch/demoapp/ThreeDSTestActivity.kt b/demo-app/src/main/java/io/hyperswitch/demoapp/ThreeDSTestActivity.kt new file mode 100644 index 00000000..19ad1636 --- /dev/null +++ b/demo-app/src/main/java/io/hyperswitch/demoapp/ThreeDSTestActivity.kt @@ -0,0 +1,496 @@ +package io.hyperswitch.demoapp + +import android.app.Activity +import android.os.Bundle +import android.util.Log +import android.widget.Button +import android.widget.TextView +import io.hyperswitch.authentication.* +import io.hyperswitch.modular_3ds.trident.TridentProviderFactory +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.io.IOException + +class ThreeDSTestActivity : Activity() { + + private lateinit var statusText: TextView + private var publishKey: String = "" + private var paymentIntentClientSecret: String = "" + // New merchant-facing API objects + private lateinit var authenticationSession: AuthenticationSession + private var session: Session? = null + private var transaction: Transaction? = null + private var aReqParams: AuthenticationRequestParameters? = null + private var challengeParameters: ChallengeParameters? = null + // API credentials and data + private val apiKey = "" + private val profileId = "" + private val baseUrl = "https://sandbox.hyperswitch.io" + private var authenticationId: String? = null + // Eligibility response data + private var threeDsServerTransactionId: String? = null + private var messageVersion: String? = null + private var directoryServerId: String? = null + // HTTP client - use singleton to avoid creating multiple instances + private val httpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(15, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(15, java.util.concurrent.TimeUnit.SECONDS) + .build() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.three_ds_test_activity) + + // Get data from intent + publishKey = intent.getStringExtra("publishKey") ?: "" + paymentIntentClientSecret = intent.getStringExtra("clientSecret") ?: "" + + statusText = findViewById(R.id.statusText) + + // Register Trident 3DS provider + ProviderRegistry.registerProvider(TridentProviderFactory()) + + // Initialize AuthenticationSession + authenticationSession = AuthenticationSession(this, publishKey) + + setupButtons() + } + + override fun onDestroy() { + super.onDestroy() + // Clean up resources to prevent memory leaks + session = null + transaction = null + aReqParams = null + challengeParameters = null + + // Cancel any pending HTTP calls + try { + httpClient.dispatcher.executorService.shutdown() + } catch (e: Exception) { + Log.w("ThreeDSTest", "Error shutting down HTTP client: ${e.message}") + } + } + + private fun setupButtons() { + findViewById