Skip to content

Commit

Permalink
Merge pull request #40 from gertrude-app/bug-fixes-macapp
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredh159 committed Sep 18, 2023
2 parents 9a8bb3b + 40fa527 commit 4e336f2
Show file tree
Hide file tree
Showing 19 changed files with 650 additions and 173 deletions.
3 changes: 3 additions & 0 deletions macapp/App/Sources/App/App+PersistantState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ enum Persistent {
var appUpdateReleaseChannel: ReleaseChannel
var filterVersion: String
var user: UserData?

// onboardingStep added for v2.1.0, but is backwards compatible
var onboardingStep: OnboardingFeature.State.Step?
}

// v2.0.0 - v2.0.3
Expand Down
68 changes: 56 additions & 12 deletions macapp/App/Sources/App/AppReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct AppReducer: Reducer, Sendable {
var filter = FilterFeature.State()
var history = HistoryFeature.State()
var menuBar = MenuBarFeature.State()
var onboarding = OnboardingFeature.State()
var requestSuspension = RequestSuspensionFeature.State()
var user = UserFeature.State()
}
Expand All @@ -38,11 +39,13 @@ struct AppReducer: Reducer, Sendable {
case history(HistoryFeature.Action)
case menuBar(MenuBarFeature.Action)
case monitoring(MonitoringFeature.Action)
case onboarding(OnboardingFeature.Action)
case loadedPersistentState(Persistent.State?)
case user(UserFeature.Action)
case heartbeat(Heartbeat.Interval)
case blockedRequests(BlockedRequestsFeature.Action)
case requestSuspension(RequestSuspensionFeature.Action)
case startHeartbeat
case websocket(WebSocketFeature.Action)

indirect case adminAuthed(Action)
Expand All @@ -51,33 +54,64 @@ struct AppReducer: Reducer, Sendable {
@Dependency(\.api) var api
@Dependency(\.device) var device
@Dependency(\.backgroundQueue) var bgQueue
@Dependency(\.network) var network
@Dependency(\.storage) var storage

var body: some ReducerOf<Self> {
Reduce<State, Action> { state, action in
#if !DEBUG
os_log("[G•] APP received action: %{public}@", String(describing: action))
#endif

switch action {
case .loadedPersistentState(.some(let persistent)):
state.appUpdates.releaseChannel = persistent.appUpdateReleaseChannel
state.filter.version = persistent.filterVersion
guard let user = persistent.user else { return .none }
state.user = .init(data: user)
return .exec { [filterVersion = state.filter.version] send in
await api.setUserToken(user.token)
try await bgQueue.sleep(for: .milliseconds(10)) // <- unit test determinism
return await send(.checkIn(
result: TaskResult { try await api.appCheckIn(filterVersion) },
reason: .appLaunched
))
case .loadedPersistentState(nil):
state.onboarding.windowOpen = true
return .none

case .loadedPersistentState(.some(let persistent)) where persistent.onboardingStep != nil:
state.onboarding.windowOpen = true
state.onboarding.step = persistent.onboardingStep ?? .welcome
return .none

case .loadedPersistentState(.some(let persisted)):
state.appUpdates.releaseChannel = persisted.appUpdateReleaseChannel
state.filter.version = persisted.filterVersion
guard let user = persisted.user else {
return .exec { send in await send(.startHeartbeat) }
}
state.user = .init(data: user)
return .merge(
.exec { send in
await send(.startHeartbeat)
},
.exec { [filterVersion = state.filter.version] send in
await api.setUserToken(user.token)
guard network.isConnected() else { return }
await send(.checkIn(
result: TaskResult { try await api.appCheckIn(filterVersion) },
reason: .appLaunched
))
}
)

case .startHeartbeat:
return .exec { send in
var numTicks = 0
for await _ in bgQueue.timer(interval: .seconds(60)) {
numTicks += 1
for interval in heartbeatIntervals(for: numTicks) {
await send(.heartbeat(interval))
}
}
}.cancellable(id: Heartbeat.CancelId.interval)

case .focusedNotification(let notification):
// dismiss windows/dropdowns so notification is visible, i.e. "focused"
state.adminWindow.windowOpen = false
state.menuBar.dropdownOpen = false
state.blockedRequests.windowOpen = false
state.requestSuspension.windowOpen = false
state.onboarding.windowOpen = false
return .exec { _ in
switch notification {
case .unexpectedError:
Expand All @@ -87,6 +121,13 @@ struct AppReducer: Reducer, Sendable {
}
}

case .onboarding(.delegate(.saveCurrentStep(let step))):
return .exec { [persist = state.persistent] _ in
var copy = persist
copy.onboardingStep = step
try await storage.savePersistentState(copy)
}

default:
return .none
}
Expand Down Expand Up @@ -133,5 +174,8 @@ struct AppReducer: Reducer, Sendable {
Scope(state: \.user, action: /Action.user) {
UserFeature.Reducer()
}
Scope(state: \.onboarding, action: /Action.onboarding) {
OnboardingFeature.Reducer()
}
}
}
43 changes: 17 additions & 26 deletions macapp/App/Sources/App/ApplicationFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,16 @@ extension ApplicationFeature.RootReducer: RootReducing {
},

.exec { send in
try await bgQueue.sleep(for: .milliseconds(5)) // <- unit test determinism
// try await bgQueue.sleep(for: .milliseconds(5)) // <- unit test determinism
let setupState = await filterExtension.setup()
await send(.filter(.receivedState(setupState)))
if setupState.installed {
_ = await filterXpc.establishConnection()
}
},

.exec { send in
var numTicks = 0
for await _ in bgQueue.timer(interval: .seconds(60)) {
numTicks += 1
for interval in heartbeatIntervals(for: numTicks) {
await send(.heartbeat(interval))
}
}
}.cancellable(id: Heartbeat.CancelId.interval),

.exec { _ in
// TODO: should be part of onboarding...
if await app.isLaunchAtLoginEnabled() == false {
await app.enableLaunchAtLogin()
}
Expand Down Expand Up @@ -91,21 +82,21 @@ extension ApplicationFeature.RootReducer: RootReducing {
return .none
}
}
}

func heartbeatIntervals(for tick: Int) -> [Heartbeat.Interval] {
var intervals: [Heartbeat.Interval] = [.everyMinute]
if tick % 5 == 0 {
intervals.append(.everyFiveMinutes)
}
if tick % 20 == 0 {
intervals.append(.everyTwentyMinutes)
}
if tick % 60 == 0 {
intervals.append(.everyHour)
}
if tick % 360 == 0 {
intervals.append(.everySixHours)
}
return intervals
func heartbeatIntervals(for tick: Int) -> [Heartbeat.Interval] {
var intervals: [Heartbeat.Interval] = [.everyMinute]
if tick % 5 == 0 {
intervals.append(.everyFiveMinutes)
}
if tick % 20 == 0 {
intervals.append(.everyTwentyMinutes)
}
if tick % 60 == 0 {
intervals.append(.everyHour)
}
if tick % 360 == 0 {
intervals.append(.everySixHours)
}
return intervals
}
166 changes: 166 additions & 0 deletions macapp/App/Sources/App/Onboarding/OnboardingFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import ComposableArchitecture
import Foundation

struct OnboardingFeature: Feature {
struct State: Equatable, Encodable {
struct MacUser: Equatable, Encodable {
var id: uid_t
var name: String
var isAdmin: Bool

enum RemediationStep: String, Equatable, Encodable {
case create
case `switch`
case demote
case choose
}
}

var windowOpen = false
var step: Step = .welcome
var userRemediationStep: MacUser.RemediationStep?
var currentUser: MacUser?
var users: [MacUser] = []
}

enum Action: Equatable, Sendable {
enum Webview: Equatable, Sendable {
case primaryBtnClicked
case secondaryBtnClicked
case chooseSwitchToNonAdminUserClicked
case chooseCreateNonAdminClicked
case chooseDemoteAdminClicked
}

enum Delegate: Equatable, Sendable {
case saveCurrentStep(State.Step?)
}

case webview(Webview)
case delegate(Delegate)
case receivedUserData(uid_t, [MacOSUser])
}

struct Reducer: FeatureReducer {
@Dependency(\.app) var app
@Dependency(\.device) var device
@Dependency(\.storage) var storage

func reduce(into state: inout State, action: Action) -> Effect<Action> {
let step = state.step
let userIsAdmin = state.currentUser?.isAdmin != false
switch action {

case .receivedUserData(let currentUserId, let users):
state.users = users.map(State.MacUser.init)
state.currentUser = state.users.first(where: { $0.id == currentUserId })
return .none

case .webview(.primaryBtnClicked) where step == .welcome:
state.step = .confirmGertrudeAccount
return .exec { send in
await send(.receivedUserData(device.currentUserId(), try await device.listMacOSUsers()))
}

case .webview(.primaryBtnClicked) where step == .confirmGertrudeAccount:
state.step = .macosUserAccountType
return .none

case .webview(.secondaryBtnClicked) where step == .confirmGertrudeAccount:
state.step = .noGertrudeAccount
return .none

case .webview(.secondaryBtnClicked) where step == .noGertrudeAccount:
return .exec { _ in
await storage.deleteAll()
await app.quit()
}

case .webview(.primaryBtnClicked) where step == .macosUserAccountType && !userIsAdmin:
state.step = .getChildConnectionCode
return .none

// they choose to ignore the warning about user type and proceed
case .webview(.secondaryBtnClicked) where step == .macosUserAccountType && userIsAdmin:
state.step = .getChildConnectionCode
return .none

// they click "show me how to fix" on the BAD mac os user landing page
case .webview(.primaryBtnClicked) where step == .macosUserAccountType && userIsAdmin:
state.userRemediationStep = state.users.count == 1 ? .create : .choose
return .send(.delegate(.saveCurrentStep(.macosUserAccountType)))

case .webview(.chooseDemoteAdminClicked):
state.userRemediationStep = .demote
return .none

case .webview(.chooseCreateNonAdminClicked):
state.userRemediationStep = .create
return .none

case .webview(.chooseSwitchToNonAdminUserClicked):
state.userRemediationStep = .switch
return .none

case .webview(.primaryBtnClicked):
return .none

case .webview(.secondaryBtnClicked):
return .none

case .delegate:
return .none
}
}
}

struct RootReducer: RootReducing {
// todo
}
}

extension OnboardingFeature.RootReducer {
func reduce(into state: inout State, action: Action) -> Effect<Self.Action> {
.none
}
}

extension OnboardingFeature.State {
enum Step: Equatable, Codable {
case welcome
case confirmGertrudeAccount
case noGertrudeAccount
case macosUserAccountType
case getChildConnectionCode
case connectChild
case allowNotifications_start
case allowNotifications_grant
case allowScreenshots_required
case allowScreenshots_openSysSettings
case allowScreenshots_grantAndRestart
case allowScreenshots_success
case allowKeylogging_required
case allowKeylogging_openSysSettings
case allowKeylogging_grant
case allowKeylogging_failed
case allowKeylogging_success
case installSysExt_explain
case installSysExt_start
case installSysExt_allowInstall
case installSysExt_allowFiltering
case installSysExt_failed
case installSysExt_success
case locateMenuBarIcon
case viewHealthCheck
case howToUseGertrude
case finish
}
}

extension OnboardingFeature.State.MacUser {
init(_ user: MacOSUser) {
id = user.id
name = user.name
isAdmin = user.type == .admin
}
}
Loading

0 comments on commit 4e336f2

Please sign in to comment.