New onboarding flow.

This commit is contained in:
Max Goedjen 2020-09-20 00:12:20 -07:00
parent d1a2ae79bf
commit e2f519987f
No known key found for this signature in database
GPG Key ID: E58C21DD77B9B8E8
5 changed files with 369 additions and 293 deletions

View File

@ -29,6 +29,8 @@
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; }; 50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; };
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; }; 50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; };
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
506772FF2426F3F400034DED /* Brief.h in Headers */ = {isa = PBXBuildFile; fileRef = 506772FD2426F3F400034DED /* Brief.h */; settings = {ATTRIBUTES = (Public, ); }; }; 506772FF2426F3F400034DED /* Brief.h in Headers */ = {isa = PBXBuildFile; fileRef = 506772FD2426F3F400034DED /* Brief.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -78,7 +80,6 @@
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A2240789E600AF2719 /* OpenSSHReader.swift */; }; 50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A2240789E600AF2719 /* OpenSSHReader.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50C385A9240B636500AF2719 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A8240B636500AF2719 /* SetupView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -234,6 +235,8 @@
50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; }; 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; };
50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; }; 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; };
50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = "<group>"; }; 50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = "<group>"; };
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
506772FB2426F3F400034DED /* Brief.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Brief.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 506772FB2426F3F400034DED /* Brief.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Brief.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -283,7 +286,6 @@
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; }; 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretKit/Common/OpenSSH/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; }; 50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretKit/Common/OpenSSH/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50C385A8240B636500AF2719 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -510,7 +512,8 @@
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */, 506772C82425BB8500034DED /* NoStoresView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */, 50153E1F250AFCB200525160 /* UpdateView.swift */,
50C385A8240B636500AF2719 /* SetupView.swift */, 5066A6C12516F303004B5A36 /* SetupView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -922,7 +925,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50C385A9240B636500AF2719 /* SetupView.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
@ -932,6 +935,7 @@
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50617D8323FCE48E0099B055 /* App.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */,

View File

@ -53,7 +53,8 @@ extension ContentView {
color = .orange color = .orange
} }
return ToolbarItem { return ToolbarItem {
AnyView(Button(action: { AnyView(
Button(action: {
selectedUpdate = update selectedUpdate = update
}, label: { }, label: {
Text(text) Text(text)
@ -74,11 +75,13 @@ extension ContentView {
return ToolbarItem { AnyView(Spacer()) } return ToolbarItem { AnyView(Spacer()) }
} }
return ToolbarItem { return ToolbarItem {
AnyView(Button(action: { AnyView(
Button(action: {
showingCreation = true showingCreation = true
}, label: { }, label: {
Image(systemName: "plus") Image(systemName: "plus")
})) })
)
} }
} }
@ -104,10 +107,11 @@ extension ContentView {
.background(Color.orange) .background(Color.orange)
.cornerRadius(5) .cornerRadius(5)
.popover(isPresented: $runningSetup, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { .popover(isPresented: $runningSetup, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
SetupView { completed in // SetupView { completed in
runningSetup = false // runningSetup = false
hasRunSetup = completed // hasRunSetup = completed
} // }
SetupView(visible: $runningSetup)
} }
) )
} }

View File

@ -0,0 +1,131 @@
import SwiftUI
struct CopyableView: View {
var title: String
var image: Image
var text: String
@State private var interactionState: InteractionState = .normal
var body: some View {
VStack(alignment: .leading) {
HStack {
image
.renderingMode(.template)
.imageScale(.large)
.foregroundColor(primaryTextColor)
Text(title)
.font(.headline)
.foregroundColor(primaryTextColor)
Spacer()
if interactionState != .normal {
Text(hoverText)
.bold()
.textCase(.uppercase)
.foregroundColor(secondaryTextColor)
.transition(.opacity)
}
}
.padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20))
Divider()
Text(text)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(primaryTextColor)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20))
.multilineTextAlignment(.leading)
.font(.system(.body, design: .monospaced))
}
.background(backgroundColor)
.frame(minWidth: 150, maxWidth: .infinity)
.cornerRadius(10)
.onHover { hovering in
withAnimation {
interactionState = hovering ? .hovering : .normal
}
}
.onDrag {
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
}
.onTapGesture {
copy()
withAnimation {
interactionState = .clicking
}
}
.gesture(
TapGesture()
.onEnded {
withAnimation {
interactionState = .normal
}
}
)
}
var hoverText: String {
switch interactionState {
case .hovering:
return "Click to Copy"
case .clicking:
return "Copied"
case .normal:
fatalError()
}
}
var backgroundColor: Color {
let color: NSColor
switch interactionState {
case .normal:
color = .windowBackgroundColor
case .hovering:
color = .unemphasizedSelectedContentBackgroundColor
case .clicking:
color = .selectedContentBackgroundColor
}
return Color(color)
}
var primaryTextColor: Color {
let color: NSColor
switch interactionState {
case .normal, .hovering:
color = .textColor
case .clicking:
color = .white
}
return Color(color)
}
var secondaryTextColor: Color {
let color: NSColor
switch interactionState {
case .normal, .hovering:
color = .secondaryLabelColor
case .clicking:
color = .white
}
return Color(color)
}
func copy() {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(text, forType: .string)
}
private enum InteractionState {
case normal, hovering, clicking
}
}
struct CopyableView_Previews: PreviewProvider {
static var previews: some View {
Group {
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Hello world.")
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
}
}
}

