-
Notifications
You must be signed in to change notification settings - Fork 90
feat: implement managed sign-in setup flow for onboarding #16668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dvargasfuertes
merged 11 commits into
main
from
devin/1773497772-managed-sign-in-setup-flow
Mar 14, 2026
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
900e4c6
feat: implement managed sign-in setup flow for onboarding
devin-ai-integration[bot] a11491a
fix: onSubmit calls saveAndHatch when managedSignInEnabled; split fil…
devin-ai-integration[bot] 6a6eb1b
fix: validate loaded hosting mode against available modes
devin-ai-integration[bot] 7dd4a02
fix: add accessibility label to file picker clear button per AGENTS.md
devin-ai-integration[bot] ffdad8c
fix: guard onSubmit against hatchButtonDisabled to prevent hatching w…
devin-ai-integration[bot] 643c3ee
feat: show Get Started when managed sign-in off, auto-advance authent…
devin-ai-integration[bot] 609f15c
feat: hide Skip/Back for authenticated users, return to welcome on si…
devin-ai-integration[bot] 951002c
fix: reset bootstrap state when user signs out during managed onboarding
devin-ai-integration[bot] e7a4183
feat: drop user into hosting selector after login instead of auto-hat…
devin-ai-integration[bot] 35dea76
fix: guard onChange advance with currentStep==0 to prevent double-adv…
devin-ai-integration[bot] 6f3ea2d
fix: remove dead didInitiateLogin, restore managedBootstrapEnabled pa…
devin-ai-integration[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
310 changes: 310 additions & 0 deletions
310
clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView+CloudFields.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,310 @@ | ||
| import SwiftUI | ||
| import UniformTypeIdentifiers | ||
| import VellumAssistantShared | ||
|
|
||
| // MARK: - Inline Cloud Credential Fields | ||
|
|
||
| extension APIKeyStepView { | ||
|
|
||
| @ViewBuilder | ||
| var inlineCloudCredentialFields: some View { | ||
| switch hostingMode { | ||
| case .gcp: | ||
| gcpInlineFields | ||
| case .aws: | ||
| awsInlineFields | ||
| case .customHardware: | ||
| customHardwareInlineFields | ||
| default: | ||
| EmptyView() | ||
| } | ||
| } | ||
|
|
||
| var gcpInlineFields: some View { | ||
| VStack(spacing: VSpacing.sm) { | ||
| gcpSetupBlurb | ||
|
|
||
| VStack(alignment: .leading, spacing: VSpacing.xs) { | ||
| Text("Project ID") | ||
| .font(.system(size: 13, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| TextField("my-gcp-project-id", text: $state.gcpProjectId) | ||
| .textFieldStyle(.plain) | ||
| .font(.system(size: 14, weight: .medium, design: .monospaced)) | ||
| .foregroundColor(VColor.contentDefault) | ||
| .padding(.horizontal, VSpacing.lg) | ||
| .padding(.vertical, VSpacing.md) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: VRadius.lg) | ||
| .stroke(VColor.borderBase, lineWidth: 1) | ||
| ) | ||
| .focused($projectIdFieldFocused) | ||
| } | ||
|
|
||
| VStack(alignment: .leading, spacing: VSpacing.xs) { | ||
| Text("Zone") | ||
| .font(.system(size: 13, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| Picker("", selection: $state.gcpZone) { | ||
| ForEach(Self.gcpZones, id: \.self) { zone in | ||
| Text(zone).tag(zone) | ||
| } | ||
| } | ||
| .pickerStyle(.menu) | ||
| .labelsHidden() | ||
| .font(.system(size: 14, weight: .medium, design: .monospaced)) | ||
| .foregroundColor(VColor.contentDefault) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .padding(.horizontal, VSpacing.sm) | ||
| .padding(.vertical, VSpacing.xs) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: VRadius.lg) | ||
| .stroke(VColor.borderBase, lineWidth: 1) | ||
| ) | ||
| } | ||
|
|
||
| VStack(alignment: .leading, spacing: VSpacing.xs) { | ||
| Text("Service Account Key (JSON)") | ||
| .font(.system(size: 13, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| filePickerButton( | ||
| fileName: gcpServiceAccountFileName, | ||
| prompt: "Select Service Account JSON File", | ||
| onPick: { pickGCPServiceAccountFile() }, | ||
| onClear: { | ||
| state.gcpServiceAccountKey = "" | ||
| gcpServiceAccountFileName = "" | ||
| } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| var awsInlineFields: some View { | ||
| VStack(spacing: VSpacing.sm) { | ||
| awsSetupBlurb | ||
|
|
||
| VStack(alignment: .leading, spacing: VSpacing.xs) { | ||
| Text("IAM Role ARN") | ||
| .font(.system(size: 13, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| TextField("arn:aws:iam::123456789012:role/VellumAssistantRole", text: $state.awsRoleArn) | ||
| .textFieldStyle(.plain) | ||
| .font(.system(size: 14, weight: .medium, design: .monospaced)) | ||
| .foregroundColor(VColor.contentDefault) | ||
| .padding(.horizontal, VSpacing.lg) | ||
| .padding(.vertical, VSpacing.md) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: VRadius.lg) | ||
| .stroke(VColor.borderBase, lineWidth: 1) | ||
| ) | ||
| .focused($arnFieldFocused) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| var customHardwareInlineFields: some View { | ||
| VStack(spacing: VSpacing.sm) { | ||
| customHardwareSetupBlurb | ||
|
|
||
| VStack(alignment: .leading, spacing: VSpacing.xs) { | ||
| Text("QR Code Image") | ||
| .font(.system(size: 13, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| filePickerButton( | ||
| fileName: qrCodeImageFileName, | ||
| prompt: "Select QR Code PNG", | ||
| onPick: { pickQRCodeImageFile() }, | ||
| onClear: { | ||
| state.customQRCodeImageData = Data() | ||
| qrCodeImageFileName = "" | ||
| } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Setup Blurbs | ||
|
|
||
| var gcpSetupBlurb: some View { | ||
| VStack(alignment: .leading, spacing: VSpacing.sm) { | ||
| Text("Before continuing, set up the following in the Google Cloud Console:") | ||
| .font(.system(size: 13, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| VStack(alignment: .leading, spacing: VSpacing.xs) { | ||
| setupStep("1. Create or select a GCP project with the Compute Engine API enabled.") | ||
| setupStep("2. Create a Service Account with the Compute Admin role.") | ||
| setupStep("3. Generate a JSON key for the service account and download it.") | ||
| } | ||
| Link(destination: URL(string: "https://console.cloud.google.com/iam-admin/serviceaccounts")!) { | ||
| Text("Open Google Cloud Console") | ||
| .font(.system(size: 12, weight: .medium)) | ||
| .foregroundColor(VColor.primaryBase) | ||
| } | ||
| .pointerCursor() | ||
| } | ||
| .padding(VSpacing.md) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .textSelection(.enabled) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: VRadius.lg) | ||
| .fill(VColor.surfaceActive) | ||
| ) | ||
| } | ||
|
|
||
| var awsSetupBlurb: some View { | ||
| VStack(alignment: .leading, spacing: VSpacing.sm) { | ||
| Text("Before continuing, set up the following in the AWS Console:") | ||
| .font(.system(size: 13, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| VStack(alignment: .leading, spacing: VSpacing.xs) { | ||
| setupStep("1. Create an IAM role with EC2 full access permissions (e.g., AmazonEC2FullAccess).") | ||
| setupStep("2. Configure the role's trust policy to allow Vellum to assume it.") | ||
| setupStep("3. Ensure your account has a default VPC in the target region.") | ||
| } | ||
| Link(destination: URL(string: "https://console.aws.amazon.com/iam/home#/roles")!) { | ||
| Text("Open AWS IAM Console") | ||
| .font(.system(size: 12, weight: .medium)) | ||
| .foregroundColor(VColor.primaryBase) | ||
| } | ||
| .pointerCursor() | ||
| } | ||
| .padding(VSpacing.md) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .textSelection(.enabled) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: VRadius.lg) | ||
| .fill(VColor.surfaceActive) | ||
| ) | ||
| } | ||
|
|
||
| var customHardwareSetupBlurb: some View { | ||
| VStack(alignment: .leading, spacing: VSpacing.sm) { | ||
| Text("Set up your Mac mini, then upload the QR code:") | ||
| .font(.system(size: 13, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| VStack(alignment: .leading, spacing: VSpacing.xs) { | ||
| setupStep("1. On your Mac mini, run: curl -fsSL https://assistant.vellum.ai/install.sh | bash") | ||
| setupStep("2. Upload the QR code PNG generated by the install script below.") | ||
| } | ||
| } | ||
| .padding(VSpacing.md) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .textSelection(.enabled) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: VRadius.lg) | ||
| .fill(VColor.surfaceActive) | ||
| ) | ||
| } | ||
|
|
||
| func setupStep(_ text: String) -> some View { | ||
| Text(text) | ||
| .font(.system(size: 12)) | ||
| .foregroundColor(VColor.contentTertiary) | ||
| } | ||
|
|
||
| static var gcpZones: [String] { | ||
| [ | ||
| "us-central1-a", | ||
| "us-east1-b", | ||
| "us-east4-a", | ||
| "us-west1-a", | ||
| "us-west2-a", | ||
| ] | ||
| } | ||
|
|
||
| // MARK: - File Picker UI | ||
|
|
||
| @ViewBuilder | ||
| func filePickerButton( | ||
| fileName: String, | ||
| prompt: String, | ||
| onPick: @escaping () -> Void, | ||
| onClear: @escaping () -> Void | ||
| ) -> some View { | ||
| if fileName.isEmpty { | ||
| Button(action: onPick) { | ||
| HStack(spacing: VSpacing.sm) { | ||
| VIconView(.filePlus, size: 14) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| Text(prompt) | ||
| .font(.system(size: 14, weight: .medium)) | ||
| .foregroundColor(VColor.contentSecondary) | ||
| } | ||
| .frame(maxWidth: .infinity) | ||
| .padding(.vertical, VSpacing.lg) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: VRadius.lg) | ||
| .stroke(VColor.borderBase, style: StrokeStyle(lineWidth: 1, dash: [6, 3])) | ||
| ) | ||
| } | ||
| .buttonStyle(.plain) | ||
| .pointerCursor() | ||
| } else { | ||
| HStack(spacing: VSpacing.sm) { | ||
| VIconView(.file, size: 14) | ||
| .foregroundColor(VColor.primaryBase) | ||
| Text(fileName) | ||
| .font(.system(size: 14, weight: .medium, design: .monospaced)) | ||
| .foregroundColor(VColor.contentDefault) | ||
| .lineLimit(1) | ||
| .truncationMode(.middle) | ||
| .textSelection(.enabled) | ||
| Spacer() | ||
| Button(action: onClear) { | ||
| VIconView(.circleX, size: 14) | ||
| .foregroundColor(VColor.contentTertiary) | ||
| } | ||
| .buttonStyle(.plain) | ||
| .pointerCursor() | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
| .accessibilityLabel("Remove file") | ||
| } | ||
| .padding(.horizontal, VSpacing.lg) | ||
| .padding(.vertical, VSpacing.md) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: VRadius.lg) | ||
| .stroke(VColor.borderBase, lineWidth: 1) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - File Picking | ||
|
|
||
| func pickGCPServiceAccountFile() { | ||
| let panel = NSOpenPanel() | ||
| panel.title = "Select Service Account JSON File" | ||
| panel.allowedContentTypes = [UTType.json] | ||
| panel.allowsMultipleSelection = false | ||
| panel.canChooseDirectories = false | ||
|
|
||
| if panel.runModal() == .OK, let url = panel.url { | ||
| do { | ||
| let contents = try String(contentsOf: url, encoding: .utf8) | ||
| state.gcpServiceAccountKey = contents | ||
| gcpServiceAccountFileName = url.lastPathComponent | ||
| } catch { | ||
| state.gcpServiceAccountKey = "" | ||
| gcpServiceAccountFileName = "" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func pickQRCodeImageFile() { | ||
| let panel = NSOpenPanel() | ||
| panel.title = "Select QR Code PNG" | ||
| panel.allowedContentTypes = [UTType.png, UTType.image] | ||
| panel.allowsMultipleSelection = false | ||
| panel.canChooseDirectories = false | ||
|
|
||
| if panel.runModal() == .OK, let url = panel.url { | ||
| do { | ||
| let data = try Data(contentsOf: url) | ||
| state.customQRCodeImageData = data | ||
| qrCodeImageFileName = url.lastPathComponent | ||
| } catch { | ||
| state.customQRCodeImageData = Data() | ||
| qrCodeImageFileName = "" | ||
| } | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 Significant code duplication between CloudCredentialsStepView and new APIKeyStepView+CloudFields
The new
APIKeyStepView+CloudFields.swiftduplicates substantial UI code from the existingCloudCredentialsStepView.swift— the GCP fields, AWS fields, custom hardware fields, setup blurbs, file picker button, and file picking functions are nearly identical between the two files. This creates a maintenance burden where bug fixes or UI changes need to be applied in two places. Consider extracting shared components (e.g.,GCPCredentialFields,AWSCredentialFields, shared file picker, shared setup blurbs) that both views can reuse. Theclients/AGENTS.mdrules state: "Do not create one-off UI elements that duplicate existing design system components."Was this helpful? React with 👍 or 👎 to provide feedback.