Skip to content

Commit 89a0a6f

Browse files
minor updates
1 parent b7b68ca commit 89a0a6f

File tree

7 files changed

+180
-75
lines changed

7 files changed

+180
-75
lines changed

Playground/YapRun/YapRun/ContentView.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,10 @@ struct ContentView: View {
9595

9696
// Keyboard
9797
statusCard(
98-
icon: viewModel.keyboardReady ? "checkmark.circle.fill" : "keyboard",
98+
icon: keyboardStatusIcon,
9999
title: "Keyboard",
100-
subtitle: viewModel.keyboardReady
101-
? "Installed with Full Access"
102-
: "Settings → General → Keyboard → Add YapRun",
103-
color: viewModel.keyboardReady ? AppColors.primaryGreen : .orange,
100+
subtitle: keyboardStatusSubtitle,
101+
color: keyboardStatusColor,
104102
actionLabel: viewModel.keyboardReady ? nil : "Setup",
105103
action: viewModel.keyboardReady ? nil : { viewModel.openSettings() }
106104
)
@@ -123,6 +121,30 @@ struct ContentView: View {
123121
}
124122
}
125123

124+
private var keyboardStatusIcon: String {
125+
if viewModel.keyboardReady {
126+
return "checkmark.circle.fill"
127+
} else if viewModel.keyboardEnabled {
128+
return "lock.open"
129+
} else {
130+
return "keyboard"
131+
}
132+
}
133+
134+
private var keyboardStatusSubtitle: String {
135+
if viewModel.keyboardReady {
136+
return "Installed with Full Access"
137+
} else if viewModel.keyboardEnabled {
138+
return "Full Access required — enable in Settings"
139+
} else {
140+
return "Add keyboard in Settings → General → Keyboard"
141+
}
142+
}
143+
144+
private var keyboardStatusColor: Color {
145+
viewModel.keyboardReady ? AppColors.primaryGreen : .orange
146+
}
147+
126148
private func statusCard(
127149
icon: String,
128150
title: String,

Playground/YapRun/YapRun/Features/Home/HomeViewModel.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ final class HomeViewModel {
3434
var errorMessage: String?
3535

3636
#if os(iOS)
37-
var keyboardReady = false
37+
var keyboardEnabled = false
38+
var keyboardFullAccess = false
39+
var keyboardReady: Bool { keyboardEnabled && keyboardFullAccess }
3840
#elseif os(macOS)
3941
var accessibilityGranted = false
4042
#endif
@@ -65,8 +67,17 @@ final class HomeViewModel {
6567

6668
// Platform-specific status
6769
#if os(iOS)
68-
let hasSessionState = SharedDataBridge.shared.defaults?.string(forKey: SharedConstants.Keys.sessionState) != nil
69-
keyboardReady = hasSessionState
70+
let keyboards = UserDefaults.standard.object(forKey: "AppleKeyboards") as? [String] ?? []
71+
keyboardEnabled = keyboards.contains(SharedConstants.keyboardExtensionBundleId)
72+
73+
if keyboardEnabled {
74+
SharedDataBridge.shared.defaults?.synchronize()
75+
keyboardFullAccess = SharedDataBridge.shared.defaults?.bool(
76+
forKey: SharedConstants.Keys.keyboardFullAccessGranted
77+
) ?? false
78+
} else {
79+
keyboardFullAccess = false
80+
}
7081
#elseif os(macOS)
7182
accessibilityGranted = AXIsProcessTrusted()
7283
#endif

Playground/YapRun/YapRun/Features/Onboarding/OnboardingViewModel.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ final class OnboardingViewModel {
2929

3030
var currentStep: Step = .welcome
3131
var micGranted = false
32-
var keyboardReady = false
32+
var keyboardEnabled = false
33+
var keyboardFullAccess = false
34+
var keyboardReady: Bool { keyboardEnabled && keyboardFullAccess }
3335
var downloadProgress: Double = 0
3436
var downloadStage: String = ""
3537
var isDownloading = false
@@ -64,14 +66,20 @@ final class OnboardingViewModel {
6466
}
6567

6668
func checkKeyboardStatus() {
67-
// If the keyboard extension has written anything to App Group UserDefaults,
68-
// it means the keyboard is installed AND Full Access is enabled.
69-
// The simplest proxy: SharedDataBridge.shared.defaults is non-nil
70-
// (App Group container is accessible).
71-
// A stronger signal: check if the keyboard has ever written sessionState.
72-
let defaults = SharedDataBridge.shared.defaults
73-
let hasSessionState = defaults?.string(forKey: SharedConstants.Keys.sessionState) != nil
74-
keyboardReady = hasSessionState
69+
// 1. Check if the keyboard is in the system's enabled keyboard list
70+
let keyboards = UserDefaults.standard.object(forKey: "AppleKeyboards") as? [String] ?? []
71+
keyboardEnabled = keyboards.contains(SharedConstants.keyboardExtensionBundleId)
72+
73+
// 2. Check full-access proxy flag written by the keyboard extension on each viewDidLoad.
74+
// If the keyboard isn't even enabled, full access is implicitly false.
75+
if keyboardEnabled {
76+
SharedDataBridge.shared.defaults?.synchronize()
77+
keyboardFullAccess = SharedDataBridge.shared.defaults?.bool(
78+
forKey: SharedConstants.Keys.keyboardFullAccessGranted
79+
) ?? false
80+
} else {
81+
keyboardFullAccess = false
82+
}
7583
}
7684

7785
// MARK: - Microphone

Playground/YapRun/YapRun/Features/Onboarding/Steps/KeyboardSetupStepView.swift

Lines changed: 87 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,29 @@ import SwiftUI
1212
struct KeyboardSetupStepView: View {
1313
let viewModel: OnboardingViewModel
1414

15-
private let steps: [(icon: String, title: String, detail: String)] = [
16-
("gear", "Open Settings", "Tap the button below to open Settings."),
17-
("keyboard", "Add YapRun Keyboard", "General → Keyboard → Keyboards → Add New Keyboard → YapRun."),
18-
("lock.open", "Grant Full Access", "Tap YapRun → enable 'Allow Full Access' for mic and App Group IPC.")
19-
]
15+
private var headerColor: Color {
16+
viewModel.keyboardReady ? AppColors.primaryGreen : AppColors.ctaOrange
17+
}
18+
19+
private var headerTitle: String {
20+
if viewModel.keyboardReady {
21+
return "Keyboard Ready!"
22+
} else if viewModel.keyboardEnabled {
23+
return "Almost There"
24+
} else {
25+
return "Add the Keyboard"
26+
}
27+
}
28+
29+
private var headerSubtitle: String {
30+
if viewModel.keyboardReady {
31+
return "YapRun keyboard is installed with Full Access."
32+
} else if viewModel.keyboardEnabled {
33+
return "Enable Full Access so YapRun can use the microphone."
34+
} else {
35+
return "Two quick steps to start dictating anywhere."
36+
}
37+
}
2038

2139
var body: some View {
2240
VStack(spacing: 0) {
@@ -25,58 +43,49 @@ struct KeyboardSetupStepView: View {
2543
// Header icon
2644
ZStack {
2745
Circle()
28-
.fill((viewModel.keyboardReady ? AppColors.primaryGreen : AppColors.ctaOrange).opacity(0.15))
46+
.fill(headerColor.opacity(0.15))
2947
.frame(width: 100, height: 100)
3048
Image(systemName: viewModel.keyboardReady ? "checkmark.circle.fill" : "keyboard.badge.ellipsis")
3149
.font(.system(size: 48, weight: .medium))
32-
.foregroundStyle(viewModel.keyboardReady ? AppColors.primaryGreen : AppColors.ctaOrange)
50+
.foregroundStyle(headerColor)
3351
.contentTransition(.symbolEffect(.replace))
3452
}
3553
.padding(.bottom, 24)
3654

37-
Text(viewModel.keyboardReady ? "Keyboard Ready!" : "Add the Keyboard")
55+
Text(headerTitle)
3856
.font(.system(size: 28, weight: .bold))
3957
.foregroundStyle(AppColors.textPrimary)
4058
.padding(.bottom, 8)
4159

42-
Text(viewModel.keyboardReady
43-
? "YapRun keyboard is installed with Full Access."
44-
: "Three quick steps to start dictating anywhere.")
60+
Text(headerSubtitle)
4561
.font(.subheadline)
4662
.foregroundStyle(AppColors.textSecondary)
4763
.multilineTextAlignment(.center)
4864
.padding(.horizontal, 40)
4965
.padding(.bottom, 32)
5066

5167
if !viewModel.keyboardReady {
52-
// Steps card
68+
// Granular steps card with live checkmarks
5369
VStack(spacing: 0) {
54-
ForEach(Array(steps.enumerated()), id: \.offset) { index, step in
55-
HStack(alignment: .top, spacing: 14) {
56-
Text("\(index + 1)")
57-
.font(.caption.weight(.bold))
58-
.foregroundStyle(.black)
59-
.frame(width: 26, height: 26)
60-
.background(AppColors.ctaOrange, in: Circle())
61-
62-
VStack(alignment: .leading, spacing: 4) {
63-
Text(step.title)
64-
.font(.subheadline.weight(.semibold))
65-
.foregroundStyle(AppColors.textPrimary)
66-
Text(step.detail)
67-
.font(.caption)
68-
.foregroundStyle(AppColors.textTertiary)
69-
}
70-
71-
Spacer()
72-
}
73-
.padding(.vertical, 12)
74-
75-
if index < steps.count - 1 {
76-
Divider()
77-
.background(AppColors.cardBorder)
78-
}
79-
}
70+
// Step 1: Add Keyboard
71+
stepRow(
72+
number: 1,
73+
icon: "keyboard",
74+
title: "Add YapRun Keyboard",
75+
detail: "Settings → General → Keyboard → Keyboards → Add New Keyboard → YapRun.",
76+
isComplete: viewModel.keyboardEnabled
77+
)
78+
79+
Divider().background(AppColors.cardBorder)
80+
81+
// Step 2: Enable Full Access
82+
stepRow(
83+
number: 2,
84+
icon: "lock.open",
85+
title: "Enable Full Access",
86+
detail: "Settings → General → Keyboard → Keyboards → YapRun → Allow Full Access.",
87+
isComplete: viewModel.keyboardFullAccess
88+
)
8089
}
8190
.padding(16)
8291
.background(AppColors.cardBackground, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
@@ -126,11 +135,50 @@ struct KeyboardSetupStepView: View {
126135
.padding(.horizontal, 40)
127136
.padding(.bottom, 60)
128137
}
129-
.animation(.easeInOut(duration: 0.3), value: viewModel.keyboardReady)
138+
.animation(.easeInOut(duration: 0.3), value: viewModel.keyboardEnabled)
139+
.animation(.easeInOut(duration: 0.3), value: viewModel.keyboardFullAccess)
130140
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
131141
viewModel.checkKeyboardStatus()
132142
}
133143
}
144+
145+
// MARK: - Step Row
146+
147+
private func stepRow(
148+
number: Int,
149+
icon: String,
150+
title: String,
151+
detail: String,
152+
isComplete: Bool
153+
) -> some View {
154+
HStack(alignment: .top, spacing: 14) {
155+
if isComplete {
156+
Image(systemName: "checkmark.circle.fill")
157+
.font(.system(size: 22))
158+
.foregroundStyle(AppColors.primaryGreen)
159+
.frame(width: 26, height: 26)
160+
} else {
161+
Text("\(number)")
162+
.font(.caption.weight(.bold))
163+
.foregroundStyle(.black)
164+
.frame(width: 26, height: 26)
165+
.background(AppColors.ctaOrange, in: Circle())
166+
}
167+
168+
VStack(alignment: .leading, spacing: 4) {
169+
Text(title)
170+
.font(.subheadline.weight(.semibold))
171+
.foregroundStyle(isComplete ? AppColors.textTertiary : AppColors.textPrimary)
172+
.strikethrough(isComplete)
173+
Text(detail)
174+
.font(.caption)
175+
.foregroundStyle(AppColors.textTertiary)
176+
}
177+
178+
Spacer()
179+
}
180+
.padding(.vertical, 12)
181+
}
134182
}
135183

136184
#endif

Playground/YapRun/YapRun/Shared/SharedConstants.swift

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,26 @@ enum SharedConstants {
1212
// App Group identifier — must match both targets' entitlements exactly
1313
static let appGroupID = "group.com.runanywhere.yaprun"
1414

15+
// Keyboard extension bundle ID — must match the keyboard target's bundle identifier exactly
16+
static let keyboardExtensionBundleId = "com.runanywhere.YapRun.YapRunKeyboard"
17+
1518
// URL scheme for keyboard → main app deep link (Flow Session trigger)
1619
static let urlScheme = "yaprun"
1720
static let startFlowURLString = "yaprun://startFlow"
1821

1922
// App Group UserDefaults keys
2023
enum Keys {
21-
static let sessionState = "sessionState"
22-
static let transcribedText = "transcribedText"
23-
static let returnToAppScheme = "returnToAppScheme"
24-
static let preferredSTTModelId = "preferredSTTModelId"
25-
static let dictationHistory = "dictationHistory"
26-
static let audioLevel = "audioLevel"
27-
static let lastInsertedText = "lastInsertedText"
28-
static let undoText = "undoText"
29-
static let lastHeartbeat = "lastHeartbeat"
30-
static let hasCompletedOnboarding = "hasCompletedOnboarding"
24+
static let sessionState = "sessionState"
25+
static let transcribedText = "transcribedText"
26+
static let returnToAppScheme = "returnToAppScheme"
27+
static let preferredSTTModelId = "preferredSTTModelId"
28+
static let dictationHistory = "dictationHistory"
29+
static let audioLevel = "audioLevel"
30+
static let lastInsertedText = "lastInsertedText"
31+
static let undoText = "undoText"
32+
static let lastHeartbeat = "lastHeartbeat"
33+
static let hasCompletedOnboarding = "hasCompletedOnboarding"
34+
static let keyboardFullAccessGranted = "keyboardFullAccessGranted"
3135
}
3236

3337
// Darwin inter-process notification names (CFNotificationCenter)

Playground/YapRun/YapRunKeyboard/KeyboardViewController.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ final class KeyboardViewController: UIInputViewController {
1717
override func viewDidLoad() {
1818
super.viewDidLoad()
1919

20+
// Write full-access status to App Group so the main app can read it.
21+
// hasFullAccess is true when the user has enabled "Allow Full Access" in Settings.
22+
SharedDataBridge.shared.defaults?.set(
23+
hasFullAccess,
24+
forKey: SharedConstants.Keys.keyboardFullAccessGranted
25+
)
26+
SharedDataBridge.shared.defaults?.synchronize()
27+
2028
DarwinNotificationCenter.shared.addObserver(
2129
name: SharedConstants.DarwinNotifications.transcriptionReady
2230
) { [weak self] in

Playground/YapRun/YapRunKeyboard/SharedConstants.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,25 @@ enum SharedConstants {
1212
// App Group identifier — must match both targets' entitlements exactly
1313
static let appGroupID = "group.com.runanywhere.yaprun"
1414

15+
// Keyboard extension bundle ID — must match the keyboard target's bundle identifier exactly
16+
static let keyboardExtensionBundleId = "com.runanywhere.YapRun.YapRunKeyboard"
17+
1518
// URL scheme for keyboard → main app deep link (Flow Session trigger)
1619
static let urlScheme = "yaprun"
1720
static let startFlowURLString = "yaprun://startFlow"
1821

1922
// App Group UserDefaults keys
2023
enum Keys {
21-
static let sessionState = "sessionState"
22-
static let transcribedText = "transcribedText"
23-
static let returnToAppScheme = "returnToAppScheme"
24-
static let preferredSTTModelId = "preferredSTTModelId"
25-
static let dictationHistory = "dictationHistory"
26-
static let audioLevel = "audioLevel"
27-
static let lastInsertedText = "lastInsertedText"
28-
static let undoText = "undoText"
29-
static let lastHeartbeat = "lastHeartbeat"
24+
static let sessionState = "sessionState"
25+
static let transcribedText = "transcribedText"
26+
static let returnToAppScheme = "returnToAppScheme"
27+
static let preferredSTTModelId = "preferredSTTModelId"
28+
static let dictationHistory = "dictationHistory"
29+
static let audioLevel = "audioLevel"
30+
static let lastInsertedText = "lastInsertedText"
31+
static let undoText = "undoText"
32+
static let lastHeartbeat = "lastHeartbeat"
33+
static let keyboardFullAccessGranted = "keyboardFullAccessGranted"
3034
}
3135

3236
// Darwin inter-process notification names (CFNotificationCenter)

0 commit comments

Comments
 (0)