View File

@ -18,8 +18,7 @@ struct SecretDetailView<SecretType: Secret>: View {
} }
} }
.padding() .padding()
.frame(minHeight: 150, maxHeight: .infinity) .frame(minHeight: 200, maxHeight: .infinity)
} }
var keyString: String { var keyString: String {
@ -39,122 +38,3 @@ struct SecretDetailView_Previews: PreviewProvider {
} }
} }
struct CopyableView: View {
var title: String
var image: Image
var text: String
@State private var interactionState: InteractionState = .normal
var body: some View {
VStack(alignment: .leading) {
HStack {
image
.renderingMode(.template)
.imageScale(.large)
.foregroundColor(primaryTextColor)
Text(title)
.font(.headline)
.foregroundColor(primaryTextColor)
Spacer()
if interactionState != .normal {
Text(hoverText)
.bold()
.textCase(.uppercase)
.foregroundColor(secondaryTextColor)
.transition(.opacity)
}
}
.padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20))
Divider()
Text(text)
.foregroundColor(primaryTextColor)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20))
.multilineTextAlignment(.leading)
.font(.system(.body, design: .monospaced))
}
.background(backgroundColor)
.frame(minWidth: 150, maxWidth: .infinity)
.cornerRadius(10)
.onHover { hovering in
withAnimation {
interactionState = hovering ? .hovering : .normal
}
}
.onDrag {
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
}
.onTapGesture {
copy()
withAnimation {
interactionState = .clicking
}
}
.gesture(
TapGesture()
.onEnded {
withAnimation {
interactionState = .normal
}
}
)
}
var hoverText: String {
switch interactionState {
case .hovering:
return "Click to Copy"
case .clicking:
return "Copied!"
case .normal:
fatalError()
}
}
var backgroundColor: Color {
let color: NSColor
switch interactionState {
case .normal:
color = .windowBackgroundColor
case .hovering:
color = .unemphasizedSelectedContentBackgroundColor
case .clicking:
color = .selectedContentBackgroundColor
}
return Color(color)
}
var primaryTextColor: Color {
let color: NSColor
switch interactionState {
case .normal, .hovering:
color = .textColor
case .clicking:
color = .white
}
return Color(color)
}
var secondaryTextColor: Color {
let color: NSColor
switch interactionState {
case .normal, .hovering:
color = .secondaryLabelColor
case .clicking:
color = .white
}
return Color(color)
}
func copy() {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(text, forType: .string)
}
private enum InteractionState {
case normal, hovering, clicking
}
}

View File

