Add Sign in with Apple for iOS

Kotlin Shared Layer:
- Add AppleSignInRequest and AppleSignInResponse models
- Add appleSignIn method to AuthApi and APILayer
- Add appleSignInState and appleSignIn() to AuthViewModel

iOS App:
- Create AppleSignInManager for AuthenticationServices integration
- Create AppleSignInViewModel to coordinate Apple auth flow
- Update LoginView with "Sign in with Apple" button
- Add Sign in with Apple entitlement
- Add accessibility identifier for UI testing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-29 01:17:38 -06:00
parent 4b905ad5fe
commit 5a1a87fe8d
10 changed files with 529 additions and 0 deletions

View File

@@ -162,3 +162,27 @@ data class ResetPasswordResponse(
data class MessageResponse(
val message: String
)
// Apple Sign In Models
/**
* Apple Sign In request matching Go API
*/
@Serializable
data class AppleSignInRequest(
@SerialName("id_token") val idToken: String,
@SerialName("user_id") val userId: String,
val email: String? = null,
@SerialName("first_name") val firstName: String? = null,
@SerialName("last_name") val lastName: String? = null
)
/**
* Apple Sign In response matching Go API
*/
@Serializable
data class AppleSignInResponse(
val token: String,
val user: User,
@SerialName("is_new_user") val isNewUser: Boolean
)

View File

@@ -921,6 +921,20 @@ object APILayer {
return authApi.resetPassword(request)
}
suspend fun appleSignIn(request: AppleSignInRequest): ApiResult<AppleSignInResponse> {
val result = authApi.appleSignIn(request)
// Update cache on success
if (result is ApiResult.Success) {
TokenStorage.saveToken(result.data.token)
DataCache.updateCurrentUser(result.data.user)
// Prefetch all data after successful Apple sign in
prefetchManager.prefetchAllData()
}
return result
}
suspend fun updateProfile(token: String, request: UpdateProfileRequest): ApiResult<User> {
val result = authApi.updateProfile(token, request)

View File

@@ -200,4 +200,27 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
// Apple Sign In
suspend fun appleSignIn(request: AppleSignInRequest): ApiResult<AppleSignInResponse> {
return try {
val response = client.post("$baseUrl/auth/apple-sign-in/") {
contentType(ContentType.Application.Json)
setBody(request)
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
val errorBody = try {
response.body<Map<String, String>>()
} catch (e: Exception) {
mapOf("error" to "Apple Sign In failed")
}
ApiResult.Error(errorBody["error"] ?: "Apple Sign In failed", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
}

View File

@@ -2,6 +2,8 @@ package com.example.casera.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.casera.models.AppleSignInRequest
import com.example.casera.models.AppleSignInResponse
import com.example.casera.models.AuthResponse
import com.example.casera.models.ForgotPasswordRequest
import com.example.casera.models.ForgotPasswordResponse
@@ -48,6 +50,9 @@ class AuthViewModel : ViewModel() {
private val _resetPasswordState = MutableStateFlow<ApiResult<ResetPasswordResponse>>(ApiResult.Idle)
val resetPasswordState: StateFlow<ApiResult<ResetPasswordResponse>> = _resetPasswordState
private val _appleSignInState = MutableStateFlow<ApiResult<AppleSignInResponse>>(ApiResult.Idle)
val appleSignInState: StateFlow<ApiResult<AppleSignInResponse>> = _appleSignInState
fun login(username: String, password: String) {
viewModelScope.launch {
_loginState.value = ApiResult.Loading
@@ -207,6 +212,36 @@ class AuthViewModel : ViewModel() {
_resetPasswordState.value = ApiResult.Idle
}
fun appleSignIn(
idToken: String,
userId: String,
email: String?,
firstName: String?,
lastName: String?
) {
viewModelScope.launch {
_appleSignInState.value = ApiResult.Loading
val result = APILayer.appleSignIn(
AppleSignInRequest(
idToken = idToken,
userId = userId,
email = email,
firstName = firstName,
lastName = lastName
)
)
_appleSignInState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(result.data)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
fun resetAppleSignInState() {
_appleSignInState.value = ApiResult.Idle
}
fun logout() {
viewModelScope.launch {
APILayer.logout()