auth/kotlin_otp_fix.md

8.6 KiB

Kotlin OTP Verification Fix

Issue Analysis

The backend /auth/verify-otp endpoint expects:

{
  "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

// In AuthManager.kt or AuthApiClient.kt
suspend fun login(phoneNumber: String, otpCode: String): Result<LoginResponse> {
    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<String, String?> {
    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:

@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

    Log.d("AuthManager", "Sending verify-otp: phone=$normalizedPhone, code=$otpString")
    Log.d("AuthManager", "Request body: $requestBody")
    
  2. Check Response: Log the response

    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