Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,22 @@
8FFD60542E849D0D0035E446 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FFD60532E849D0D0035E446 /* RegistrationView.swift */; };
8FFD60602E84A2F70035E446 /* MainAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FFD605F2E84A2F70035E446 /* MainAppView.swift */; };
8FFD60612E84A2F70035E446 /* DebugMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FFD605E2E84A2F70035E446 /* DebugMenuView.swift */; };
9DD6CAAF2F3C426600ED7098 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD6CAAD2F3C426600ED7098 /* Secrets.swift */; };

A1B2C3D42F0A000200000001 /* GeminiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000001 /* GeminiConfig.swift */; };
A1B2C3D42F0A000200000002 /* GeminiLiveService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000002 /* GeminiLiveService.swift */; };
A1B2C3D42F0A000200000003 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000003 /* AudioManager.swift */; };
A1B2C3D42F0A000200000004 /* GeminiSessionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000004 /* GeminiSessionViewModel.swift */; };
A1B2C3D42F0A000200000005 /* GeminiOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000005 /* GeminiOverlayView.swift */; };
E0AAD7C02F3D109200015D5A /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0AAD7BF2F3D109200015D5A /* Secrets.swift */; };
E66D30242E7DA71900470B48 /* MockDeviceKitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66D30232E7DA71900470B48 /* MockDeviceKitButton.swift */; };
E6A188482EB918740097D0E1 /* StreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A188472EB918740097D0E1 /* StreamView.swift */; };
E6DA451D2E79A63100E3F688 /* MockDeviceCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6DA45182E79A63100E3F688 /* MockDeviceCardView.swift */; };
E6DA451E2E79A63100E3F688 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6DA45172E79A63100E3F688 /* CardView.swift */; };
E6DA451F2E79A63100E3F688 /* MockDeviceKitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6DA451A2E79A63100E3F688 /* MockDeviceKitView.swift */; };
E6FD3BCE2EB4D53A00E7FE5D /* NonStreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FD3BCD2EB4D53A00E7FE5D /* NonStreamView.swift */; };
E6FD3BD02EB4D53A00E7FE5E /* SecretsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FD3BCF2EB4D53A00E7FE5E /* SecretsManager.swift */; };
E6FD3BD22EB4D53A00E7FE5F /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FD3BD12EB4D53A00E7FE5F /* SettingsView.swift */; };
E6FD3BD42EB4D53A00E7FE63 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FD3BD32EB4D53A00E7FE63 /* AuthManager.swift */; };
E6FD3BD62EB4D53A00E7FE64 /* SecretsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FD3BD52EB4D53A00E7FE64 /* SecretsView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -86,20 +89,24 @@
8FFD60532E849D0D0035E446 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
8FFD605E2E84A2F70035E446 /* DebugMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugMenuView.swift; sourceTree = "<group>"; };
8FFD605F2E84A2F70035E446 /* MainAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppView.swift; sourceTree = "<group>"; };
9DD6CAAD2F3C426600ED7098 /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
9DD6CAAE2F3C426600ED7098 /* Secrets.swift.example */ = {isa = PBXFileReference; lastKnownFileType = text; path = Secrets.swift.example; sourceTree = "<group>"; };
A1B2C3D42F0A000100000001 /* GeminiConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiConfig.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000002 /* GeminiLiveService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiLiveService.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000003 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000004 /* GeminiSessionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiSessionViewModel.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000005 /* GeminiOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiOverlayView.swift; sourceTree = "<group>"; };
E0AAD7BF2F3D109200015D5A /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
E66D30232E7DA71900470B48 /* MockDeviceKitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceKitButton.swift; sourceTree = "<group>"; };
E699CC952E8150670052C240 /* CameraAccessTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CameraAccessTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E6A188472EB918740097D0E1 /* StreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamView.swift; sourceTree = "<group>"; };
E6DA45172E79A63100E3F688 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; };
E6DA45182E79A63100E3F688 /* MockDeviceCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceCardView.swift; sourceTree = "<group>"; };
E6DA451A2E79A63100E3F688 /* MockDeviceKitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceKitView.swift; sourceTree = "<group>"; };
E6FD3BCD2EB4D53A00E7FE5D /* NonStreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonStreamView.swift; sourceTree = "<group>"; };
E6FD3BCF2EB4D53A00E7FE5E /* SecretsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsManager.swift; sourceTree = "<group>"; };
E6FD3BD12EB4D53A00E7FE5F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
E6FD3BD32EB4D53A00E7FE63 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
E6FD3BD52EB4D53A00E7FE64 /* SecretsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand Down Expand Up @@ -172,14 +179,16 @@
8FD96B752E6F0A9800F56AB1 /* StreamSessionView.swift */,
E6A188472EB918740097D0E1 /* StreamView.swift */,
E6FD3BCD2EB4D53A00E7FE5D /* NonStreamView.swift */,
E6FD3BD12EB4D53A00E7FE5F /* SettingsView.swift */,
E6FD3BD52EB4D53A00E7FE64 /* SecretsView.swift */,
);
path = Views;
sourceTree = "<group>";
};
8FD96B7D2E6F0A9800F56AB1 /* CameraAccess */ = {
isa = PBXGroup;
children = (
9DD6CAAD2F3C426600ED7098 /* Secrets.swift */,
E0AAD7BF2F3D109200015D5A /* Secrets.swift */,
9DD6CAAE2F3C426600ED7098 /* Secrets.swift.example */,
9D3C69602F367CF700E641A5 /* iPhone */,
A1B2C3D42F0A000300000001 /* Gemini */,
Expand Down Expand Up @@ -223,6 +232,8 @@
A1B2C3D42F0A000100000001 /* GeminiConfig.swift */,
A1B2C3D42F0A000100000002 /* GeminiLiveService.swift */,
A1B2C3D42F0A000100000004 /* GeminiSessionViewModel.swift */,
E6FD3BD32EB4D53A00E7FE63 /* AuthManager.swift */,
E6FD3BCF2EB4D53A00E7FE5E /* SecretsManager.swift */,
);
path = Gemini;
sourceTree = "<group>";
Expand Down Expand Up @@ -348,11 +359,10 @@
buildActionMask = 2147483647;
files = (
8FD96B7F2E6F0A9800F56AB1 /* CameraAccessApp.swift in Sources */,
E0AAD7C02F3D109200015D5A /* Secrets.swift in Sources */,
8FD96B812E6F0A9800F56AB1 /* HomeScreenView.swift in Sources */,
8F2D23802E856711002D0588 /* DebugMenuViewModel.swift in Sources */,
8FFD60542E849D0D0035E446 /* RegistrationView.swift in Sources */,
9DD6CAAF2F3C426600ED7098 /* Secrets.swift in Sources */,

8FFD60342E8434070035E446 /* MediaPickerView.swift in Sources */,
8FFD60352E8434070035E446 /* StatusText.swift in Sources */,
8FFD602C2E8433E20035E446 /* MockDeviceKitViewModel.swift in Sources */,
Expand All @@ -371,6 +381,10 @@
8FFD5FF52E8422580035E446 /* CircleButton.swift in Sources */,
8FFD5FF62E8422580035E446 /* CustomButton.swift in Sources */,
E6FD3BCE2EB4D53A00E7FE5D /* NonStreamView.swift in Sources */,
E6FD3BD02EB4D53A00E7FE5E /* SecretsManager.swift in Sources */,
E6FD3BD22EB4D53A00E7FE5F /* SettingsView.swift in Sources */,
E6FD3BD42EB4D53A00E7FE63 /* AuthManager.swift in Sources */,
E6FD3BD62EB4D53A00E7FE64 /* SecretsView.swift in Sources */,
A1B2C3D42F0A000200000001 /* GeminiConfig.swift in Sources */,
A1B2C3D42F0A000200000002 /* GeminiLiveService.swift in Sources */,
A1B2C3D42F0A000200000003 /* AudioManager.swift in Sources */,
Expand Down Expand Up @@ -408,7 +422,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = WY253UX7FC;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "";
INFOPLIST_FILE = CameraAccess/Info.plist;
Expand Down Expand Up @@ -437,7 +451,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = WY253UX7FC;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = CameraAccess/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
Expand Down
140 changes: 140 additions & 0 deletions samples/CameraAccess/CameraAccess/Gemini/AuthManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//
// AuthManager.swift
// CameraAccess
//
// Manages PIN and Face ID authentication for the Secrets view.
// PIN is stored hashed in Keychain. Face ID preference in UserDefaults.
//

import Foundation
import LocalAuthentication
import Security

final class AuthManager: ObservableObject {
static let shared = AuthManager()

private enum Keys {
static let pinHash = "com.cameraaccess.secrets.pinHash"
static let useFaceID = "secrets.useFaceID"
}

private let defaults = UserDefaults.standard

@Published var isUnlocked = false
@Published var useFaceID: Bool {
didSet { defaults.set(useFaceID, forKey: Keys.useFaceID) }
}

var hasPIN: Bool {
KeychainHelper.load(key: Keys.pinHash) != nil
}

var isFaceIDAvailable: Bool {
let context = LAContext()
var error: NSError?
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
}

private init() {
self.useFaceID = defaults.object(forKey: Keys.useFaceID) as? Bool ?? true
}

func setPIN(_ pin: String) -> Bool {
guard pin.count >= 4 else { return false }
let hash = pin.sha256Hash
return KeychainHelper.save(key: Keys.pinHash, value: hash)
}

func verifyPIN(_ pin: String) -> Bool {
guard let stored = KeychainHelper.load(key: Keys.pinHash) else { return false }
return pin.sha256Hash == stored
}

func authenticateWithFaceID(reason: String = "Unlock secrets") async -> Bool {
let context = LAContext()
var error: NSError?

guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
return false
}

do {
let success = try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason
)
await MainActor.run { isUnlocked = success }
return success
} catch {
return false
}
}

func unlock(withPIN pin: String) -> Bool {
let ok = verifyPIN(pin)
if ok { isUnlocked = true }
return ok
}

func lock() {
isUnlocked = false
}

func removePIN() {
KeychainHelper.delete(key: Keys.pinHash)
useFaceID = false
isUnlocked = false
}
}

