auth/kotlin_otp_fix.md

232 lines
8.6 KiB
Markdown

# 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<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:
```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