Step 1: Install OTPLESS SDK Dependency

To get started, you need to install dependencies for kotlinx-serialization, kotlin-cocoapods and Otpless Android & iOS dependencies. You can do this by adding following the below mentioned steps:

Adding dependencies in gradle files:

Add the kotlinx-serialization dependency in your libs.versions.toml file:

kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }

In your project level build.gradle.kts file, add the following code:

plugins {
    alias(libs.plugins.kotlinCocoapods) apply false
}

In your build.gradle.kts(:composeApp) file, add the following code:

plugins {
    alias(libs.plugins.kotlinCocoapods)
    kotlin("plugin.serialization") version "2.1.10"
}

Adding Otpless SDK dependencies and kotlinx-serialization in your build.gradle.kts(:composeApp) file:

kotlin {
    sourceSets {
    
        androidMain.dependencies {
            implementation("io.github.otpless-tech:otpless-headless-sdk:0.2.0")
        }
        commonMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
        }
    }

    // Cocoapods configuration: This will create a cocoapod for your iOS app. 
    // So, set the description that you would want in your Podspec file.
    cocoapods {
        homepage = "Your homepage name"
        summary = "Your cocoapods summary"
        version = "1.0"
        ios.deploymentTarget = "15.3"
        podfile = project.file("../iosApp/Podfile")

        framework {
            baseName = "composeApp"
            isStatic = true
        }

        // Add the Otpless SDK dependency to the iOS framework
        pod("OtplessBM/Core") {
            version = "1.1.3"
            extraOpts += listOf("-compiler-option", "-fmodules")
        }
    }
}

Adding dependencies in Podfile:

Navigate to your iosApp directory and initialize the Podfile (if not done already) by running the following command:

cd iosApp
pod init

This will create a Podfile in the iosApp directory. Open the Podfile and add the following lines:

# Pods for iosApp
  pod 'OtplessBM/Core', '1.1.3'

After adding the above lines, run the following command to install the dependencies:

pod install
Once all the dependencies are added, sync the gradle and run pod install to install the iOS app dependency.

Step 2: Platform specific integration

  1. Add intent filter inside your android/app/src/main/AndroidManifest.xml file into your Main activity code block:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
    android:host="otpless"
    android:scheme= "otpless.YOUR_APP_ID_IN_LOWERCASE"/>
</intent-filter>

Replace YOUR_APP_ID with your actual App ID provided in your OTPLESS dashboard.

  1. Add Network Security Config inside your android/app/src/main/AndroidManifest.xml file into your <application> code block (Only required if you are using the SNA feature):
android:networkSecurityConfig="@xml/otpless_network_security_config"
  1. Change your activity launchMode to singleTop and exported true for your Main Activity:
android:launchMode="singleTop"
android:exported="true"
  1. In your MainActivity.kt file, add the following code to handle deeplinks:
import com.otpless.v2.android.sdk.main.OtplessSDK

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    lifecycleScope.launch {
        OtplessSDK.onNewIntent(intent)
    }
}

Step 3: Create expect function declarations in commonMain

In your commonMain source set, create a new file named OtplessAuthHandler.kt and add the following code:

expect fun initializeOtpless(appId: String, onOtplessResponse: (String) -> Unit, loginUri: String?)

expect fun start(otplessCMPRequest: OtplessCMPRequest)

expect fun cleanup()


data class OtplessCMPRequest(
    val phoneNumber: String? = null,
    val countryCode: String? = null,
    val email: String? = null,
    val otp: String? = null,
    val otpExpiry: String? = null,
    val otpLength: String? = null,
    val deliveryChannel: String? =  null,
    val oAuthChannel: String? = null
)

Step 4: Implement expect function declarations in androidMain and iosMain

import com.otpless.v2.android.sdk.dto.OtplessChannelType
import com.otpless.v2.android.sdk.dto.OtplessRequest
import com.otpless.v2.android.sdk.dto.OtplessResponse
import com.otpless.v2.android.sdk.main.OtplessSDK
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

var job: Job? = null

actual fun initializeOtpless(appId: String, onOtplessResponse: (String) -> Unit, loginUri: String?) {
    OtplessSDK.initialize(appId, activity)
    OtplessSDK.setResponseCallback(OtplessResponseHandler::otplessResponseCallback)
    OtplessResponseHandler.setBridgeResponseHandler(onOtplessResponse)
}

