Skip to content

Commit 097c04a

Browse files
crazytanclaude
andcommitted
Improve launch robustness: fix sandbox networking, update signing and bundle ID
- Add network.client, network.name, and network.server sandbox entitlements to fix DNS resolution blocked by app sandbox - Switch to automatic signing with Apple Development identity - Set app category to Productivity - Change bundle identifier from com.taskmenu to dev.crazytan.TaskMenu - Make keychain load/save log errors via os.log instead of silently swallowing with try?; on load failure, clear all tokens so the user gets a clean sign-in prompt - Add FailingKeychainService test stub and keychain error test - Move signing config (CODE_SIGN_STYLE, CODE_SIGN_IDENTITY) from Config.xcconfig into project.yml; only DEVELOPMENT_TEAM remains per-developer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9c13b43 commit 097c04a

File tree

11 files changed

+102
-36
lines changed

11 files changed

+102
-36
lines changed

Config.xcconfig.example

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
GOOGLE_CLIENT_ID = your-client-id.apps.googleusercontent.com
66
GOOGLE_CLIENT_SECRET = your-client-secret
77

8-
// Optional local signing overrides.
9-
// Keep these out of source control so different machines and CI can provide their own values.
8+
// Developer-specific signing. CODE_SIGN_STYLE and CODE_SIGN_IDENTITY are set
9+
// in project.yml (Automatic / Apple Development). Override here if needed.
1010
DEVELOPMENT_TEAM = YOUR_TEAM_ID
11-
CODE_SIGN_STYLE = Automatic
12-
CODE_SIGN_IDENTITY =