@ -1,149 +1,219 @@
import Foundation
import SwiftUI import SwiftUI
struct SetupView: View { struct SetupView: View {
var completion: ((Bool) -> Void)? @State var stepIndex = 0
@State var completedSteps: Set<Step> = [] @Binding var visible: Bool
var body: some View { var body: some View {
Form { VStack {
SetupStepView<Spacer>(text: "Secretive needs to install a helper app to sign requests when the main app isn't running. This app is called \"SecretAgent\" and you might see it in Activity Manager from time to time.", StepView(numberOfSteps: 3, currentStep: stepIndex)
stepID: .agent, GeometryReader { proxy in
nestedView: nil,
actionText: "Install") {
let success = installLaunchAgent()
if success {
completedSteps.insert(.agent)
}
return success
}
SetupStepView(text: "Add this line to your shell config telling SSH to talk to SecretAgent when it wants to authenticate. Drag this into your config file.",
stepID: .shellConfig,
nestedView: SetupStepCommandView(instructions: Constants.socketPrompts, selectedShellInstruction: Constants.socketPrompts.first!),
actionText: "Added") {
markAsDone(.shellConfig)
}
SetupStepView<Link>(text: "Secretive will periodically check with GitHub to see if there's a new release. If you see any network requests to GitHub, that's why.",
stepID: .updateNotice,
nestedView: Link("Read more about this here.", destination: Constants.updaterFAQURL),
actionText: "Got it") {
markAsDone(.updateNotice)
}
HStack { HStack {
Spacer() SecretAgentSetupView(buttonAction: advance)
Button("Finish") { .frame(width: proxy.size.width)
completion?(completedAllSteps) SSHAgentSetupView(buttonAction: advance)
}.disabled(!completedAllSteps) .frame(width: proxy.size.width)
.padding() UpdaterExplainerView {
visible = false
} }
}.frame(minWidth: 640, minHeight: 400) .frame(width: proxy.size.width)
}
.offset(x: -proxy.size.width * CGFloat(stepIndex), y: 0)
.animation(.spring())
}
}
.frame(idealWidth: 500, idealHeight: 500)
}
func advance() {
stepIndex += 1
} }
} }
struct SetupStepView<NestedViewType: View>: View { struct SetupView_Previews: PreviewProvider {
let text: String static var previews: some View {
let stepID: Step Group {
let nestedView: NestedViewType? SetupView(visible: .constant(true))
@State var completed = false }
let actionText: String }
let action: (() -> Bool)
}
struct StepView: View {
let numberOfSteps: Int
let currentStep: Int
var body: some View { var body: some View {
Section {
HStack {
ZStack { ZStack {
if completed { Rectangle()
Circle().foregroundColor(.green) .foregroundColor(.blue)
.frame(height: 5)
HStack {
ForEach(0..<numberOfSteps) { index in
ZStack {
if currentStep > index {
Circle()
.foregroundColor(.green)
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
Text("") Text("")
.foregroundColor(.white) .foregroundColor(.white)
.bold() .bold()
} else { } else {
Circle().foregroundColor(.blue) Circle()
.foregroundColor(currentStep == index ? .white : .blue)
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
Text(String(describing: stepID.rawValue + 1)) Text(String(describing: index + 1))
.foregroundColor(.white) .foregroundColor(currentStep == index ? .blue : .white)
.bold() .bold()
} }
} }
.padding() if index < numberOfSteps - 1 {
VStack { Spacer(minLength: 30)
Text(text) }
.opacity(completed ? 0.5 : 1) }
.lineLimit(nil)
if nestedView != nil {
nestedView!.padding()
} }
} }
.padding() .padding()
Button(actionText) {
completed = action()
}.frame(alignment: .trailing)
.disabled(completed)
.padding()
}
}
} }
} }
struct SetupStepCommandView: View { struct SetupStepView<Content> : View where Content : View {
let instructions: [ShellConfigInstruction] let title: String
let image: Image
let bodyText: String
let buttonTitle: String
let buttonAction: () -> Void
let content: Content
@State var selectedShellInstruction: ShellConfigInstruction init(title: String, image: Image, bodyText: String, buttonTitle: String, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
self.title = title
self.image = image
self.bodyText = bodyText
self.buttonTitle = buttonTitle
self.buttonAction = buttonAction
self.content = content()
}
var body: some View { var body: some View {
TabView(selection: $selectedShellInstruction) { VStack {
ForEach(instructions) { instruction in Text(title)
VStack(alignment: .leading) { .font(.title)
Text(instruction.text)
.lineLimit(nil)
.font(.system(.caption, design: .monospaced))
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
HStack {
Spacer() Spacer()
Button(action: copy) { image
Text("Copy") .resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Spacer()
Text(bodyText)
.multilineTextAlignment(.center)
Spacer()
content
Spacer()
Button(buttonTitle) {
buttonAction()
} }
} }
}.tabItem { .padding()
}
}
struct SecretAgentSetupView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: "Setup Secret Agent",
image: Image(nsImage: NSApp.applicationIconImage),
bodyText: "Secretive needs to set up a helper app to work properly. It will sign requests from SSH clients in the background, so you don't need to keep the main Secretive app open.",
buttonTitle: "Install",
buttonAction: install) {
(Text("This helper app is called ") + Text("Secret Agent").bold().underline() + Text(" and you may see it in Activity Manager from time to time."))
.multilineTextAlignment(.center)
}
}
func install() {
_ = LaunchAgentController().install()
buttonAction()
}
}
struct SecretAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SecretAgentSetupView(buttonAction: {})
}
}
}
struct SSHAgentSetupView: View {
@State var selectedShellInstruction: ShellConfigInstruction = SetupView.Constants.socketPrompts.first!
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: "Configure your SSH Agent",
image: Image(systemName: "terminal"),
bodyText: "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Drag this into your config file.",
buttonTitle: "Done",
buttonAction: buttonAction) {
Picker(selection: $selectedShellInstruction, label: EmptyView()) {
ForEach(SetupView.Constants.socketPrompts) { instruction in
Text(instruction.shell) Text(instruction.shell)
}
.tag(instruction) .tag(instruction)
.padding() .padding()
} }
}.pickerStyle(SegmentedPickerStyle())
CopyableView(title: "Add to \(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
} }
.onDrag {
return NSItemProvider(item: NSData(data: selectedShellInstruction.text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
}
}
func copy() {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(selectedShellInstruction.text, forType: .string)
} }
} }
extension SetupView { struct SSHAgentSetupView_Previews: PreviewProvider {
func installLaunchAgent() -> Bool { static var previews: some View {
LaunchAgentController().install() Group {
SSHAgentSetupView(buttonAction: {})
}
} }
func markAsDone(_ step: Step) -> Bool { }
completedSteps.insert(step)
return true
struct UpdaterExplainerView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: "Updates",
image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: "Secretive will periodically check with GitHub to see if there's a new release. If you see any network requests to GitHub, that's why.",
buttonTitle: "Okay",
buttonAction: buttonAction) {
Link("Read more about this here.", destination: SetupView.Constants.updaterFAQURL)
}
} }
var completedAllSteps: Bool { }
completedSteps == Set(Step.allCases)
}
struct UpdaterExplainerView_Previews: PreviewProvider {
static var previews: some View {
Group {
UpdaterExplainerView(buttonAction: {})
}
}
} }
extension SetupView { extension SetupView {
@ -151,9 +221,15 @@ extension SetupView {
enum Constants { enum Constants {
static let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String static let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String
static let socketPrompts: [ShellConfigInstruction] = [ static let socketPrompts: [ShellConfigInstruction] = [
ShellConfigInstruction(shell: "zsh", text: "export SSH_AUTH_SOCK=\(socketPath)"), ShellConfigInstruction(shell: "zsh",
ShellConfigInstruction(shell: "bash", text: "export SSH_AUTH_SOCK=\(socketPath)"), shellConfigPath: "~/.zshrc",
ShellConfigInstruction(shell: "fish", text: "set -x SSH_AUTH_SOCK=\(socketPath)"), text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "bash",
shellConfigPath: "~/.bashrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "fish",
shellConfigPath: "~/.config/fish/config.fish",
text: "set -x SSH_AUTH_SOCK=\(socketPath)"),
] ]
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")! static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
} }
@ -163,6 +239,7 @@ extension SetupView {
struct ShellConfigInstruction: Identifiable, Hashable { struct ShellConfigInstruction: Identifiable, Hashable {
var shell: String var shell: String
var shellConfigPath: String
var text: String var text: String
var id: String { var id: String {
@ -170,23 +247,3 @@ struct ShellConfigInstruction: Identifiable, Hashable {
} }
} }
enum Step: Int, Identifiable, Hashable, CaseIterable {
case agent, shellConfig, updateNotice
var id: Int {
rawValue
}
}
struct SetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SetupView()
SetupView()
.frame(width: 1500, height: 400)
}
}
}