refactor(ios): migrate Cloud/ files to ByteLystPlatformSDK — eliminate duplicated platform code

Replaced standalone implementations with thin wrappers over shared SDK:
- KeychainHelper → delegates to BLKeychain (53→27 lines)
- TelemetryService → delegates to BLTelemetryClient (139→55 lines)
- FeatureFlagService → delegates to BLFeatureFlagClient (72→39 lines)
- AuthService → delegates to BLAuthClient (359→171 lines)
- project.yml → added ByteLystPlatformSDK local package dependency

Total: 623 lines of duplicated code → 292 lines of thin wrappers.
PlatformSyncManager kept as-is (product-specific timer DTOs).
All existing call-site APIs preserved — zero breaking changes.
This commit is contained in:
saravanakumardb1 2026-02-28 22:12:37 -08:00
parent 3596a8f350
commit d1b4534b22
5 changed files with 112 additions and 436 deletions

View File

@ -1,50 +1,13 @@
// Auth Service // Auth Service
// Login, register, refresh, logout via platform-service /auth/* endpoints. // Thin wrapper over ByteLystPlatformSDK's BLAuthClient.
// Stores tokens in Keychain; wires into PlatformSyncManager. // Keeps existing call-site API + ChronoMind-specific sync wiring.
// AuthUser = BLAuthUser (re-exported from SDK).
import Foundation import Foundation
import SwiftUI import SwiftUI
import ByteLystPlatformSDK
struct AuthUser: Codable { typealias AuthUser = BLAuthUser
let id: String
let email: String
let name: String
let plan: String
let role: String
enum CodingKeys: String, CodingKey {
case id, email, plan, role
case name = "displayName"
}
init(id: String, email: String, name: String, plan: String, role: String = "user") {
self.id = id
self.email = email
self.name = name
self.plan = plan
self.role = role
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id)
email = try c.decode(String.self, forKey: .email)
name = try c.decode(String.self, forKey: .name)
plan = try c.decodeIfPresent(String.self, forKey: .plan) ?? "free"
role = try c.decodeIfPresent(String.self, forKey: .role) ?? "user"
}
}
private struct TokenResponse: Codable {
let accessToken: String
let refreshToken: String
let user: AuthUser
}
private struct RefreshResponse: Codable {
let accessToken: String
let refreshToken: String
}
enum CMAuthState { enum CMAuthState {
case loading case loading
@ -62,42 +25,35 @@ final class CMAuthService: ObservableObject {
@AppStorage("cm_user_name") private var userName = "" @AppStorage("cm_user_name") private var userName = ""
@AppStorage("cm_user_plan") private var userPlan = "free" @AppStorage("cm_user_plan") private var userPlan = "free"
private var accessToken: String { private let authClient: BLAuthClient
get { KeychainHelper.read(key: "cm_access_token") ?? "" } private let platformClient: BLPlatformClient
set {
if newValue.isEmpty { KeychainHelper.delete(key: "cm_access_token") }
else { KeychainHelper.save(key: "cm_access_token", value: newValue) }
}
}
private var refreshToken: String {
get { KeychainHelper.read(key: "cm_refresh_token") ?? "" }
set {
if newValue.isEmpty { KeychainHelper.delete(key: "cm_refresh_token") }
else { KeychainHelper.save(key: "cm_refresh_token", value: newValue) }
}
}
private var refreshTimer: Timer?
private var baseURL: String {
Bundle.main.object(forInfoDictionaryKey: "PLATFORM_SERVICE_URL") as? String
?? "https://api.chronomind.app"
}
private let productId = "chronomind"
private init() { private init() {
let config = BLPlatformConfig.fromInfoPlist(
productId: "chronomind",
defaultBaseURL: "https://api.chronomind.app",
bundleId: "com.saravana.chronomind",
appGroupId: "group.com.chronomind.shared"
)
platformClient = BLPlatformClient(config: config)
authClient = BLAuthClient(config: config, client: platformClient)
// Wire token updates sync manager
authClient.onTokensUpdated = { [weak self] token in
Task { @MainActor in
self?.wireSyncToken(token)
}
}
checkExistingSession() checkExistingSession()
} }
private func checkExistingSession() { private func checkExistingSession() {
if !accessToken.isEmpty, !userEmail.isEmpty { if authClient.isAuthenticated, !userEmail.isEmpty {
state = .loggedIn(AuthUser(id: "", email: userEmail, name: userName, plan: userPlan)) state = .loggedIn(AuthUser(id: "", email: userEmail, displayName: userName, plan: userPlan))
wireSyncToken() wireSyncToken(authClient.accessToken)
startRefreshTimer()
Task { await fetchCurrentUser() } Task { await fetchCurrentUser() }
} else if !accessToken.isEmpty { } else if authClient.isAuthenticated {
Task { await fetchCurrentUser() } Task { await fetchCurrentUser() }
} else { } else {
state = .loggedOut state = .loggedOut
@ -108,62 +64,36 @@ final class CMAuthService: ObservableObject {
func login(email: String, password: String) async { func login(email: String, password: String) async {
state = .loading state = .loading
let body: [String: String] = [ do {
"email": email, let user = try await authClient.login(email: email, password: password)
"password": password, saveUserInfo(user)
"productId": productId, state = .loggedIn(user)
] } catch {
guard let result = await postAuth(path: "/auth/login", body: body) else {
state = .error("Invalid email or password") state = .error("Invalid email or password")
return
} }
saveSession(result)
} }
func register(name: String, email: String, password: String) async { func register(name: String, email: String, password: String) async {
state = .loading state = .loading
let body: [String: String] = [ do {
"email": email, let user = try await authClient.register(displayName: name, email: email, password: password)
"displayName": name, saveUserInfo(user)
"password": password, state = .loggedIn(user)
"productId": productId, } catch {
]
guard let result = await postAuth(path: "/auth/register", body: body) else {
state = .error("Registration failed") state = .error("Registration failed")
return
} }
saveSession(result)
} }
func logout() { func logout() {
stopRefreshTimer() authClient.logout()
accessToken = ""
refreshToken = ""
userEmail = "" userEmail = ""
userName = "" userName = ""
state = .loggedOut state = .loggedOut
PlatformSyncManager.shared.setAuthToken(nil)
} }
func forgotPassword(email: String) async -> String? { func forgotPassword(email: String) async -> String? {
guard let url = URL(string: "\(baseURL)/auth/forgot-password"),
let jsonData = try? JSONSerialization.data(withJSONObject: [
"email": email, "productId": productId
]) else { return "Invalid request" }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
request.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-Id")
request.httpBody = jsonData
request.timeoutInterval = 15
do { do {
let (_, response) = try await URLSession.shared.data(for: request) try await authClient.forgotPassword(email: email)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
return "Failed to send reset email"
}
return nil return nil
} catch { } catch {
return error.localizedDescription return error.localizedDescription
@ -171,28 +101,8 @@ final class CMAuthService: ObservableObject {
} }
func changePassword(currentPassword: String, newPassword: String) async -> String? { func changePassword(currentPassword: String, newPassword: String) async -> String? {
guard !accessToken.isEmpty,
let url = URL(string: "\(baseURL)/auth/change-password"),
let jsonData = try? JSONSerialization.data(withJSONObject: [
"currentPassword": currentPassword, "newPassword": newPassword
]) else { return "Not authenticated" }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
request.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-Id")
request.httpBody = jsonData
request.timeoutInterval = 15
do { do {
let (data, response) = try await URLSession.shared.data(for: request) try await authClient.changePassword(currentPassword: currentPassword, newPassword: newPassword)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
if let body = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let msg = body["message"] as? String { return msg }
return "Failed to change password"
}
return nil return nil
} catch { } catch {
return error.localizedDescription return error.localizedDescription
@ -200,29 +110,11 @@ final class CMAuthService: ObservableObject {
} }
func deleteAccount(password: String) async -> String? { func deleteAccount(password: String) async -> String? {
guard !accessToken.isEmpty,
let url = URL(string: "\(baseURL)/auth/account"),
let jsonData = try? JSONSerialization.data(withJSONObject: [
"password": password
]) else { return "Not authenticated" }
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
request.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-Id")
request.httpBody = jsonData
request.timeoutInterval = 15
do { do {
let (data, response) = try await URLSession.shared.data(for: request) try await authClient.deleteAccount(password: password)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { userEmail = ""
if let body = try? JSONSerialization.jsonObject(with: data) as? [String: Any], userName = ""
let msg = body["message"] as? String { return msg } state = .loggedOut
return "Failed to delete account"
}
logout()
return nil return nil
} catch { } catch {
return error.localizedDescription return error.localizedDescription
@ -240,119 +132,39 @@ final class CMAuthService: ObservableObject {
} }
func getAccessToken() -> String? { func getAccessToken() -> String? {
let t = accessToken authClient.accessToken
return t.isEmpty ? nil : t
} }
// MARK: - Token Refresh // MARK: - Token Refresh
func refreshAccessToken() async -> Bool { func refreshAccessToken() async -> Bool {
guard !refreshToken.isEmpty else { return false } let ok = await authClient.refreshAccessToken()
let body: [String: String] = ["refreshToken": refreshToken] if !ok { logout() }
guard let url = URL(string: "\(baseURL)/auth/refresh"), return ok
let jsonData = try? JSONSerialization.data(withJSONObject: body) else { return false } }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
request.httpBody = jsonData
request.timeoutInterval = 10
func fetchCurrentUser() async {
do { do {
let (data, response) = try await URLSession.shared.data(for: request) let user = try await authClient.getMe()
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { saveUserInfo(user)
if let http = response as? HTTPURLResponse, http.statusCode == 401 { logout() } state = .loggedIn(user)
return false
}
let r = try JSONDecoder().decode(RefreshResponse.self, from: data)
accessToken = r.accessToken
refreshToken = r.refreshToken
wireSyncToken()
return true
} catch { } catch {
return false if let netErr = error as? BLNetworkError, netErr.statusCode == 401 {
let ok = await refreshAccessToken()
if ok { await fetchCurrentUser() } else { state = .loggedOut }
}
} }
} }
private func startRefreshTimer() {
stopRefreshTimer()
refreshTimer = Timer.scheduledTimer(withTimeInterval: 45 * 60, repeats: true) { [weak self] _ in
guard let self else { return }
Task { @MainActor in _ = await self.refreshAccessToken() }
}
}
private func stopRefreshTimer() {
refreshTimer?.invalidate()
refreshTimer = nil
}
// MARK: - Helpers // MARK: - Helpers
private func wireSyncToken() { private func wireSyncToken(_ token: String?) {
PlatformSyncManager.shared.setAuthToken(accessToken.isEmpty ? nil : accessToken) PlatformSyncManager.shared.setAuthToken(token)
} }
private func postAuth(path: String, body: [String: String]) async -> TokenResponse? { private func saveUserInfo(_ user: AuthUser) {
guard let url = URL(string: "\(baseURL)\(path)"), userEmail = user.email
let jsonData = try? JSONSerialization.data(withJSONObject: body) else { return nil } userName = user.displayName
userPlan = user.plan
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
request.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-Id")
request.httpBody = jsonData
request.timeoutInterval = 15
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, (200...201).contains(http.statusCode) else { return nil }
return try JSONDecoder().decode(TokenResponse.self, from: data)
} catch {
return nil
}
}
private func saveSession(_ resp: TokenResponse) {
accessToken = resp.accessToken
refreshToken = resp.refreshToken
userEmail = resp.user.email
userName = resp.user.name
userPlan = resp.user.plan
state = .loggedIn(resp.user)
wireSyncToken()
startRefreshTimer()
}
func fetchCurrentUser() async {
guard !accessToken.isEmpty, let url = URL(string: "\(baseURL)/auth/me") else { return }
var request = URLRequest(url: url)
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
request.timeoutInterval = 10
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
if let http = response as? HTTPURLResponse, http.statusCode == 401 {
let ok = await refreshAccessToken()
if ok { await fetchCurrentUser() } else { state = .loggedOut }
}
return
}
struct MeResponse: Codable {
let id: String; let email: String; let displayName: String
let plan: String?; let role: String?
}
let info = try JSONDecoder().decode(MeResponse.self, from: data)
let plan = info.plan ?? "free"
userPlan = plan; userEmail = info.email; userName = info.displayName
state = .loggedIn(AuthUser(id: info.id, email: info.email, name: info.displayName, plan: plan, role: info.role ?? "user"))
wireSyncToken()
} catch {
// Keep existing state
}
} }
} }

View File

@ -1,71 +1,38 @@
// Feature Flag Client // Feature Flag Client
// Polls platform-service /flags/poll for feature flags. // Thin wrapper over ByteLystPlatformSDK's BLFeatureFlagClient.
// Flags cached in memory, re-polled every 5 minutes. // Keeps existing call-site API (FeatureFlagService.shared.isEnabled).
// Consumers call FeatureFlagService.shared.isEnabled("flag_key").
import Foundation import Foundation
import ByteLystPlatformSDK
@MainActor @MainActor
final class FeatureFlagService: ObservableObject { final class FeatureFlagService: ObservableObject {
static let shared = FeatureFlagService() static let shared = FeatureFlagService()
@Published private(set) var flags: [String: Bool] = [:] @Published private(set) var flags: [String: Bool] = [:]
private var pollTimer: Timer? private let flagClient: BLFeatureFlagClient
private let pollIntervalSec: TimeInterval = 5 * 60 // 5 minutes
private let productId = "chronomind"
private var baseURL: String { private init() {
Bundle.main.object(forInfoDictionaryKey: "PLATFORM_SERVICE_URL") as? String let config = BLPlatformConfig.fromInfoPlist(
?? "https://api.chronomind.app" productId: "chronomind",
defaultBaseURL: "https://api.chronomind.app",
bundleId: "com.saravana.chronomind"
)
let client = BLPlatformClient(config: config)
flagClient = BLFeatureFlagClient(config: config, client: client)
} }
private init() {}
// MARK: - Public API // MARK: - Public API
func start(userId: String? = nil) { func start(userId: String? = nil) {
Task { await fetchFlags(userId: userId) } flagClient.start(userId: userId)
pollTimer?.invalidate()
pollTimer = Timer.scheduledTimer(withTimeInterval: pollIntervalSec, repeats: true) { [weak self] _ in
guard let self else { return }
Task { @MainActor in await self.fetchFlags(userId: userId) }
}
} }
func stop() { func stop() {
pollTimer?.invalidate() flagClient.stop()
pollTimer = nil
} }
func isEnabled(_ key: String) -> Bool { func isEnabled(_ key: String) -> Bool {
flags[key] == true flagClient.isEnabled(key)
}
// MARK: - Fetch
private func fetchFlags(userId: String? = nil) async {
var components = URLComponents(string: "\(baseURL)/api/flags/poll")
var queryItems: [URLQueryItem] = [.init(name: "platform", value: "ios")]
if let userId { queryItems.append(.init(name: "userId", value: userId)) }
components?.queryItems = queryItems
guard let url = components?.url else { return }
var request = URLRequest(url: url)
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
request.timeoutInterval = 10
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return }
struct FlagsResponse: Codable {
let flags: [String: Bool]
}
let parsed = try JSONDecoder().decode(FlagsResponse.self, from: data)
flags = parsed.flags
} catch {
// Keep existing flags on error
}
} }
} }

View File

@ -1,8 +1,10 @@
// Keychain Helper // Keychain Helper
// Lightweight wrapper for storing auth tokens securely in iOS Keychain. // Thin wrapper over ByteLystPlatformSDK's BLKeychain.
// Keeps the existing call-site API (KeychainHelper.save/read/delete)
// while delegating to the shared implementation.
import Foundation import Foundation
import Security import ByteLystPlatformSDK
enum KeychainHelper { enum KeychainHelper {
@ -10,43 +12,15 @@ enum KeychainHelper {
@discardableResult @discardableResult
static func save(key: String, value: String) -> Bool { static func save(key: String, value: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false } BLKeychain.save(service: service, key: key, value: value)
delete(key: key)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
]
return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
} }
static func read(key: String) -> String? { static func read(key: String) -> String? {
let query: [String: Any] = [ BLKeychain.read(service: service, key: key)
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
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 else { return nil }
return String(data: data, encoding: .utf8)
} }
@discardableResult @discardableResult
static func delete(key: String) -> Bool { static func delete(key: String) -> Bool {
let query: [String: Any] = [ BLKeychain.delete(service: service, key: key)
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
} }
} }

View File

@ -1,76 +1,32 @@
// Platform Telemetry Client // Platform Telemetry Client
// Sends events to platform-service /telemetry/events endpoint. // Thin wrapper over ByteLystPlatformSDK's BLTelemetryClient.
// Keeps existing call-site API (CMTelemetryService.shared.trackEvent/trackTimer/trackScreen).
// Privacy: no PII, only action names + timing metrics. // Privacy: no PII, only action names + timing metrics.
// Fire-and-forget errors never surface to the user.
import Foundation import Foundation
import ByteLystPlatformSDK
@MainActor @MainActor
final class CMTelemetryService { final class CMTelemetryService {
static let shared = CMTelemetryService() static let shared = CMTelemetryService()
private let productId = "chronomind" private let telemetry: BLTelemetryClient
private let platform = "ios"
private let channel = "native"
private var queue: [TelemetryPayload] = []
private var flushTimer: Timer?
private let maxQueue = 50
private let flushIntervalSec: TimeInterval = 30
private var installId: String { private init() {
let key = "chronomind-telemetry-install-id" let config = BLPlatformConfig.fromInfoPlist(
if let id = UserDefaults.standard.string(forKey: key) { return id } productId: "chronomind",
let id = UUID().uuidString defaultBaseURL: "https://api.chronomind.app",
UserDefaults.standard.set(id, forKey: key) bundleId: "com.saravana.chronomind",
return id appGroupId: "group.com.chronomind.shared"
)
let client = BLPlatformClient(config: config)
telemetry = BLTelemetryClient(config: config, client: client)
} }
private let sessionId = UUID().uuidString
private var baseURL: String {
Bundle.main.object(forInfoDictionaryKey: "PLATFORM_SERVICE_URL") as? String
?? "https://api.chronomind.app"
}
private struct TelemetryPayload: Codable {
let id: String
let productId: String
let anonymousInstallId: String
let sessionId: String
let platform: String
let channel: String
let osFamily: String
let osVersion: String
let appVersion: String
let buildNumber: String
let releaseChannel: String
let eventType: String
let module: String
let eventName: String
let feature: String?
let message: String?
let tags: [String: String]?
let metrics: [String: Double]?
let occurredAt: String
}
private init() {}
// MARK: - Public API // MARK: - Public API
func start() { func start() { telemetry.start() }
guard flushTimer == nil else { return } func stop() { telemetry.stop() }
flushTimer = Timer.scheduledTimer(withTimeInterval: flushIntervalSec, repeats: true) { [weak self] _ in
guard let self else { return }
Task { @MainActor in self.flush() }
}
}
func stop() {
flush()
flushTimer?.invalidate()
flushTimer = nil
}
func trackEvent( func trackEvent(
_ eventType: String, _ eventType: String,
@ -81,32 +37,9 @@ final class CMTelemetryService {
tags: [String: String]? = nil, tags: [String: String]? = nil,
metrics: [String: Double]? = nil metrics: [String: Double]? = nil
) { ) {
let event = TelemetryPayload( telemetry.trackEvent(eventType, module: module, name: name,
id: UUID().uuidString, feature: feature, message: message,
productId: productId, tags: tags, metrics: metrics)
anonymousInstallId: installId,
sessionId: sessionId,
platform: platform,
channel: channel,
osFamily: "ios",
osVersion: ProcessInfo.processInfo.operatingSystemVersionString,
appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.1.0",
buildNumber: Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1",
releaseChannel: "beta",
eventType: eventType,
module: module,
eventName: name,
feature: feature,
message: message,
tags: tags,
metrics: metrics,
occurredAt: ISO8601DateFormatter().string(from: Date())
)
queue.append(event)
if queue.count >= maxQueue {
flush()
}
} }
func trackTimer(_ name: String, tags: [String: String]? = nil, metrics: [String: Double]? = nil) { func trackTimer(_ name: String, tags: [String: String]? = nil, metrics: [String: Double]? = nil) {
@ -114,25 +47,8 @@ final class CMTelemetryService {
} }
func trackScreen(_ screen: String) { func trackScreen(_ screen: String) {
trackEvent("info", module: "navigation", name: "screen_view", tags: ["screen": screen]) telemetry.trackScreen(screen)
} }
func flush() { func flush() { telemetry.flush() }
guard !queue.isEmpty else { return }
let batch = queue
queue.removeAll()
Task.detached { [baseURL, productId] in
guard let url = URL(string: "\(baseURL)/telemetry/events") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
request.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-Id")
request.httpBody = try? JSONEncoder().encode(["events": batch])
request.timeoutInterval = 10
_ = try? await URLSession.shared.data(for: request)
}
}
} }

View File

@ -10,6 +10,10 @@ options:
generateEmptyDirectories: true generateEmptyDirectories: true
groupSortPosition: top groupSortPosition: top
packages:
ByteLystPlatformSDK:
path: ../../learning_ai_common_plat/packages/swift-platform-sdk
settings: settings:
base: base:
SWIFT_VERSION: "5.9" SWIFT_VERSION: "5.9"
@ -45,6 +49,7 @@ targets:
dependencies: dependencies:
- target: ChronoMindWidgets - target: ChronoMindWidgets
- target: ChronoMindWatch - target: ChronoMindWatch
- package: ByteLystPlatformSDK
entitlements: entitlements:
path: ChronoMind/ChronoMind.entitlements path: ChronoMind/ChronoMind.entitlements
properties: properties:
@ -159,6 +164,8 @@ targets:
- path: ChronoMind/Shared/Reschedule - path: ChronoMind/Shared/Reschedule
- path: ChronoMind/Shared/Gamification - path: ChronoMind/Shared/Gamification
- path: ChronoMind/Shared/Cloud - path: ChronoMind/Shared/Cloud
dependencies:
- package: ByteLystPlatformSDK
settings: settings:
base: base:
PRODUCT_BUNDLE_IDENTIFIER: com.chronomind.mac PRODUCT_BUNDLE_IDENTIFIER: com.chronomind.mac