actual fun start(otplessCMPRequest: OtplessCMPRequest) {
    val otplessRequest = OtplessRequest()

    if (!otplessCMPRequest.phoneNumber.isNullOrBlank() && !otplessCMPRequest.countryCode.isNullOrBlank()) {
        otplessRequest.setPhoneNumber(
            number = otplessCMPRequest.phoneNumber,
            countryCode = otplessCMPRequest.countryCode
        )
    } else if (!otplessCMPRequest.email.isNullOrBlank()) {
        otplessRequest.setEmail(
            email = otplessCMPRequest.email
        )
    } else {
        otplessRequest.setChannelType(OtplessChannelType.valueOf(otplessCMPRequest.oAuthChannel ?: "NONE"))
    }

    if (!otplessCMPRequest.deliveryChannel.isNullOrBlank()) {
        otplessRequest.setDeliveryChannel(otplessCMPRequest.deliveryChannel)
    }

    if (!otplessCMPRequest.otpExpiry.isNullOrBlank()) {
        otplessRequest.setExpiry(otplessCMPRequest.otpExpiry)
    }

    if (!otplessCMPRequest.otpLength.isNullOrBlank()) {
        otplessRequest.setOtpLength(otplessCMPRequest.otpLength)
    }

    val otp = otplessCMPRequest.otp

    if (!otp.isNullOrBlank()) {
        otplessRequest.setOtp(otp)
        job = CoroutineScope(Dispatchers.IO).launch {
            OtplessSDK.start(request = otplessRequest, OtplessResponseHandler::otplessResponseCallback)
        }
    } else {
        job?.cancel()
        job = CoroutineScope(Dispatchers.IO).launch {
            OtplessSDK.start(request = otplessRequest, OtplessResponseHandler::otplessResponseCallback)
        }
    }
}

object OtplessResponseHandler {
    private var bridgeResponseHandler: ((String) -> Unit)? = null
    fun otplessResponseCallback(otplessResponse: OtplessResponse) {
        OtplessSDK.commit(otplessResponse)
        bridgeResponseHandler?.invoke(
            convertOtplessResponseToJsonString(otplessResponse)
        )
    }

    private fun convertOtplessResponseToJsonString(otplessResponse: OtplessResponse): String {
        return """
        {
            "responseType": "${otplessResponse.responseType}",
            "response": ${otplessResponse.response?.toString() ?: "null"},
            "statusCode": ${otplessResponse.statusCode}
        }
    """.trimIndent()
    }

    fun setBridgeResponseHandler(responseHandler: (String) -> Unit) {
        bridgeResponseHandler = responseHandler
    }
}

actual fun cleanup() {
    OtplessSDK.cleanup()
}

Step 5: Initialize the Otpless SDK

In your LoginScreen.kt file, initialize the Otpless SDK. Make sure to initialize the SDK in DisposableEffect block to free the resources consumed by Otpless once your LoginScreen.kt is destroyed.

DisposableEffect(key1 = Unit) {
    // Use key1 as Unit so that the SDK is not re-initialized on recomposition
    initializeOtpless(
        appId = "YOUR_APPID",
        onOtplessResponse = { otplessResponseString ->
            response = otplessResponseString + "\n\n" + response
            handleOtplessResponse(otplessResponseString, onOTPAutoRead = {
                otp = it
            })
        },
        loginUri = null
    )

    onDispose {
        cleanup()
    }
}

To handle the Otpless response, create a function named handleOtplessResponse in your LoginScreen.kt file. This function will handle the response received from the Otpless SDK.

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

