232 lines
8.6 KiB
Markdown
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
|
|
|