8.6 KiB
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
- Phone Number Format: Must include
+prefix (E.164 format) - OTP Code Type: Must be sent as string, not integer
- Request Body: Must match exact field names (
phone_number,code) - 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
- Phone Number Normalization: Ensure
+prefix is present - OTP as String: Send OTP as string, not integer
- Request Body Format: Use exact field names from backend
- Error Handling: Check response status and error body
Debugging Steps
-
Add Logging: Log the exact request being sent
Log.d("AuthManager", "Sending verify-otp: phone=$normalizedPhone, code=$otpString") Log.d("AuthManager", "Request body: $requestBody") -
Check Response: Log the response
Log.d("AuthManager", "Response code: ${response.code()}") Log.d("AuthManager", "Response body: ${response.body()?.string()}") -
Compare with HTML: Use the same phone number and OTP in HTML test page to verify backend is working
Most Likely Issues
- Phone number missing
+prefix - Backend normalizes but expects E.164 format - OTP sent as number instead of string - Backend expects string
- Wrong field names - Must be
phone_numberandcode(with underscores) - Request body not properly serialized - Check your JSON serialization