TaskMenu.xcodeproj/project.pbxproj

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
E53F1F56DBC73CF159E93516 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7270F80F12239828D873B133 /* SignInView.swift */; };
4444
E54169E513191306010635A7 /* AppIcon.svg in Resources */ = {isa = PBXBuildFile; fileRef = 8C8C52C19BDA6F8F2D3C638F /* AppIcon.svg */; };
4545
E7D0145136EE024CB87D7963 /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612D4C039654B488ADC17151 /* TaskListView.swift */; };
46+
F583510B7875405F768BADD3 /* TaskDetailViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C9B2D2EE1B7668B9245DBE /* TaskDetailViewTests.swift */; };
4647
FEBB3BC550B3CC63B620ED8F /* AppStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F3614C7335BB0200D28F074 /* AppStateTests.swift */; };
4748
FF5F630937F736E64BD3C1FD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D04BFBB6F10A824FCFD02C9 /* SettingsView.swift */; };
4849
/* End PBXBuildFile section */
@@ -72,6 +73,7 @@
7273
5AF69FFA11009E78484C090A /* TaskMenuApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskMenuApp.swift; sourceTree = "<group>"; };
7374
5D04BFBB6F10A824FCFD02C9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
7475
612D4C039654B488ADC17151 /* TaskListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListView.swift; sourceTree = "<group>"; };
76+
62C9B2D2EE1B7668B9245DBE /* TaskDetailViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDetailViewTests.swift; sourceTree = "<group>"; };
7577
633899C51990EF71D24F5C89 /* TaskList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskList.swift; sourceTree = "<group>"; };
7678
63E868E0B0520F6ABD40D350 /* GoogleAuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthService.swift; sourceTree = "<group>"; };
7779
68CD93FC69779BFB08340F08 /* TaskMenu.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TaskMenu.entitlements; sourceTree = "<group>"; };
@@ -208,6 +210,7 @@
208210
058C00FB539FD9B1D05BCBFD /* KeychainServiceTests.swift */,
209211
BB598B8D34C0A014D7826A63 /* MockURLProtocol.swift */,
210212
4AC00155161191D4DBBDA015 /* SearchFilterTests.swift */,
213+
62C9B2D2EE1B7668B9245DBE /* TaskDetailViewTests.swift */,
211214
6E4CD6808B2587AD2F8F9563 /* TaskItemModelTests.swift */,
212215
A2191A69341B04C760925B55 /* TaskListViewTests.swift */,
213216
9C0DB3189CC6045A5C0A230F /* TaskMenuAppTests.swift */,
@@ -275,12 +278,11 @@
275278
LastUpgradeCheck = 2640;
276279
TargetAttributes = {
277280
2499498786EF41DCFC6BE7F6 = {
278-
DevelopmentTeam = ZW5U6862Q8;
279-
ProvisioningStyle = Manual;
281+
DevelopmentTeam = V82M9YX8BR;
280282
};
281283
54F3465CB5E85F801680719A = {
282-
DevelopmentTeam = ZW5U6862Q8;
283-
ProvisioningStyle = Manual;
284+
DevelopmentTeam = V82M9YX8BR;
285+
ProvisioningStyle = Automatic;
284286
};
285287
};
286288
};
@@ -360,6 +362,7 @@
360362
3059000A80728B12B6989398 /* KeychainServiceTests.swift in Sources */,
361363
D321CE95DA3DC0D6090CD619 /* MockURLProtocol.swift in Sources */,
362364
8F3D1A4FC0600A0D85BA22E8 /* SearchFilterTests.swift in Sources */,
365+
F583510B7875405F768BADD3 /* TaskDetailViewTests.swift in Sources */,
363366
0FD718B29CF8183561A6F550 /* TaskItemModelTests.swift in Sources */,
364367
B1E9E90C4184BDD3DED97A2B /* TaskListViewTests.swift in Sources */,
365368
44D8CA5D6D124CF97A57EF1D /* TaskMenuAppTests.swift in Sources */,
@@ -383,16 +386,20 @@
383386
buildSettings = {
384387
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
385388
CODE_SIGN_ENTITLEMENTS = TaskMenu/Resources/TaskMenu.entitlements;
389+
CODE_SIGN_IDENTITY = "Apple Development";
390+
CODE_SIGN_STYLE = Automatic;
386391
COMBINE_HIDPI_IMAGES = YES;
387392
ENABLE_APP_SANDBOX = YES;
388393
ENABLE_HARDENED_RUNTIME = YES;
389394
INFOPLIST_FILE = TaskMenu/Resources/Info.plist;
395+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
390396
LD_RUNPATH_SEARCH_PATHS = (
391397
"$(inherited)",
392398
"@executable_path/../Frameworks",
393399
);
394-
PRODUCT_BUNDLE_IDENTIFIER = com.taskmenu.TaskMenu;
400+
PRODUCT_BUNDLE_IDENTIFIER = dev.crazytan.TaskMenu;
395401
PRODUCT_NAME = TaskMenu;
402+
PROVISIONING_PROFILE_SPECIFIER = "";
396403
SDKROOT = macosx;
397404
};
398405
name = Release;
@@ -478,7 +485,7 @@
478485
"@executable_path/../Frameworks",
479486
"@loader_path/../Frameworks",
480487
);
481-
PRODUCT_BUNDLE_IDENTIFIER = com.taskmenu.TaskMenuTests;
488+
PRODUCT_BUNDLE_IDENTIFIER = dev.crazytan.TaskMenuTests;
482489
SDKROOT = macosx;
483490
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TaskMenu.app/Contents/MacOS/TaskMenu";
484491
};
@@ -552,16 +559,20 @@
552559
buildSettings = {
553560
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
554561
CODE_SIGN_ENTITLEMENTS = TaskMenu/Resources/TaskMenu.entitlements;
562+
CODE_SIGN_IDENTITY = "Apple Development";
563+
CODE_SIGN_STYLE = Automatic;
555564
COMBINE_HIDPI_IMAGES = YES;
556565
ENABLE_APP_SANDBOX = YES;
557566
ENABLE_HARDENED_RUNTIME = YES;
558567
INFOPLIST_FILE = TaskMenu/Resources/Info.plist;
568+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
559569
LD_RUNPATH_SEARCH_PATHS = (
560570
"$(inherited)",
561571
"@executable_path/../Frameworks",
562572
);
563-
PRODUCT_BUNDLE_IDENTIFIER = com.taskmenu.TaskMenu;
573+
PRODUCT_BUNDLE_IDENTIFIER = dev.crazytan.TaskMenu;
564574
PRODUCT_NAME = TaskMenu;
575+
PROVISIONING_PROFILE_SPECIFIER = "";
565576
SDKROOT = macosx;
566577
};
567578
name = Debug;
@@ -577,7 +588,7 @@
577588
"@executable_path/../Frameworks",
578589
"@loader_path/../Frameworks",
579590
);
580-
PRODUCT_BUNDLE_IDENTIFIER = com.taskmenu.TaskMenuTests;
591+
PRODUCT_BUNDLE_IDENTIFIER = dev.crazytan.TaskMenuTests;
581592
SDKROOT = macosx;
582593
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TaskMenu.app/Contents/MacOS/TaskMenu";
583594
};

