diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts
index a4b1fed820e..c480deb7e9f 100644
--- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts
@@ -170,6 +170,7 @@ describe('connect.schema:steps resolution', () => {
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined()
+ expect(steps.find((s) => s.id === 'shadcn-env')).toBeDefined()
expect(steps.find((s) => s.id === 'shadcn-explore')).toBeDefined()
})
@@ -186,6 +187,7 @@ describe('connect.schema:steps resolution', () => {
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined()
+ expect(steps.find((s) => s.id === 'shadcn-env')).toBeDefined()
})
test('should resolve default steps for other frameworks', () => {
@@ -337,6 +339,14 @@ describe('connect.schema:step content paths', () => {
expect(exploreStep?.content).toBe('steps/shadcn/explore')
})
+ test('shadcn env step should have valid content path', () => {
+ const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
+ const steps = resolveSteps(connectSchema, state)
+ const envStep = steps.find((s) => s.id === 'shadcn-env')
+
+ expect(envStep?.content).toBe('steps/shadcn/env')
+ })
+
test('direct connection step should have valid content path', () => {
const state: ConnectState = { mode: 'direct' }
const steps = resolveSteps(connectSchema, state)
diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts
index 41723910b08..32f0f6f2460 100644
--- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts
@@ -53,6 +53,13 @@ const frameworkShadcnStep: StepDefinition = {
content: 'steps/shadcn/command',
}
+const frameworkShadcnEnvStep: StepDefinition = {
+ id: 'shadcn-env',
+ title: 'Set env variables',
+ description: 'Add the following values to your env file.',
+ content: 'steps/shadcn/env',
+}
+
const frameworkShadcnExploreStep: StepDefinition = {
id: 'shadcn-explore',
title: 'Check out more UI components',
@@ -297,6 +304,7 @@ export const connectSchema: ConnectSchema = {
true: [
frameworkInstallStep,
frameworkShadcnStep,
+ frameworkShadcnEnvStep,
frameworkShadcnExploreStep,
skillsInstallStep,
],
@@ -308,6 +316,7 @@ export const connectSchema: ConnectSchema = {
true: [
frameworkInstallStep,
frameworkShadcnStep,
+ frameworkShadcnEnvStep,
frameworkShadcnExploreStep,
skillsInstallStep,
],
diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/env/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/env/content.tsx
new file mode 100644
index 00000000000..6bef222fdcd
--- /dev/null
+++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/env/content.tsx
@@ -0,0 +1,52 @@
+import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
+
+import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types'
+
+function getEnvFile(
+ state: StepContentProps['state'],
+ projectKeys: StepContentProps['projectKeys']
+) {
+ if (state.framework === 'nextjs') {
+ return {
+ name: '.env.local',
+ language: 'bash',
+ code: [
+ `NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
+ projectKeys.publishableKey
+ ? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
+ : `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
+ '',
+ ].join('\n'),
+ }
+ }
+
+ if (state.framework === 'react') {
+ const isCreateReactApp = state.frameworkVariant === 'create-react-app'
+ const envFileName = isCreateReactApp ? '.env.local' : '.env'
+ const keyPrefix = isCreateReactApp ? 'REACT_APP' : 'VITE'
+
+ return {
+ name: envFileName,
+ language: 'bash',
+ code: [
+ `${keyPrefix}_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
+ projectKeys.publishableKey
+ ? `${keyPrefix}_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
+ : `${keyPrefix}_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
+ '',
+ ].join('\n'),
+ }
+ }
+
+ return null
+}
+
+function ShadcnEnvContent({ state, projectKeys }: StepContentProps) {
+ const envFile = getEnvFile(state, projectKeys)
+
+ if (!envFile) return null
+
+ return
+}
+
+export default ShadcnEnvContent
diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts
index dbd40cd39a8..a882d8c77de 100644
--- a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts
@@ -371,6 +371,7 @@ describe('useConnectState', () => {
const stepIds = result.current.resolvedSteps.map((s) => s.id)
expect(stepIds).toContain('shadcn-add')
+ expect(stepIds).toContain('shadcn-env')
})
})
diff --git a/packages/ui-patterns/src/MultipleCodeBlock/index.test.tsx b/packages/ui-patterns/src/MultipleCodeBlock/index.test.tsx
new file mode 100644
index 00000000000..fa9025fa192
--- /dev/null
+++ b/packages/ui-patterns/src/MultipleCodeBlock/index.test.tsx
@@ -0,0 +1,38 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { MultipleCodeBlock } from './index'
+
+describe('MultipleCodeBlock', () => {
+ it('resets the active tab when the available files change in uncontrolled mode', () => {
+ const { rerender } = render(
+
+ )
+
+ expect(screen.getByRole('tab', { selected: true })).toHaveTextContent('.env.local')
+ expect(screen.getByText('NEXT_PUBLIC_SUPABASE_URL=https://next.example')).toBeVisible()
+
+ rerender(
+
+ )
+
+ expect(screen.getByRole('tab', { selected: true })).toHaveTextContent('.env')
+ expect(screen.getByText('VITE_SUPABASE_URL=https://react.example')).toBeVisible()
+ })
+})
diff --git a/packages/ui-patterns/src/MultipleCodeBlock/index.tsx b/packages/ui-patterns/src/MultipleCodeBlock/index.tsx
index f9bb257ca1e..19a75e25542 100644
--- a/packages/ui-patterns/src/MultipleCodeBlock/index.tsx
+++ b/packages/ui-patterns/src/MultipleCodeBlock/index.tsx
@@ -1,3 +1,5 @@
+import { useEffect, useState } from 'react'
+
import {
CodeBlock,
CodeBlockLang,
@@ -102,17 +104,37 @@ export const MultipleCodeBlock = ({ files, value, onValueChange }: MultipleCodeB
}
const defaultValue = files[0]?.name ?? ''
+ const isControlled = value !== undefined
+ const [internalValue, setInternalValue] = useState(defaultValue)
const trimmedFiles = files.map((file) => ({
...file,
code: typeof file.code === 'string' ? file.code.trim() : file.code,
}))
+ useEffect(() => {
+ if (isControlled) return
+
+ setInternalValue((currentValue) => {
+ const currentValueExists = files.some((file) => file.name === currentValue)
+ return currentValueExists ? currentValue : defaultValue
+ })
+ }, [defaultValue, files, isControlled])
+
+ const activeValue = isControlled ? value : internalValue
+
+ const handleValueChange = (nextValue: string) => {
+ if (!isControlled) {
+ setInternalValue(nextValue)
+ }
+
+ onValueChange?.(nextValue)
+ }
+
return (