Skip to content

Commit

Permalink
Merge pull request #46 from gertrude-app/onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredh159 authored Oct 21, 2023
2 parents 690a107 + dfc1fe1 commit 402f673
Show file tree
Hide file tree
Showing 74 changed files with 3,069 additions and 378 deletions.
3 changes: 3 additions & 0 deletions api/Sources/Api/Environment/Ephemeral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ actor Ephemeral {
}

func getPendingAppConnection(_ code: Int) -> User.Id? {
#if DEBUG
if code == 999_999 { return AdminBetsy.Ids.jimmysId }
#endif
guard let (userId, expiration) = pendingAppConnections[code],
expiration > Current.date() else {
return nil
Expand Down
1 change: 1 addition & 0 deletions api/Sources/Api/Models/Admin/Device+Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ extension Device.Model.Chip {
}

enum MacOS: Encodable {
case sonoma
case ventura
case monterey
case bigSur
Expand Down
10 changes: 8 additions & 2 deletions api/Sources/Api/PairQL/MacApp/Resolvers/ConnectUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ extension ConnectUser: Resolver {
else { throw context.error(
id: "6e7fc234",
type: .unauthorized,
debugMessage: "ConnectApp verification code not found",
debugMessage: "verification code not found",
userMessage: "Connection code not found, or expired. Please try again.",
appTag: .connectionCodeNotFound
) }

Expand Down Expand Up @@ -37,7 +38,12 @@ extension ConnectUser: Resolver {
// sanity check - we only "transfer" a device, if the admin accounts match
let existingUser = try await existingUserDevice.user()
if existingUser.adminId != user.adminId {
throw Abort(.forbidden, reason: "Device already registered to another admin's user")
throw context.error(
id: "41a43089",
type: .unauthorized,
debugMessage: "invalid connect transfer attempt",
userMessage: "This user is associated with another Gertrude parent account."
)
}

let oldUserId = existingUserDevice.userId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ final class ConnectUserResolversTests: ApiTestCase {

try await expectErrorFrom { [self] in
_ = try await ConnectUser.resolve(with: input, in: self.context)
}.toContain("registered to another admin")
}.toContain("associated with another Gertrude parent account")

// old token is not deleted
let retrievedOldToken = try? await Current.db.find(existingUserToken.id)
Expand Down
2 changes: 1 addition & 1 deletion gertie/Sources/Gertie/WebSocketMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public enum WebSocketMessage {
case suspendFilterRequestDenied(parentComment: String?)
}

public enum FromAppToApi: Codable {
public enum FromAppToApi: Codable, Equatable {
case currentFilterState(UserFilterState)
case goingOffline
}
Expand Down
6 changes: 5 additions & 1 deletion macapp/App/Sources/App/AdminFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extension AdminFeature.RootReducer: RootReducing, AdminAuthenticating {
case .adminWindow(.webview(.inactiveAccountDisconnectAppClicked)),
.blockedRequests(.webview(.inactiveAccountDisconnectAppClicked)),
.requestSuspension(.webview(.inactiveAccountDisconnectAppClicked)),
.requestSuspension(.webview(.noFilterCommunicationAdministrateClicked)),
.blockedRequests(.webview(.noFilterCommunicationAdministrateClicked)):
return adminAuthenticated(action)

Expand All @@ -49,7 +50,10 @@ extension AdminFeature.RootReducer: RootReducing, AdminAuthenticating {
state.requestSuspension.windowOpen = false
state.adminWindow.windowOpen = true
state.adminWindow.screen = .healthCheck
return .none
return .exec { send in
// we've already authed the admin, this kicks off a full health check
await send(.adminAuthed(.menuBar(.administrateClicked)))
}

default:
return .none
Expand Down
6 changes: 5 additions & 1 deletion 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?

// added for v2.1.0, but is backwards compatible
var resumeOnboarding: OnboardingFeature.Resume?
}

// v2.0.0 - v2.0.3
Expand All @@ -28,7 +31,8 @@ extension AppReducer.State {
appVersion: appUpdates.installedVersion,
appUpdateReleaseChannel: appUpdates.releaseChannel,
filterVersion: filter.version,
user: user.data
user: user.data,
resumeOnboarding: nil
)
}
}
10 changes: 9 additions & 1 deletion macapp/App/Sources/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ typealias UserData = GetUserData.Output
var blockedRequestsWindow: BlockedRequestsWindow
var adminWindow: AdminWindow
var requestSuspensionWindow: RequestSuspensionWindow
var onboardingWindow: OnboardingWindow
let store = Store(
initialState: AppReducer.State(),
initialState: AppReducer.State(appVersion: {
@Dependency(\.app) var appClient
return appClient.installedVersion()
}()),
reducer: {
AppReducer()._printChanges(.filteredBy { action in
switch action {
Expand Down Expand Up @@ -45,6 +49,10 @@ typealias UserData = GetUserData.Output
state: { $0 },
action: AppReducer.Action.requestSuspension
))
onboardingWindow = OnboardingWindow(store: store.scope(
state: { $0 },
action: AppReducer.Action.onboarding
))

#if !DEBUG
setEventReporter { kind, eventId, detail in
Expand Down
120 changes: 105 additions & 15 deletions macapp/App/Sources/App/AppReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,25 @@ struct AppReducer: Reducer, Sendable {
struct State: Equatable, Sendable {
var admin = AdminFeature.State()
var adminWindow = AdminWindowFeature.State()
var appUpdates = AppUpdatesFeature.State()
var appUpdates: AppUpdatesFeature.State
var blockedRequests = BlockedRequestsFeature.State()
var filter = FilterFeature.State()
var filter: FilterFeature.State
var history = HistoryFeature.State()
var menuBar = MenuBarFeature.State()
var onboarding = OnboardingFeature.State()
var monitoring = MonitoringFeature.State()
var requestSuspension = RequestSuspensionFeature.State()
var user = UserFeature.State()

init(appVersion: String?) {
appUpdates = .init(installedVersion: appVersion)
filter = .init(appVersion: appVersion)
}
}

enum CancelId {
case heartbeatInterval
case websocketMessages
}

enum Action: Equatable, Sendable {
Expand All @@ -43,46 +54,105 @@ 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 heartbeat(HeartbeatInterval)
case blockedRequests(BlockedRequestsFeature.Action)
case requestSuspension(RequestSuspensionFeature.Action)
case startProtecting(user: UserData)
case websocket(WebSocketFeature.Action)

indirect case adminAuthed(Action)
}

@Dependency(\.api) var api
@Dependency(\.app) var app
@Dependency(\.device) var device
@Dependency(\.backgroundQueue) var bgQueue
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.network) var network
@Dependency(\.storage) var storage
@Dependency(\.websocket) var websocket

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(.none):
state.onboarding.windowOpen = true
return .exec { [new = state.persistent] _ in
try await storage.savePersistentState(new)
}

case .loadedPersistentState(.some(let persisted)):
state.appUpdates.releaseChannel = persisted.appUpdateReleaseChannel
state.filter.version = persisted.filterVersion
var effects: [Effect<Action>] = []
if let user = persisted.user {
state.user = .init(data: user)
if persisted.resumeOnboarding == nil {
effects.append(.exec { send in
await send(.startProtecting(user: user))
})
} else {
state.onboarding.connectChildRequest = .succeeded(payload: user.name)
}
}
if let onboardingStep = persisted.resumeOnboarding {
effects.append(.exec { send in
await send(.onboarding(.resume(onboardingStep)))
})
effects.append(.exec { [persist = state.persistent] _ in
var withoutResume = persist
withoutResume.resumeOnboarding = nil
try await storage.savePersistentState(withoutResume)
})
}
return .merge(effects)

case .startProtecting(let user):
let onboardingWindowOpen = state.onboarding.windowOpen
return .merge(
.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: .startProtecting
))
},
.exec { _ in
if onboardingWindowOpen == false, (await app.isLaunchAtLoginEnabled()) == false {
await app.enableLaunchAtLogin()
}
},
.publisher {
websocket.receive()
.map { .websocket(.receivedMessage($0)) }
.receive(on: mainQueue)
}.cancellable(id: CancelId.websocketMessages),
.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: CancelId.heartbeatInterval)
)

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 @@ -92,6 +162,23 @@ struct AppReducer: Reducer, Sendable {
}
}

case .onboarding(.delegate(.saveForResume(let resume))):
OnboardingFeature.Reducer()
.log("save for resume: \(String(describing: resume))", "93e00bac")
return .exec { [persist = state.persistent] _ in
var copy = persist
copy.resumeOnboarding = resume
try await storage.savePersistentState(copy)
}

case .onboarding(.delegate(.onboardingConfigComplete)):
OnboardingFeature.Reducer().log("finished", "079cbee4")
if let user = state.user.data {
return .exec { send in await send(.startProtecting(user: user)) }
} else {
return .none
}

default:
return .none
}
Expand Down Expand Up @@ -144,5 +231,8 @@ struct AppReducer: Reducer, Sendable {
Scope(state: \.user, action: /Action.user) {
UserFeature.Reducer()
}
Scope(state: \.onboarding, action: /Action.onboarding) {
OnboardingFeature.Reducer()
}
}
}
11 changes: 3 additions & 8 deletions macapp/App/Sources/App/AppUpdatesFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@ struct AppUpdatesFeature: Feature {
}

extension AppUpdatesFeature.State {
init() {
@Dependency(\.app) var appClient
init(installedVersion: String?) {
self.init(
installedVersion: appClient.installedVersion() ?? "0.0.0",
installedVersion: installedVersion ?? "0.0.0",
releaseChannel: .stable,
latestVersion: nil
)
Expand All @@ -56,10 +55,6 @@ extension AppUpdatesFeature.RootReducer: FilterControlling {

func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .loadedPersistentState(.none):
return .exec { [new = state.persistent] _ in
try await storage.savePersistentState(new)
}

case .loadedPersistentState(.some(let restored)):
guard restored.appVersion != state.appUpdates.installedVersion else {
Expand All @@ -84,7 +79,7 @@ extension AppUpdatesFeature.RootReducer: FilterControlling {
// refresh the rules post-update, or else health check will complain
await send(.checkIn(
result: TaskResult { try await api.appCheckIn(version) },
reason: .appLaunched
reason: .appUpdated
))

// big sur doesn't get notification pushed when filter restarts
Expand Down
10 changes: 10 additions & 0 deletions macapp/App/Sources/App/AppWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ protocol AppWindow: AnyObject {
var windowLevel: NSWindow.Level { get }
var title: String { get }
var screen: String { get }
var showTitleBar: Bool { get }
var closeWindowAction: Action { get }

func embed(_ webviewAction: WebViewAction) -> Action
Expand All @@ -29,6 +30,7 @@ extension AppWindow {
var windowLevel: NSWindow.Level { .normal }
var initialSize: NSRect { NSRect(x: 0, y: 0, width: 900, height: 600) }
var minSize: NSSize { NSSize(width: 800, height: 500) }
var showTitleBar: Bool { true }

@MainActor func bind() {
windowDelegate.events
Expand Down Expand Up @@ -69,10 +71,18 @@ extension AppWindow {
window?.delegate = windowDelegate
window?.tabbingMode = .disallowed
window?.titlebarAppearsTransparent = true

if !showTitleBar {
window?.titleVisibility = .hidden
window?.styleMask.insert(NSWindow.StyleMask.fullSizeContentView)
window?.isMovableByWindowBackground = true
}

window?.isReleasedWhenClosed = false
window?.level = windowLevel

let wvc = WebViewController<State, WebViewAction>()
wvc.withTitleBar = showTitleBar

wvc.send = { [weak self] action in
guard let self = self else { return }
Expand Down
Loading

0 comments on commit 402f673

Please sign in to comment.