# Kotlin OTP Verification Fix ## Issue Analysis The backend `/auth/verify-otp` endpoint expects: ```json { "phone_number": "+919876543210", // Must be E.164 format with + "code": "123456", // String, not number "device_id": "optional-device-id", "device_info": { "platform": "android", "model": "device-model", "os_version": "Android 13", "app_version": "1.0.0", "language_code": "en", "timezone": "Asia/Kolkata" } } ``` ## Common Issues in Kotlin Implementation 1. **Phone Number Format**: Must include `+` prefix (E.164 format) 2. **OTP Code Type**: Must be sent as string, not integer 3. **Request Body**: Must match exact field names (`phone_number`, `code`) 4. **Missing Device Info**: Backend accepts but doesn't require device_info ## Fixed Kotlin Code ### Option 1: Update AuthManager.login() method ```kotlin // In AuthManager.kt or AuthApiClient.kt suspend fun login(phoneNumber: String, otpCode: String): Result { return try { // Ensure phone number has + prefix (E.164 format) val normalizedPhone = if (phoneNumber.startsWith("+")) { phoneNumber } else if (phoneNumber.length == 10) { "+91$phoneNumber" // Add +91 for 10-digit Indian numbers } else { phoneNumber // Keep as is if already formatted } // Ensure OTP is string (not integer) val otpString = otpCode.toString().trim() // Get device info val deviceId = getDeviceId() // Your method to get device ID val deviceInfo = getDeviceInfo() // Your method to get device info val requestBody = mapOf( "phone_number" to normalizedPhone, "code" to otpString, "device_id" to deviceId, "device_info" to deviceInfo ) val response = apiClient.post("/auth/verify-otp", requestBody) if (response.isSuccessful) { val loginResponse = response.body() // Parse your LoginResponse Result.success(loginResponse) } else { // Handle error response val errorBody = response.errorBody()?.string() Result.failure(Exception("OTP verification failed: $errorBody")) } } catch (e: Exception) { Result.failure(e) } } // Helper function to get device info private fun getDeviceInfo(): Map { return mapOf( "platform" to "android", "model" to android.os.Build.MODEL, "os_version" to android.os.Build.VERSION.RELEASE, "app_version" to getAppVersion(), // Your method "language_code" to Locale.getDefault().language, "timezone" to TimeZone.getDefault().id ) } private fun getDeviceId(): String { // Use your existing device ID logic // Could be Android ID, UUID, etc. return Settings.Secure.getString( context.contentResolver, Settings.Secure.ANDROID_ID ) ?: UUID.randomUUID().toString() } ``` ### Option 2: Quick Fix in OtpScreen.kt Update your `OtpScreen.kt` to ensure proper formatting: ```kotlin @Composable fun OtpScreen(navController: NavController, phoneNumber: String, name: String) { val otp = remember { mutableStateOf("") } val context = LocalContext.current.applicationContext val scope = rememberCoroutineScope() val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) } val isSignInFlow = name == "existing_user" // Normalize phone number to ensure it has + prefix val normalizedPhone = remember(phoneNumber) { if (phoneNumber.startsWith("+")) { phoneNumber } else if (phoneNumber.length == 10) { "+91$phoneNumber" } else { phoneNumber } } Box( modifier = Modifier.fillMaxSize() ) { Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 36.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.height(200.dp)) Text("Enter OTP", fontSize = 24.sp, fontWeight = FontWeight.Medium, color = Color(0xFF364153)) Spacer(modifier = Modifier.height(32.dp)) TextField( value = otp.value, onValueChange = { if (it.length <= 6 && it.all { char -> char.isDigit() }) otp.value = it }, modifier = Modifier .fillMaxWidth() .height(60.dp) .shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)), shape = RoundedCornerShape(16.dp), colors = TextFieldDefaults.colors( focusedContainerColor = Color.White.copy(alpha = 0.9f), unfocusedContainerColor = Color.White.copy(alpha = 0.9f), disabledContainerColor = Color.White.copy(alpha = 0.9f), focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center, fontSize = 24.sp), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) ) Spacer(modifier = Modifier.height(48.dp)) Button( onClick = { scope.launch { // Ensure OTP is not empty and is 6 digits if (otp.value.length != 6) { Toast.makeText(context, "Please enter a valid 6-digit OTP", Toast.LENGTH_SHORT).show() return@launch } // Use normalized phone number authManager.login(normalizedPhone, otp.value.trim()) .onSuccess { response -> if (isSignInFlow) { navController.navigate("success") { popUpTo("login") { inclusive = true } } } else { if (response.needsProfile) { navController.navigate("create_profile/$name") } else { navController.navigate("success") { popUpTo("login") { inclusive = true } } } } } .onFailure { error -> // More detailed error handling val errorMessage = error.message ?: "Invalid or expired OTP" Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() Log.e("OtpScreen", "OTP verification failed", error) } } }, shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFE9A00)), modifier = Modifier .fillMaxWidth() .height(56.dp) .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) ) { Text("Continue", color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.Medium) } } } } ``` ## Key Changes to Check in Your AuthManager/AuthApiClient 1. **Phone Number Normalization**: Ensure `+` prefix is present 2. **OTP as String**: Send OTP as string, not integer 3. **Request Body Format**: Use exact field names from backend 4. **Error Handling**: Check response status and error body ## Debugging Steps 1. **Add Logging**: Log the exact request being sent ```kotlin Log.d("AuthManager", "Sending verify-otp: phone=$normalizedPhone, code=$otpString") Log.d("AuthManager", "Request body: $requestBody") ``` 2. **Check Response**: Log the response ```kotlin Log.d("AuthManager", "Response code: ${response.code()}") Log.d("AuthManager", "Response body: ${response.body()?.string()}") ``` 3. **Compare with HTML**: Use the same phone number and OTP in HTML test page to verify backend is working ## Most Likely Issues 1. **Phone number missing `+` prefix** - Backend normalizes but expects E.164 format 2. **OTP sent as number instead of string** - Backend expects string 3. **Wrong field names** - Must be `phone_number` and `code` (with underscores) 4. **Request body not properly serialized** - Check your JSON serialization