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:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -12,6 +12,7 @@ struct AccessibilityIdentifiers {
|
||||
static let signUpButton = "Login.SignUpButton"
|
||||
static let forgotPasswordButton = "Login.ForgotPasswordButton"
|
||||
static let passwordVisibilityToggle = "Login.PasswordVisibilityToggle"
|
||||
static let appleSignInButton = "Login.AppleSignInButton"
|
||||
|
||||
// Registration
|
||||
static let registerUsernameField = "Register.UsernameField"
|
||||
|
||||
@@ -12,6 +12,7 @@ struct AccessibilityIdentifiers {
|
||||
static let signUpButton = "Login.SignUpButton"
|
||||
static let forgotPasswordButton = "Login.ForgotPasswordButton"
|
||||
static let passwordVisibilityToggle = "Login.PasswordVisibilityToggle"
|
||||
static let appleSignInButton = "Login.AppleSignInButton"
|
||||
|
||||
// Registration
|
||||
static let registerUsernameField = "Register.UsernameField"
|
||||
|
||||
176
iosApp/iosApp/Login/AppleSignInManager.swift
Normal file
176
iosApp/iosApp/Login/AppleSignInManager.swift
Normal file
@@ -0,0 +1,176 @@
|
||||
import Foundation
|
||||
import AuthenticationServices
|
||||
|
||||
/// Handles Sign in with Apple authentication flow
|
||||
class AppleSignInManager: NSObject, ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var isProcessing: Bool = false
|
||||
@Published var error: Error?
|
||||
|
||||
// MARK: - Completion Handler
|
||||
private var completionHandler: ((Result<AppleSignInCredential, Error>) -> Void)?
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Initiates the Sign in with Apple flow
|
||||
/// - Parameter completion: Callback with either credentials or error
|
||||
func signIn(completion: @escaping (Result<AppleSignInCredential, Error>) -> Void) {
|
||||
self.completionHandler = completion
|
||||
self.error = nil
|
||||
self.isProcessing = true
|
||||
|
||||
let request = ASAuthorizationAppleIDProvider().createRequest()
|
||||
request.requestedScopes = [.fullName, .email]
|
||||
|
||||
let controller = ASAuthorizationController(authorizationRequests: [request])
|
||||
controller.delegate = self
|
||||
controller.presentationContextProvider = self
|
||||
controller.performRequests()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ASAuthorizationControllerDelegate
|
||||
|
||||
extension AppleSignInManager: ASAuthorizationControllerDelegate {
|
||||
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
|
||||
isProcessing = false
|
||||
|
||||
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
|
||||
let error = AppleSignInError.invalidCredential
|
||||
self.error = error
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// Get the identity token as a string
|
||||
guard let identityTokenData = appleIDCredential.identityToken,
|
||||
let identityToken = String(data: identityTokenData, encoding: .utf8) else {
|
||||
let error = AppleSignInError.missingIdentityToken
|
||||
self.error = error
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user info (only available on first sign in)
|
||||
let email = appleIDCredential.email
|
||||
let firstName = appleIDCredential.fullName?.givenName
|
||||
let lastName = appleIDCredential.fullName?.familyName
|
||||
let userIdentifier = appleIDCredential.user
|
||||
|
||||
let credential = AppleSignInCredential(
|
||||
identityToken: identityToken,
|
||||
userIdentifier: userIdentifier,
|
||||
email: email,
|
||||
firstName: firstName,
|
||||
lastName: lastName
|
||||
)
|
||||
|
||||
completionHandler?(.success(credential))
|
||||
}
|
||||
|
||||
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
|
||||
isProcessing = false
|
||||
|
||||
// Check if user cancelled
|
||||
if let authError = error as? ASAuthorizationError {
|
||||
switch authError.code {
|
||||
case .canceled:
|
||||
// User cancelled, don't treat as error
|
||||
self.error = AppleSignInError.userCancelled
|
||||
completionHandler?(.failure(AppleSignInError.userCancelled))
|
||||
return
|
||||
case .failed:
|
||||
self.error = AppleSignInError.authorizationFailed
|
||||
completionHandler?(.failure(AppleSignInError.authorizationFailed))
|
||||
return
|
||||
case .invalidResponse:
|
||||
self.error = AppleSignInError.invalidResponse
|
||||
completionHandler?(.failure(AppleSignInError.invalidResponse))
|
||||
return
|
||||
case .notHandled:
|
||||
self.error = AppleSignInError.notHandled
|
||||
completionHandler?(.failure(AppleSignInError.notHandled))
|
||||
return
|
||||
case .notInteractive:
|
||||
self.error = AppleSignInError.notInteractive
|
||||
completionHandler?(.failure(AppleSignInError.notInteractive))
|
||||
return
|
||||
case .unknown:
|
||||
break // Fall through to generic error
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
self.error = error
|
||||
completionHandler?(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ASAuthorizationControllerPresentationContextProviding
|
||||
|
||||
extension AppleSignInManager: ASAuthorizationControllerPresentationContextProviding {
|
||||
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
||||
// Get the key window for presentation
|
||||
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = scene.windows.first(where: { $0.isKeyWindow }) else {
|
||||
// Fallback to first window
|
||||
return UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap { $0.windows }
|
||||
.first ?? ASPresentationAnchor()
|
||||
}
|
||||
return window
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Supporting Types
|
||||
|
||||
/// Credentials returned from successful Apple Sign In
|
||||
struct AppleSignInCredential {
|
||||
let identityToken: String
|
||||
let userIdentifier: String
|
||||
let email: String?
|
||||
let firstName: String?
|
||||
let lastName: String?
|
||||
}
|
||||
|
||||
/// Custom errors for Apple Sign In
|
||||
enum AppleSignInError: LocalizedError {
|
||||
case invalidCredential
|
||||
case missingIdentityToken
|
||||
case userCancelled
|
||||
case authorizationFailed
|
||||
case invalidResponse
|
||||
case notHandled
|
||||
case notInteractive
|
||||
case serverError(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidCredential:
|
||||
return "Invalid Apple ID credential"
|
||||
case .missingIdentityToken:
|
||||
return "Missing identity token from Apple"
|
||||
case .userCancelled:
|
||||
return nil // Don't show error for user cancellation
|
||||
case .authorizationFailed:
|
||||
return "Apple Sign In authorization failed"
|
||||
case .invalidResponse:
|
||||
return "Invalid response from Apple"
|
||||
case .notHandled:
|
||||
return "Apple Sign In request was not handled"
|
||||
case .notInteractive:
|
||||
return "Apple Sign In requires user interaction"
|
||||
case .serverError(let message):
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
var isCancellation: Bool {
|
||||
if case .userCancelled = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
171
iosApp/iosApp/Login/AppleSignInViewModel.swift
Normal file
171
iosApp/iosApp/Login/AppleSignInViewModel.swift
Normal file
@@ -0,0 +1,171 @@
|
||||
import Foundation
|
||||
import ComposeApp
|
||||
import Combine
|
||||
|
||||
/// ViewModel for handling Apple Sign In flow
|
||||
/// Coordinates between AppleSignInManager (iOS) and AuthViewModel (Kotlin)
|
||||
@MainActor
|
||||
class AppleSignInViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var isNewUser: Bool = false
|
||||
|
||||
// MARK: - Private Properties
|
||||
private let appleSignInManager = AppleSignInManager()
|
||||
private let sharedViewModel: ComposeApp.AuthViewModel
|
||||
private let tokenStorage: TokenStorageProtocol
|
||||
|
||||
// MARK: - Callbacks
|
||||
var onSignInSuccess: ((Bool) -> Void)? // Bool indicates if user is verified
|
||||
|
||||
// MARK: - Initialization
|
||||
init(
|
||||
sharedViewModel: ComposeApp.AuthViewModel? = nil,
|
||||
tokenStorage: TokenStorageProtocol? = nil
|
||||
) {
|
||||
self.sharedViewModel = sharedViewModel ?? ComposeApp.AuthViewModel()
|
||||
self.tokenStorage = tokenStorage ?? Dependencies.current.makeTokenStorage()
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Initiates the Apple Sign In flow
|
||||
func signInWithApple() {
|
||||
guard !isLoading else { return }
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
appleSignInManager.signIn { [weak self] result in
|
||||
Task { @MainActor in
|
||||
switch result {
|
||||
case .success(let credential):
|
||||
self?.sendCredentialToBackend(credential)
|
||||
case .failure(let error):
|
||||
self?.handleAppleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the error state
|
||||
func clearError() {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/// Sends Apple credential to backend for verification/authentication
|
||||
private func sendCredentialToBackend(_ credential: AppleSignInCredential) {
|
||||
sharedViewModel.appleSignIn(
|
||||
idToken: credential.identityToken,
|
||||
userId: credential.userIdentifier,
|
||||
email: credential.email,
|
||||
firstName: credential.firstName,
|
||||
lastName: credential.lastName
|
||||
)
|
||||
|
||||
// Observe the result
|
||||
Task {
|
||||
for await state in sharedViewModel.appleSignInState {
|
||||
if state is ApiResultLoading {
|
||||
await MainActor.run {
|
||||
self.isLoading = true
|
||||
}
|
||||
} else if let success = state as? ApiResultSuccess<AppleSignInResponse> {
|
||||
await MainActor.run {
|
||||
self.handleSuccess(success.data)
|
||||
}
|
||||
break
|
||||
} else if let error = state as? ApiResultError {
|
||||
await MainActor.run {
|
||||
self.handleBackendError(error)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles successful authentication
|
||||
private func handleSuccess(_ response: AppleSignInResponse?) {
|
||||
isLoading = false
|
||||
|
||||
guard let response = response,
|
||||
let token = response.token as String? else {
|
||||
errorMessage = "Invalid response from server"
|
||||
return
|
||||
}
|
||||
|
||||
let user = response.user
|
||||
|
||||
// Store the token
|
||||
tokenStorage.saveToken(token: token)
|
||||
|
||||
// Track if this is a new user
|
||||
isNewUser = response.isNewUser
|
||||
|
||||
// Initialize lookups
|
||||
Task {
|
||||
_ = try? await APILayer.shared.initializeLookups()
|
||||
}
|
||||
|
||||
// Prefetch data
|
||||
Task {
|
||||
do {
|
||||
print("Starting data prefetch after Apple Sign In...")
|
||||
let prefetchManager = DataPrefetchManager.Companion().getInstance()
|
||||
_ = try await prefetchManager.prefetchAllData()
|
||||
print("Data prefetch completed successfully")
|
||||
} catch {
|
||||
print("Data prefetch failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
print("Apple Sign In successful! User: \(user.username), New user: \(isNewUser)")
|
||||
|
||||
// Call success callback with verification status
|
||||
onSignInSuccess?(user.verified)
|
||||
}
|
||||
|
||||
/// Handles Apple Sign In errors
|
||||
private func handleAppleError(_ error: Error) {
|
||||
isLoading = false
|
||||
|
||||
if let appleError = error as? AppleSignInError {
|
||||
// Don't show error for user cancellation
|
||||
if appleError.isCancellation {
|
||||
return
|
||||
}
|
||||
errorMessage = appleError.errorDescription
|
||||
} else {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles backend API errors
|
||||
private func handleBackendError(_ error: ApiResultError) {
|
||||
isLoading = false
|
||||
sharedViewModel.resetAppleSignInState()
|
||||
|
||||
if let code = error.code?.intValue {
|
||||
switch code {
|
||||
case 400:
|
||||
errorMessage = "Invalid Apple Sign In token"
|
||||
case 401:
|
||||
errorMessage = "Authentication failed. Please try again."
|
||||
case 403:
|
||||
errorMessage = "Access denied"
|
||||
case 500...599:
|
||||
errorMessage = "Server error. Please try again later."
|
||||
default:
|
||||
errorMessage = ErrorMessageParser.parse(error.message)
|
||||
}
|
||||
} else {
|
||||
errorMessage = ErrorMessageParser.parse(error.message)
|
||||
}
|
||||
|
||||
print("Apple Sign In backend error: \(error.message)")
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import SwiftUI
|
||||
import AuthenticationServices
|
||||
|
||||
struct LoginView: View {
|
||||
@StateObject private var viewModel = LoginViewModel()
|
||||
@StateObject private var appleSignInViewModel = AppleSignInViewModel()
|
||||
@FocusState private var focusedField: Field?
|
||||
@State private var showingRegister = false
|
||||
@State private var showVerification = false
|
||||
@@ -202,6 +204,70 @@ struct LoginView: View {
|
||||
.disabled(!isFormValid || viewModel.isLoading)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.loginButton)
|
||||
|
||||
// Divider
|
||||
HStack {
|
||||
Rectangle()
|
||||
.fill(Color.appTextSecondary.opacity(0.3))
|
||||
.frame(height: 1)
|
||||
Text("or")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.padding(.horizontal, AppSpacing.sm)
|
||||
Rectangle()
|
||||
.fill(Color.appTextSecondary.opacity(0.3))
|
||||
.frame(height: 1)
|
||||
}
|
||||
.padding(.vertical, AppSpacing.xs)
|
||||
|
||||
// Sign in with Apple Button
|
||||
SignInWithAppleButton(
|
||||
onRequest: { request in
|
||||
request.requestedScopes = [.fullName, .email]
|
||||
},
|
||||
onCompletion: { _ in }
|
||||
)
|
||||
.frame(height: 56)
|
||||
.cornerRadius(AppRadius.md)
|
||||
.signInWithAppleButtonStyle(.black)
|
||||
.disabled(appleSignInViewModel.isLoading)
|
||||
.opacity(appleSignInViewModel.isLoading ? 0.6 : 1.0)
|
||||
.overlay {
|
||||
// Custom tap handler to use our view model
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
appleSignInViewModel.signInWithApple()
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.appleSignInButton)
|
||||
|
||||
// Apple Sign In loading indicator
|
||||
if appleSignInViewModel.isLoading {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
Text("Signing in with Apple...")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.padding(.top, AppSpacing.xs)
|
||||
}
|
||||
|
||||
// Apple Sign In Error
|
||||
if let appleError = appleSignInViewModel.errorMessage {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundColor(Color.appError)
|
||||
Text(appleError)
|
||||
.font(.callout)
|
||||
.foregroundColor(Color.appError)
|
||||
Spacer()
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(Color.appError.opacity(0.1))
|
||||
.cornerRadius(AppRadius.md)
|
||||
}
|
||||
|
||||
// Sign Up Link
|
||||
HStack(spacing: AppSpacing.xs) {
|
||||
Text("Don't have an account?")
|
||||
@@ -242,6 +308,20 @@ struct LoginView: View {
|
||||
// since AuthenticationManager.isVerified is now false
|
||||
}
|
||||
}
|
||||
|
||||
// Set up callback for Apple Sign In success
|
||||
appleSignInViewModel.onSignInSuccess = { [self] isVerified in
|
||||
// Update the shared authentication manager
|
||||
AuthenticationManager.shared.login(verified: isVerified)
|
||||
|
||||
if isVerified {
|
||||
// User is verified, call the success callback
|
||||
self.onLoginSuccess?()
|
||||
} else {
|
||||
// User needs verification - RootView will handle showing VerifyEmailView
|
||||
// since AuthenticationManager.isVerified is now false
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showVerification) {
|
||||
VerifyEmailView(
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.mycrib.MyCribDev</string>
|
||||
|
||||
Reference in New Issue
Block a user