TaskMenu/Resources/TaskMenu.entitlements

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>com.apple.security.app-sandbox</key>
6-
<true/>
75
<key>com.apple.security.network.client</key>
86
<true/>
7+
<key>com.apple.security.network.name</key>
8+
<true/>
99
<key>com.apple.security.network.server</key>
1010
<true/>
1111
</dict>

TaskMenu/Services/GoogleAuthService.swift

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import AuthenticationServices
22
import CryptoKit
33
import Foundation
4+
import os.log
5+
6+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "TaskMenu", category: "Auth")
47

58
@MainActor
69
final class GoogleAuthService: Sendable {
@@ -138,22 +141,32 @@ final class GoogleAuthService: Sendable {
138141
}
139142

140143
private func saveTokens() {
141-
try? keychain.save(key: Constants.Keychain.accessTokenKey, string: accessToken ?? "")
142-
if let refreshToken {
143-
try? keychain.save(key: Constants.Keychain.refreshTokenKey, string: refreshToken)
144-
}
145-
if let expiration = tokenExpiration {
146-
let data = String(expiration.timeIntervalSince1970)
147-
try? keychain.save(key: Constants.Keychain.expirationKey, string: data)
144+
do {
145+
try keychain.save(key: Constants.Keychain.accessTokenKey, string: accessToken ?? "")
146+
if let refreshToken {
147+
try keychain.save(key: Constants.Keychain.refreshTokenKey, string: refreshToken)
148+
}
149+
if let expiration = tokenExpiration {
150+
try keychain.save(key: Constants.Keychain.expirationKey, string: String(expiration.timeIntervalSince1970))
151+
}
152+
} catch {
153+
logger.error("Failed to save tokens to keychain: \(error.localizedDescription)")
148154
}
149155
}
150156

151157
private func loadTokens() {
152-
accessToken = try? keychain.readString(key: Constants.Keychain.accessTokenKey)
153-
refreshToken = try? keychain.readString(key: Constants.Keychain.refreshTokenKey)
154-
if let expStr = try? keychain.readString(key: Constants.Keychain.expirationKey),
155-
let interval = Double(expStr) {
156-
tokenExpiration = Date(timeIntervalSince1970: interval)
158+
do {
159+
accessToken = try keychain.readString(key: Constants.Keychain.accessTokenKey)
160+
refreshToken = try keychain.readString(key: Constants.Keychain.refreshTokenKey)
161+
if let expStr = try keychain.readString(key: Constants.Keychain.expirationKey),
162+
let interval = Double(expStr) {
163+
tokenExpiration = Date(timeIntervalSince1970: interval)
164+
}
165+
} catch {
166+
logger.error("Failed to load tokens from keychain: \(error.localizedDescription)")
167+
accessToken = nil
168+
refreshToken = nil
169+
tokenExpiration = nil
157170
}
158171
}
159172

TaskMenu/Utilities/Constants.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ enum Constants {
2020
static let redirectHost = "127.0.0.1"
2121

2222
enum Keychain {
23-
static let service = "com.taskmenu.oauth"
23+
static let service = "dev.crazytan.TaskMenu.oauth"
2424
static let accessTokenKey = "access_token"
2525
static let refreshTokenKey = "refresh_token"
2626
static let expirationKey = "token_expiration"
@@ -31,6 +31,6 @@ enum Constants {
3131
}
3232

3333
enum Notifications {
34-
static let dueDateIdentifierPrefix = "com.taskmenu.dueDate"
34+
static let dueDateIdentifierPrefix = "dev.crazytan.TaskMenu.dueDate"
3535
}
3636
}

TaskMenuTests/AppStateBehaviorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class AppStateBehaviorTests: XCTestCase {
1616
MockURLProtocol.reset()
1717

1818
keychain = InMemoryKeychainService()
19-
userDefaultsSuiteName = "com.taskmenu.tests.appstate.behavior.\(UUID().uuidString)"
19+
userDefaultsSuiteName = "dev.crazytan.TaskMenu.tests.appstate.behavior.\(UUID().uuidString)"
2020
userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)
2121
userDefaults.removePersistentDomain(forName: userDefaultsSuiteName)
2222
dueDateNotificationService = TestDueDateNotificationService()

TaskMenuTests/AppStateTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ final class AppStateTests: XCTestCase {
1010

1111
override func setUp() async throws {
1212
keychain = InMemoryKeychainService()
13-
userDefaultsSuiteName = "com.taskmenu.tests.appstate.\(UUID().uuidString)"
13+
userDefaultsSuiteName = "dev.crazytan.TaskMenu.tests.appstate.\(UUID().uuidString)"
1414
userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)
1515
userDefaults.removePersistentDomain(forName: userDefaultsSuiteName)
1616
dueDateNotificationService = TestDueDateNotificationService()

TaskMenuTests/GoogleAuthServiceTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ final class GoogleAuthServiceTests: XCTestCase {
7474
XCTAssertNil(auth.tokenExpiration)
7575
}
7676

77+
func testLoadTokensClearsAllOnKeychainError() {
78+
let failingKeychain = FailingKeychainService()
79+
let auth = GoogleAuthService(keychain: failingKeychain)
80+
XCTAssertNil(auth.accessToken)
81+
XCTAssertNil(auth.refreshToken)
82+
XCTAssertNil(auth.tokenExpiration)
83+
XCTAssertFalse(auth.isSignedIn)
84+
}
85+
7786
// MARK: - Sign Out
7887

7988
func testSignOutClearsTokens() throws {

TaskMenuTests/InMemoryKeychainService.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
@testable import TaskMenu
22
import Foundation
33

4+
/// Keychain stub that always throws — for testing error-handling paths.
5+
final class FailingKeychainService: KeychainServiceProtocol, @unchecked Sendable {
6+
func save(key: String, data: Data) throws {
7+
throw KeychainError.saveFailed(-1)
8+
}
9+
10+
func save(key: String, string: String) throws {
11+
throw KeychainError.saveFailed(-1)
12+
}
13+
14+
func read(key: String) throws -> Data? {
15+
throw KeychainError.readFailed(-1)
16+
}
17+
18+
func readString(key: String) throws -> String? {
19+
throw KeychainError.readFailed(-1)
20+
}
21+
22+
func delete(key: String) throws {
23+
throw KeychainError.deleteFailed(-1)
24+
}
25+
26+
func deleteAll() throws {
27+
throw KeychainError.deleteFailed(-1)
28+
}
29+
}
30+
431
/// In-memory keychain replacement for tests — avoids macOS Keychain prompts.
532
final class InMemoryKeychainService: KeychainServiceProtocol, @unchecked Sendable {
633
private var storage: [String: Data] = [:]

TaskMenuTests/KeychainServiceTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ final class KeychainServiceTests: XCTestCase {
6060

6161
func testProductionKeychainUsesInMemoryStoreUnderXCTest() throws {
6262
let keychain = KeychainService(
63-
service: "com.taskmenu.test.production.\(UUID().uuidString)",
63+
service: "dev.crazytan.TaskMenu.test.production.\(UUID().uuidString)",
6464
environment: testEnvironment
6565
)
6666

@@ -71,11 +71,11 @@ final class KeychainServiceTests: XCTestCase {
7171

7272
func testProductionKeychainInMemoryStoreIsScopedByService() throws {
7373
let keychainA = KeychainService(
74-
service: "com.taskmenu.test.production.a.\(UUID().uuidString)",
74+
service: "dev.crazytan.TaskMenu.test.production.a.\(UUID().uuidString)",
7575
environment: testEnvironment
7676
)
7777
let keychainB = KeychainService(
78-
service: "com.taskmenu.test.production.b.\(UUID().uuidString)",
78+
service: "dev.crazytan.TaskMenu.test.production.b.\(UUID().uuidString)",
7979
environment: testEnvironment
8080
)
8181

0 commit comments

Comments
 (0)