// MARK: - Keychain Helper

private enum KeychainHelper {
static func save(key: String, value: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
]
SecItemDelete(query as CFDictionary) // Remove existing
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}

static func load(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let string = String(data: data, encoding: .utf8) else { return nil }
return string
}

static func delete(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
}
}

// MARK: - SHA256 Hash

private extension String {
var sha256Hash: String {
guard let data = data(using: .utf8) else { return "" }
let hash = SHA256.hash(data: data)
return hash.map { String(format: "%02x", $0) }.joined()
}
}

import CryptoKit
13 changes: 6 additions & 7 deletions samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,12 @@ enum GeminiConfig {
For messages, confirm recipient and content before delegating unless clearly urgent.
"""

// Secrets are stored in Secrets.swift (gitignored).
// Copy Secrets.example.swift -> Secrets.swift and fill in your values.
static let apiKey = Secrets.geminiAPIKey
static let openClawHost = Secrets.openClawHost
static let openClawPort = Secrets.openClawPort
static let openClawHookToken = Secrets.openClawHookToken
static let openClawGatewayToken = Secrets.openClawGatewayToken
// Secrets are loaded from SecretsManager (device storage). Configure via Settings screen.
static var apiKey: String { SecretsManager.shared.geminiAPIKey }
static var openClawHost: String { SecretsManager.shared.openClawHost }
static var openClawPort: Int { SecretsManager.shared.openClawPort }
static var openClawHookToken: String { SecretsManager.shared.openClawHookToken }
static var openClawGatewayToken: String { SecretsManager.shared.openClawGatewayToken }

static func websocketURL() -> URL? {
guard apiKey != "YOUR_GEMINI_API_KEY" && !apiKey.isEmpty else { return nil }
Expand Down
66 changes: 66 additions & 0 deletions samples/CameraAccess/CameraAccess/Gemini/SecretsManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// SecretsManager.swift
// CameraAccess
//
// Manages app secrets with device storage. Falls back to Secrets.swift defaults
// when no stored values exist. Values are persisted in UserDefaults.
//

import Combine
import Foundation

class SecretsManager: ObservableObject {
static let shared = SecretsManager()

private enum Keys {
static let geminiAPIKey = "secrets.geminiAPIKey"
static let openClawHost = "secrets.openClawHost"
static let openClawPort = "secrets.openClawPort"
static let openClawHookToken = "secrets.openClawHookToken"
static let openClawGatewayToken = "secrets.openClawGatewayToken"
static let hasStoredValues = "secrets.hasStoredValues"
}

private let defaults = UserDefaults.standard

@Published var geminiAPIKey: String
@Published var openClawHost: String
@Published var openClawPort: Int
@Published var openClawHookToken: String
@Published var openClawGatewayToken: String

private init() {
if defaults.bool(forKey: Keys.hasStoredValues) {
self.geminiAPIKey = defaults.string(forKey: Keys.geminiAPIKey) ?? Secrets.geminiAPIKey
self.openClawHost = defaults.string(forKey: Keys.openClawHost) ?? Secrets.openClawHost
self.openClawPort = defaults.object(forKey: Keys.openClawPort) as? Int ?? Secrets.openClawPort
self.openClawHookToken = defaults.string(forKey: Keys.openClawHookToken) ?? Secrets.openClawHookToken
self.openClawGatewayToken = defaults.string(forKey: Keys.openClawGatewayToken) ?? Secrets.openClawGatewayToken
} else {
self.geminiAPIKey = Secrets.geminiAPIKey
self.openClawHost = Secrets.openClawHost
self.openClawPort = Secrets.openClawPort
self.openClawHookToken = Secrets.openClawHookToken
self.openClawGatewayToken = Secrets.openClawGatewayToken
save()
}
}

func save() {
defaults.set(true, forKey: Keys.hasStoredValues)
defaults.set(geminiAPIKey, forKey: Keys.geminiAPIKey)
defaults.set(openClawHost, forKey: Keys.openClawHost)
defaults.set(openClawPort, forKey: Keys.openClawPort)
defaults.set(openClawHookToken, forKey: Keys.openClawHookToken)
defaults.set(openClawGatewayToken, forKey: Keys.openClawGatewayToken)
}

func resetToDefaults() {
geminiAPIKey = Secrets.geminiAPIKey
openClawHost = Secrets.openClawHost
openClawPort = Secrets.openClawPort
openClawHookToken = Secrets.openClawHookToken
openClawGatewayToken = Secrets.openClawGatewayToken
save()
}
}
2 changes: 2 additions & 0 deletions samples/CameraAccess/CameraAccess/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Needed to connect to Meta AI Glasses</string>
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to unlock your secrets instead of entering your PIN.</string>
<key>UISupportedExternalAccessoryProtocols</key>
<array>
<string>com.meta.ar.wearable</string>
Expand Down
17 changes: 17 additions & 0 deletions samples/CameraAccess/CameraAccess/Views/HomeScreenView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,26 @@ import SwiftUI

struct HomeScreenView: View {
@ObservedObject var viewModel: WearablesViewModel
@State private var showSettings = false

var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)

VStack(spacing: 12) {
HStack {
Spacer()
Button {
showSettings = true
} label: {
Image(systemName: "gearshape")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.black)
.frame(width: 24, height: 24)
}
}

Spacer()

Image(.cameraAccessIcon)
Expand Down Expand Up @@ -70,6 +84,9 @@ struct HomeScreenView: View {
}
.padding(.all, 24)
}
.sheet(isPresented: $showSettings) {
SettingsView(wearablesVM: viewModel)
}
}

}
Expand Down
Loading