Add premium features and reorganize Settings tab
Premium Features: - Journal notes and photo attachments for mood entries - Data export (CSV and PDF reports) - Privacy lock with Face ID/Touch ID - Apple Health integration for mood correlation - 4 new personality packs (Motivational Coach, Zen Master, Best Friend, Data Analyst) Settings Tab Reorganization: - Combined Customize and Settings into single tab with segmented control - Added upgrade banner with trial countdown above segment - "Why Upgrade?" sheet showing all premium benefits - Subscribe button opens improved StoreKit 2 subscription view UI Improvements: - Enhanced subscription store with feature highlights - Entry detail view for viewing/editing notes and photos - Removed duplicate subscription banners from tab content 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
173
Shared/Services/BiometricAuthManager.swift
Normal file
173
Shared/Services/BiometricAuthManager.swift
Normal file
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// BiometricAuthManager.swift
|
||||
// Feels
|
||||
//
|
||||
// Manages Face ID / Touch ID authentication for app privacy lock.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
class BiometricAuthManager: ObservableObject {
|
||||
|
||||
// MARK: - Published State
|
||||
|
||||
@Published var isUnlocked: Bool = true
|
||||
@Published var isAuthenticating: Bool = false
|
||||
|
||||
// MARK: - App Storage
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.privacyLockEnabled.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
var isLockEnabled: Bool = false
|
||||
|
||||
// MARK: - Biometric Capabilities
|
||||
|
||||
var canUseBiometrics: Bool {
|
||||
let context = LAContext()
|
||||
var error: NSError?
|
||||
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
|
||||
}
|
||||
|
||||
var canUseDevicePasscode: Bool {
|
||||
let context = LAContext()
|
||||
var error: NSError?
|
||||
return context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
|
||||
}
|
||||
|
||||
var biometricType: LABiometryType {
|
||||
let context = LAContext()
|
||||
var error: NSError?
|
||||
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
|
||||
return context.biometryType
|
||||
}
|
||||
|
||||
var biometricName: String {
|
||||
switch biometricType {
|
||||
case .faceID:
|
||||
return "Face ID"
|
||||
case .touchID:
|
||||
return "Touch ID"
|
||||
case .opticID:
|
||||
return "Optic ID"
|
||||
@unknown default:
|
||||
return "Biometrics"
|
||||
}
|
||||
}
|
||||
|
||||
var biometricIcon: String {
|
||||
switch biometricType {
|
||||
case .faceID:
|
||||
return "faceid"
|
||||
case .touchID:
|
||||
return "touchid"
|
||||
case .opticID:
|
||||
return "opticid"
|
||||
@unknown default:
|
||||
return "lock.fill"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
func authenticate() async -> Bool {
|
||||
guard isLockEnabled else {
|
||||
isUnlocked = true
|
||||
return true
|
||||
}
|
||||
|
||||
let context = LAContext()
|
||||
context.localizedCancelTitle = "Cancel"
|
||||
|
||||
// Try biometrics first, fall back to device passcode
|
||||
let policy: LAPolicy = canUseBiometrics ? .deviceOwnerAuthenticationWithBiometrics : .deviceOwnerAuthentication
|
||||
|
||||
isAuthenticating = true
|
||||
defer { isAuthenticating = false }
|
||||
|
||||
do {
|
||||
let success = try await context.evaluatePolicy(
|
||||
policy,
|
||||
localizedReason: "Unlock Feels to access your mood data"
|
||||
)
|
||||
|
||||
isUnlocked = success
|
||||
if success {
|
||||
EventLogger.log(event: "biometric_unlock_success")
|
||||
}
|
||||
return success
|
||||
} catch {
|
||||
print("Authentication failed: \(error.localizedDescription)")
|
||||
EventLogger.log(event: "biometric_unlock_failed", withData: ["error": error.localizedDescription])
|
||||
|
||||
// If biometrics failed, try device passcode as fallback
|
||||
if canUseDevicePasscode && policy == .deviceOwnerAuthenticationWithBiometrics {
|
||||
return await authenticateWithPasscode()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticateWithPasscode() async -> Bool {
|
||||
let context = LAContext()
|
||||
|
||||
do {
|
||||
let success = try await context.evaluatePolicy(
|
||||
.deviceOwnerAuthentication,
|
||||
localizedReason: "Unlock Feels to access your mood data"
|
||||
)
|
||||
|
||||
isUnlocked = success
|
||||
return success
|
||||
} catch {
|
||||
print("Passcode authentication failed: \(error.localizedDescription)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lock Management
|
||||
|
||||
func lock() {
|
||||
guard isLockEnabled else { return }
|
||||
isUnlocked = false
|
||||
EventLogger.log(event: "app_locked")
|
||||
}
|
||||
|
||||
func enableLock() async -> Bool {
|
||||
// Authenticate first to enable lock - require biometrics
|
||||
let context = LAContext()
|
||||
var error: NSError?
|
||||
|
||||
// Only allow enabling if biometrics are available
|
||||
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
|
||||
print("Biometric authentication not available: \(error?.localizedDescription ?? "Unknown")")
|
||||
return false
|
||||
}
|
||||
|
||||
do {
|
||||
let success = try await context.evaluatePolicy(
|
||||
.deviceOwnerAuthenticationWithBiometrics,
|
||||
localizedReason: "Verify your identity to enable app lock"
|
||||
)
|
||||
|
||||
if success {
|
||||
isLockEnabled = true
|
||||
isUnlocked = true
|
||||
EventLogger.log(event: "privacy_lock_enabled")
|
||||
}
|
||||
|
||||
return success
|
||||
} catch {
|
||||
print("Failed to enable lock: \(error.localizedDescription)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func disableLock() {
|
||||
isLockEnabled = false
|
||||
isUnlocked = true
|
||||
EventLogger.log(event: "privacy_lock_disabled")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user