fun handleOtplessResponse(responseJsonString: String, onOTPAutoRead: (String) -> Unit) {
    val parsedElement = Json.parseToJsonElement(responseJsonString).jsonObject

    val responseType = parsedElement["responseType"]?.jsonPrimitive?.contentOrNull
    val statusCode = parsedElement["statusCode"]?.jsonPrimitive?.intOrNull
    val responseJsonObject = parsedElement["response"]?.jsonObject

    when (responseType) {
        "SDK_READY" -> {
            // SDK initialized successfully, enable continue button
            log("SDK is ready!")
        }

        "FAILED" -> {
            log("SDK initialization failed!")
            if (statusCode == 5003) {
                // SDK initialization failed, retry initializing
            } else {
                // General failure handling
            }
        }

        "INITIATE" -> {
            if (statusCode != 200) {
                if (getPlatformName().lowercase() == "android") {
                    handleInitiateErrorAndroid(responseJsonObject)
                } else {
                    handleInitiateErrorIos(responseJsonObject)
                }
            } else {
                val authType = responseJsonObject?.get("authType")?.jsonPrimitive?.contentOrNull
                when (authType) {
                    "OTP" -> {
                        log("Authentication started using OTP")
                        // Take user to OTP verification screen
                    }

                    "SILENT_AUTH" -> {
                        log("Authentication started using Silent Auth")
                        // Handle Silent Authentication initiation (show loading)
                    }
                }
            }
        }

        "OTP_AUTO_READ" -> {
            // Only applicable in Android
            val otp = responseJsonObject?.get("otp")?.jsonPrimitive?.contentOrNull
            if (!otp.isNullOrBlank()) {
                // Autofill OTP in your text field
                onOTPAutoRead(otp)
            }
        }

        "VERIFY" -> {
            val authType = responseJsonObject?.get("authType")?.jsonPrimitive?.contentOrNull
            if (authType == "SILENT_AUTH") {
                if (statusCode == 9106) {
                    // Silent Auth + fallback failed → gracefully exit auth flow
                    log("SNA + fallback failed!")
                } else {
                    log("SNA failed, trying fallback!")
                }
            } else {
                if (getPlatformName().lowercase() == "android") {
                    handleVerifyErrorAndroid(responseJsonObject)
                } else {
                    handleVerifyErrorIos(responseJsonObject)
                }
            }
        }

        "DELIVERY_STATUS" -> {
            val authType = responseJsonObject?.get("authType")?.jsonPrimitive?.contentOrNull
            val deliveryChannel = responseJsonObject?.get("deliveryChannel")?.jsonPrimitive?.contentOrNull
            // Handle delivery status (authType, deliveryChannel)
            log("DELIVERY_STATUS: authType: $authType \b deliveryChannel: $deliveryChannel")
        }

        "FALLBACK_TRIGGERED" -> {
            val newDeliveryChannel = responseJsonObject?.get("deliveryChannel")?.jsonPrimitive?.contentOrNull
            // Handle fallback deliveryChannel
            log("Fallback triggered, new delivery channel: $newDeliveryChannel")
        }

        "ONETAP" -> {
            val data = responseJsonObject?.get("data")?.jsonObject
            val token = data?.get("token")?.jsonPrimitive?.contentOrNull
            if (!token.isNullOrBlank()) {
                // Process token and proceed
                log("Token: $token")
            }
        }
    }
}

Step 6: Start the Otpless SDK

In your LoginScreen.kt file, start the Otpless SDK by calling the start function with the required parameters.

Phone authentication allows users to verify their identity using their phone number. Merchants can choose from various authentication methods:

  • Silent Authentication (SNA) – Automatically verifies the user without requiring OTP or MAGICLINK.
  • OTP on Desired Channel – Sends a one-time password (OTP) via SMS, WhatsApp, or another preferred channel.
  • Magic Link – Sends a link that users can click to authenticate.
  • SNA + OTP – Uses silent authentication first and falls back to OTP if needed.
  • OTP + Magic Link – Sends both an OTP and a magic link, allowing users to authenticate via either method.
val otplessCMPRequest = OtplessCMPRequest(
    phoneNumber = phoneNumber,
    countryCode = countryCode
)
start(otplessCMPRequest)

Verify OTP

To verify the OTP entered by the user, use the verify method with the necessary parameters. Verifying OTP is required only in case of OTP authentication. No need to verify OTP in case of MAGICLINK.

val otplessCMPRequest = OtplessCMPRequest(
    otp = otp,
    phoneNumber = phoneNumber,
    countryCode = countryCode
)
start(otplessCMPRequest)

Error Handling

The error codes for android and iOS have to be handled separately.

private fun handleInitiateErrorAndroid(responseJsonObject: JsonObject?) {
    val errorCode = responseJsonObject?.get("errorCode")?.jsonPrimitive?.contentOrNull
    val errorMessage = responseJsonObject?.get("errorMessage")?.jsonPrimitive?.contentOrNull

    when (errorCode) {
        "7101" -> log("Android OTPless Error: $errorMessage")
        "7102" -> log("Android OTPless Error: $errorMessage")
        "7103" -> log("Android OTPless Error: $errorMessage")
        "7104" -> log("Android OTPless Error: $errorMessage")
        "7105" -> log("Android OTPless Error: $errorMessage")
        "7106" -> log("Android OTPless Error: $errorMessage")
        "7113" -> log("Android OTPless Error: $errorMessage")
        "7116" -> log("Android OTPless Error: $errorMessage")
        "7121" -> log("Android OTPless Error: $errorMessage")
        "4000" -> log("Android OTPless Error: $errorMessage")
        "4003" -> log("Android OTPless Error: $errorMessage")
        "401", "7025" -> log("Android OTPless Error: $errorMessage")
        "7020", "7022", "7023", "7024" -> log("Android OTPless Error: $errorMessage")
        "500" -> log("Android OTPless Error: $errorMessage")
        "9100", "9104", "9103" -> log("Android OTPless Error: $errorMessage")
    }
}