Compare commits

..

5 Commits

Author SHA1 Message Date
Piyush Jain eeda5ac361 chore(one-click-setup): add minio support 2025-03-28 12:55:30 +05:30
Piyush Jain c8a1f6f0de fix: once-clik-k8s setup and helm chart redis
- update chart to add minio dependency
- fix variable issues
2025-03-27 14:55:49 +05:30
Piyush Jain 4fc81cb2a2 fix: once-clik-k8s setup and helm chart redis
- fix variables
2025-03-27 14:17:38 +05:30
Piyush Jain e1f958c905 fix: once-clik-k8s setup and helm chart redis
- fixes redis auth issues with self hosted redis
- fixes few one-click-setup
2025-03-27 13:15:54 +05:30
Matthias Nannt 4b9a819abf feat: move one-click setup to k8s 2025-03-26 23:29:05 +09:00
1613 changed files with 41918 additions and 83344 deletions
+17 -18
View File
@@ -80,9 +80,6 @@ S3_ENDPOINT_URL=
# Force path style for S3 compatible storage (0 for disabled, 1 for enabled) # Force path style for S3 compatible storage (0 for disabled, 1 for enabled)
S3_FORCE_PATH_STYLE=0 S3_FORCE_PATH_STYLE=0
# Set this URL to add a custom domain to your survey links(default is WEBAPP_URL)
# SURVEY_URL=https://survey.example.com
##################### #####################
# Disable Features # # Disable Features #
##################### #####################
@@ -93,6 +90,10 @@ EMAIL_VERIFICATION_DISABLED=1
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too. # Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
PASSWORD_RESET_DISABLED=1 PASSWORD_RESET_DISABLED=1
# Signup. Disable the ability for new users to create an account.
# Note: This variable is only available to the SaaS setup of Formbricks Cloud. Signup is disable by default for self-hosting.
# SIGNUP_DISABLED=1
# Email login. Disable the ability for users to login with email. # Email login. Disable the ability for users to login with email.
# EMAIL_AUTH_DISABLED=1 # EMAIL_AUTH_DISABLED=1
@@ -113,13 +114,9 @@ IMPRINT_URL=
IMPRINT_ADDRESS= IMPRINT_ADDRESS=
# Configure Turnstile in signup flow # Configure Turnstile in signup flow
# TURNSTILE_SITE_KEY= # NEXT_PUBLIC_TURNSTILE_SITE_KEY=
# TURNSTILE_SECRET_KEY= # TURNSTILE_SECRET_KEY=
# Google reCAPTCHA v3 keys
RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET_KEY=
# Configure Github Login # Configure Github Login
GITHUB_ID= GITHUB_ID=
GITHUB_SECRET= GITHUB_SECRET=
@@ -154,6 +151,11 @@ NOTION_OAUTH_CLIENT_SECRET=
STRIPE_SECRET_KEY= STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
# Configure Formbricks usage within Formbricks
NEXT_PUBLIC_FORMBRICKS_API_HOST=
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
# Oauth credentials for Google sheet integration # Oauth credentials for Google sheet integration
GOOGLE_SHEETS_CLIENT_ID= GOOGLE_SHEETS_CLIENT_ID=
GOOGLE_SHEETS_CLIENT_SECRET= GOOGLE_SHEETS_CLIENT_SECRET=
@@ -172,9 +174,8 @@ ENTERPRISE_LICENSE_KEY=
# Automatically assign new users to a specific organization and role within that organization # Automatically assign new users to a specific organization and role within that organization
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn) # Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
# (Role Management is an Enterprise feature) # (Role Management is an Enterprise feature)
# DEFAULT_ORGANIZATION_ID=
# DEFAULT_ORGANIZATION_ROLE=owner # DEFAULT_ORGANIZATION_ROLE=owner
# AUTH_SSO_DEFAULT_TEAM_ID=
# AUTH_SKIP_INVITE_FOR_SSO=
# Send new users to Brevo # Send new users to Brevo
# BREVO_API_KEY= # BREVO_API_KEY=
@@ -203,6 +204,12 @@ UNKEY_ROOT_KEY=
# Disable custom cache handler if necessary (e.g. if deployed on Vercel) # Disable custom cache handler if necessary (e.g. if deployed on Vercel)
# CUSTOM_CACHE_DISABLED=1 # CUSTOM_CACHE_DISABLED=1
# Azure AI settings
# AI_AZURE_RESSOURCE_NAME=
# AI_AZURE_API_KEY=
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
# AI_AZURE_LLM_DEPLOYMENT_ID=
# INTERCOM_APP_ID= # INTERCOM_APP_ID=
# INTERCOM_SECRET_KEY= # INTERCOM_SECRET_KEY=
@@ -210,11 +217,3 @@ UNKEY_ROOT_KEY=
# PROMETHEUS_ENABLED= # PROMETHEUS_ENABLED=
# PROMETHEUS_EXPORTER_PORT= # PROMETHEUS_EXPORTER_PORT=
# The SENTRY_DSN is used for error tracking and performance monitoring with Sentry.
# SENTRY_DSN=
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
# It's used automatically by Sentry during the build for authentication when uploading source maps.
# SENTRY_AUTH_TOKEN=
# Disable the user management from UI
# DISABLE_USER_MANAGEMENT=1
+1 -12
View File
@@ -8,14 +8,6 @@ on:
required: false required: false
default: "0" default: "0"
inputs:
turbo_token:
description: "Turborepo token"
required: false
turbo_team:
description: "Turborepo team"
required: false
runs: runs:
using: "composite" using: "composite"
steps: steps:
@@ -64,14 +56,11 @@ runs:
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env - name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
run: | run: |
RANDOM_KEY=$(openssl rand -hex 32) RANDOM_KEY=$(openssl rand -hex 32)
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
echo "E2E_TESTING=${{ inputs.e2e_testing_mode }}" >> .env echo "E2E_TESTING=${{ inputs.e2e_testing_mode }}" >> .env
shell: bash shell: bash
- run: | - run: |
pnpm build --filter=@formbricks/web... pnpm build --filter=@formbricks/web...
if: steps.cache-build.outputs.cache-hit != 'true' if: steps.cache-build.outputs.cache-hit != 'true'
shell: bash shell: bash
env:
TURBO_TOKEN: ${{ inputs.turbo_token }}
TURBO_TEAM: ${{ inputs.turbo_team }}
-31
View File
@@ -1,31 +0,0 @@
# Testing Instructions
When generating test files inside the "/app/web" path, follow these rules:
- You are an experienced senior software engineer
- Use vitest
- Ensure 100% code coverage
- Add as few comments as possible
- The test file should be located in the same folder as the original file
- Use the `test` function instead of `it`
- Follow the same test pattern used for other files in the package where the file is located
- All imports should be at the top of the file, not inside individual tests
- For mocking inside "test" blocks use "vi.mocked"
- Add the original file path to the "test.coverage.include"array in the "apps/web/vite.config.mts" file. Do this only when the test file is created.
- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file
- When using "screen.getByText" check for the tolgee string if it is being used in the file.
- The types for mocked variables can be found in the "packages/types" path. Be sure that every imported type exists before using it. Don't create types that are not already in the codebase.
- When mocking data check if the properties added are part of the type of the object being mocked. Only specify known properties, don't use properties that are not part of the type.
If it's a test for a ".tsx" file, follow these extra instructions:
- Add this code inside the "describe" block and before any test:
afterEach(() => {
cleanup();
});
- The "afterEach" function should only have the "cleanup()" line inside it and should be adde to the "vitest" imports.
- For click events, import userEvent from "@testing-library/user-event"
- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components.
- You don't need to mock @tolgee/react
-84
View File
@@ -1,84 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # For pnpm monorepos, use npm ecosystem
directory: "/" # Root package.json
schedule:
interval: "weekly"
versioning-strategy: increase
# Apps directory packages
- package-ecosystem: "npm"
directory: "/apps/demo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/apps/demo-react-native"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/apps/storybook"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/apps/web"
schedule:
interval: "weekly"
# Packages directory
- package-ecosystem: "npm"
directory: "/packages/database"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/lib"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/types"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/config-eslint"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/config-prettier"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/config-typescript"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/js-core"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/surveys"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/logger"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
@@ -19,12 +19,12 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- name: Apply labels from linked issue to PR - name: Apply labels from linked issue to PR
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@211cb3fefb35a799baa5156f9321bb774fe56294 # v5.2.0
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
+3 -5
View File
@@ -4,7 +4,7 @@ on:
permissions: permissions:
contents: read contents: read
jobs: jobs:
build: build:
name: Build Formbricks-web name: Build Formbricks-web
@@ -13,11 +13,11 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/dangerous-git-checkout
- name: Build & Cache Web Binaries - name: Build & Cache Web Binaries
@@ -25,5 +25,3 @@ jobs:
id: cache-build-web id: cache-build-web
with: with:
e2e_testing_mode: "0" e2e_testing_mode: "0"
turbo_token: ${{ secrets.TURBO_TOKEN }}
turbo_team: ${{ vars.TURBO_TEAM }}
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
@@ -0,0 +1,33 @@
name: Cron - Survey status update
on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs "At 00:00." (see https://crontab.guru)
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
cron-weeklySummary:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |
curl ${{ env.APP_URL }}/api/cron/survey-status \
-X POST \
-H 'content-type: application/json' \
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
--fail
+33
View File
@@ -0,0 +1,33 @@
name: Cron - Weekly summary
on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
- cron: "0 8 * * 1"
permissions:
contents: read
jobs:
cron-weeklySummary:
permissions:
contents: read
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
with:
egress-policy: audit
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |
curl ${{ env.APP_URL }}/api/cron/weekly-summary \
-X POST \
-H 'content-type: application/json' \
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
--fail
+2 -2
View File
@@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0 uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
+2 -38
View File
@@ -12,13 +12,6 @@ on:
required: false required: false
type: string type: string
default: 'ghcr.io/formbricks/formbricks' default: 'ghcr.io/formbricks/formbricks'
ENVIRONMENT:
description: 'The environment to deploy to'
required: true
type: choice
options:
- stage
- prod
workflow_call: workflow_call:
inputs: inputs:
VERSION: VERSION:
@@ -30,10 +23,6 @@ on:
required: false required: false
type: string type: string
default: 'ghcr.io/formbricks/formbricks' default: 'ghcr.io/formbricks/formbricks'
ENVIRONMENT:
description: 'The environment to deploy to'
required: true
type: string
permissions: permissions:
id-token: write id-token: write
@@ -44,14 +33,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
- name: Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:github
- name: Configure AWS Credentials - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
@@ -66,8 +48,6 @@ jobs:
AWS_REGION: eu-central-1 AWS_REGION: eu-central-1
- uses: helmfile/helmfile-action@v2 - uses: helmfile/helmfile-action@v2
name: Deploy Formbricks Cloud Prod
if: (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && github.event.inputs.ENVIRONMENT == 'prod'
env: env:
VERSION: ${{ inputs.VERSION }} VERSION: ${{ inputs.VERSION }}
REPOSITORY: ${{ inputs.REPOSITORY }} REPOSITORY: ${{ inputs.REPOSITORY }}
@@ -78,23 +58,7 @@ jobs:
helm-plugins: > helm-plugins: >
https://github.com/databus23/helm-diff, https://github.com/databus23/helm-diff,
https://github.com/jkroepke/helm-secrets https://github.com/jkroepke/helm-secrets
helmfile-args: apply -l environment=prod helmfile-args: apply
helmfile-auto-init: "false"
helmfile-workdirectory: infra/formbricks-cloud-helm
- uses: helmfile/helmfile-action@v2
name: Deploy Formbricks Cloud Stage
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ENVIRONMENT == 'stage'
env:
VERSION: ${{ inputs.VERSION }}
REPOSITORY: ${{ inputs.REPOSITORY }}
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
with:
helm-plugins: >
https://github.com/databus23/helm-diff,
https://github.com/jkroepke/helm-secrets
helmfile-args: apply -l environment=stage
helmfile-auto-init: "false" helmfile-auto-init: "false"
helmfile-workdirectory: infra/formbricks-cloud-helm helmfile-workdirectory: infra/formbricks-cloud-helm
@@ -1,167 +0,0 @@
name: Docker Build Validation
on:
pull_request:
branches:
- main
merge_group:
branches:
- main
workflow_dispatch:
permissions:
contents: read
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
validate-docker-build:
name: Validate Docker Build
runs-on: ubuntu-latest
# Add PostgreSQL service container
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: formbricks
ports:
- 5432:5432
# Health check to ensure PostgreSQL is ready before using it
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout Repository
uses: actions/checkout@v4.2.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker Image
uses: docker/build-push-action@v6
with:
context: .
file: ./apps/web/Dockerfile
push: false
load: true
tags: formbricks-test:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
secrets: |
database_url=${{ secrets.DUMMY_DATABASE_URL }}
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
- name: Verify PostgreSQL Connection
run: |
echo "Verifying PostgreSQL connection..."
# Install PostgreSQL client to test connection
sudo apt-get update && sudo apt-get install -y postgresql-client
# Test connection using psql
PGPASSWORD=test psql -h localhost -U test -d formbricks -c "\dt" || echo "Failed to connect to PostgreSQL"
# Show network configuration
echo "Network configuration:"
ip addr show
netstat -tulpn | grep 5432 || echo "No process listening on port 5432"
- name: Test Docker Image with Health Check
shell: bash
run: |
echo "🧪 Testing if the Docker image starts correctly..."
# Add extra docker run args to support host.docker.internal on Linux
DOCKER_RUN_ARGS="--add-host=host.docker.internal:host-gateway"
# Start the container with host.docker.internal pointing to the host
docker run --name formbricks-test \
$DOCKER_RUN_ARGS \
-p 3000:3000 \
-e DATABASE_URL="postgresql://test:test@host.docker.internal:5432/formbricks" \
-e ENCRYPTION_KEY="${{ secrets.DUMMY_ENCRYPTION_KEY }}" \
-d formbricks-test:${{ github.sha }}
# Give it more time to start up
echo "Waiting 45 seconds for application to start..."
sleep 45
# Check if the container is running
if [ "$(docker inspect -f '{{.State.Running}}' formbricks-test)" != "true" ]; then
echo "❌ Container failed to start properly!"
docker logs formbricks-test
exit 1
else
echo "✅ Container started successfully!"
fi
# Try connecting to PostgreSQL from inside the container
echo "Testing PostgreSQL connection from inside container..."
docker exec formbricks-test sh -c 'apt-get update && apt-get install -y postgresql-client && PGPASSWORD=test psql -h host.docker.internal -U test -d formbricks -c "\dt" || echo "Failed to connect to PostgreSQL from container"'
# Try to access the health endpoint
echo "🏥 Testing /health endpoint..."
MAX_RETRIES=10
RETRY_COUNT=0
HEALTH_CHECK_SUCCESS=false
set +e # Disable exit on error to allow for retries
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Attempt $RETRY_COUNT of $MAX_RETRIES..."
# Show container logs before each attempt to help debugging
if [ $RETRY_COUNT -gt 1 ]; then
echo "📋 Current container logs:"
docker logs --tail 20 formbricks-test
fi
# Get detailed curl output for debugging
HTTP_OUTPUT=$(curl -v -s -m 30 http://localhost:3000/health 2>&1)
CURL_EXIT_CODE=$?
echo "Curl exit code: $CURL_EXIT_CODE"
echo "Curl output: $HTTP_OUTPUT"
if [ $CURL_EXIT_CODE -eq 0 ]; then
STATUS_CODE=$(echo "$HTTP_OUTPUT" | grep -oP "HTTP/\d(\.\d)? \K\d+")
echo "Status code detected: $STATUS_CODE"
if [ "$STATUS_CODE" = "200" ]; then
echo "✅ Health check successful!"
HEALTH_CHECK_SUCCESS=true
break
else
echo "❌ Health check returned non-200 status code: $STATUS_CODE"
fi
else
echo "❌ Curl command failed with exit code: $CURL_EXIT_CODE"
fi
echo "Waiting 15 seconds before next attempt..."
sleep 15
done
# Show full container logs for debugging
echo "📋 Full container logs:"
docker logs formbricks-test
# Clean up the container
echo "🧹 Cleaning up..."
docker rm -f formbricks-test
# Exit with failure if health check did not succeed
if [ "$HEALTH_CHECK_SUCCESS" != "true" ]; then
echo "❌ Health check failed after $MAX_RETRIES attempts"
exit 1
fi
echo "✨ Docker validation complete - all checks passed!"
+2 -4
View File
@@ -16,8 +16,6 @@ on:
env: env:
TELEMETRY_DISABLED: 1 TELEMETRY_DISABLED: 1
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
permissions: permissions:
id-token: write id-token: write
@@ -46,11 +44,11 @@ jobs:
--health-retries=5 --health-retries=5
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x - name: Setup Node.js 20.x
-1
View File
@@ -31,4 +31,3 @@ jobs:
- helm-chart-release - helm-chart-release
with: with:
VERSION: ${{ needs.docker-build.outputs.VERSION }} VERSION: ${{ needs.docker-build.outputs.VERSION }}
ENVIRONMENT: "prod"
+27
View File
@@ -0,0 +1,27 @@
name: "Pull Request Labeler"
on:
- pull_request_target
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
labeler:
name: Pull Request Labeler
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
sync-labels: ""
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
statuses: write statuses: write
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
with: with:
egress-policy: audit egress-policy: audit
- name: fail if conditional jobs failed - name: fail if conditional jobs failed
+56
View File
@@ -0,0 +1,56 @@
name: Release Changesets
on:
workflow_dispatch:
#push:
# branches:
# - main
permissions:
contents: write
pull-requests: write
packages: write
concurrency: ${{ github.workflow }}-${{ github.ref }}
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
timeout-minutes: 15
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout Repo
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: Setup Node.js 18.x
uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2
with:
node-version: 18.x
- name: Install pnpm
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4
- name: Install Dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9
with:
# This expects you to have a script called release which does a build for your packages and calls changeset publish
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -15,6 +15,7 @@ env:
IMAGE_NAME: ${{ github.repository }}-experimental IMAGE_NAME: ${{ github.repository }}-experimental
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
permissions: permissions:
contents: read contents: read
@@ -31,12 +32,12 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Set up Depot CLI - name: Set up Depot CLI
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
@@ -45,13 +46,13 @@ jobs:
# https://github.com/sigstore/cosign-installer # https://github.com/sigstore/cosign-installer
- name: Install cosign - name: Install cosign
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
# Login against a Docker registry except on PR # Login against a Docker registry except on PR
# https://github.com/docker/login-action # https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }} - name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -79,9 +80,8 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
secrets: | cache-from: type=gha
database_url=${{ secrets.DUMMY_DATABASE_URL }} cache-to: type=gha,mode=max
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
# Sign the resulting Docker image digest except on PRs. # Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker # This will only write to the public Rekor transparency log when the Docker
+7 -7
View File
@@ -19,6 +19,7 @@ env:
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
permissions: permissions:
contents: read contents: read
@@ -38,12 +39,12 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Get Release Tag - name: Get Release Tag
id: extract_release_tag id: extract_release_tag
@@ -65,13 +66,13 @@ jobs:
# https://github.com/sigstore/cosign-installer # https://github.com/sigstore/cosign-installer
- name: Install cosign - name: Install cosign
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
# Login against a Docker registry except on PR # Login against a Docker registry except on PR
# https://github.com/docker/login-action # https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }} - name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -99,9 +100,8 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
secrets: | cache-from: type=gha
database_url=${{ secrets.DUMMY_DATABASE_URL }} cache-to: type=gha,mode=max
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
# Sign the resulting Docker image digest except on PRs. # Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker # This will only write to the public Rekor transparency log when the Docker
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
+2 -2
View File
@@ -35,12 +35,12 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
persist-credentials: false persist-credentials: false
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
+4 -4
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
@@ -23,10 +23,10 @@ jobs:
with: with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Setup Node.js 22.x - name: Setup Node.js 20.x
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with: with:
node-version: 22.x node-version: 20.x
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
@@ -48,7 +48,7 @@ jobs:
run: | run: |
pnpm test:coverage pnpm test:coverage
- name: SonarQube Scan - name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -3,21 +3,16 @@ name: 'Terraform'
on: on:
workflow_dispatch: workflow_dispatch:
# TODO: enable it back when migration is completed. # TODO: enable it back when migration is completed.
push: # push:
branches: # branches:
- main # - main
paths: # pull_request:
- "infra/terraform/**" # branches:
pull_request: # - main
branches:
- main
paths:
- "infra/terraform/**"
permissions: permissions:
id-token: write id-token: write
contents: write contents: write
pull-requests: write
jobs: jobs:
terraform: terraform:
@@ -26,20 +21,13 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:github
- name: Configure AWS Credentials - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with: with:
@@ -70,17 +58,18 @@ jobs:
run: terraform plan -out .planfile run: terraform plan -out .planfile
working-directory: infra/terraform working-directory: infra/terraform
- name: Post PR comment # - name: Post PR comment
uses: borchero/terraform-plan-comment@3399d8dbae8b05185e815e02361ede2949cd99c4 # v2.4.0 # uses: borchero/terraform-plan-comment@3399d8dbae8b05185e815e02361ede2949cd99c4 # v2.4.0
if: always() && github.ref != 'refs/heads/main' && (steps.plan.outcome == 'success' || steps.plan.outcome == 'failure') # if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure')
with: # with:
token: ${{ github.token }} # token: ${{ github.token }}
planfile: .planfile # planfile: .planfile
working-directory: "infra/terraform" # working-directory: "infra/terraform"
# skip-comment: true
- name: Terraform Apply - name: Terraform Apply
id: apply id: apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push' # if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply .planfile run: terraform apply .planfile
working-directory: "infra/terraform" working-directory: "infra/terraform"
+2 -2
View File
@@ -14,11 +14,11 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x - name: Setup Node.js 20.x
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
@@ -18,7 +18,7 @@ jobs:
if: github.event.action == 'opened' if: github.event.action == 'opened'
steps: steps:
- name: Harden the runner (Audit all outbound calls) - name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
-1
View File
@@ -72,4 +72,3 @@ infra/terraform/.terraform/
# IntelliJ IDEA # IntelliJ IDEA
/.idea/ /.idea/
/*.iml /*.iml
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata
+1 -1
View File
@@ -16,6 +16,6 @@ if [ -f branch.json ]; then
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set" echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
else else
pnpm run tolgee-pull pnpm run tolgee-pull
git add apps/web/locales git add packages/lib/messages
fi fi
fi fi
+7 -7
View File
@@ -4,33 +4,33 @@
"patterns": ["./apps/web/**/*.ts?(x)"], "patterns": ["./apps/web/**/*.ts?(x)"],
"projectId": 10304, "projectId": 10304,
"pull": { "pull": {
"path": "./apps/web/locales" "path": "./packages/lib/messages"
}, },
"push": { "push": {
"files": [ "files": [
{ {
"language": "en-US", "language": "en-US",
"path": "./apps/web/locales/en-US.json" "path": "./packages/lib/messages/en-US.json"
}, },
{ {
"language": "de-DE", "language": "de-DE",
"path": "./apps/web/locales/de-DE.json" "path": "./packages/lib/messages/de-DE.json"
}, },
{ {
"language": "fr-FR", "language": "fr-FR",
"path": "./apps/web/locales/fr-FR.json" "path": "./packages/lib/messages/fr-FR.json"
}, },
{ {
"language": "pt-BR", "language": "pt-BR",
"path": "./apps/web/locales/pt-BR.json" "path": "./packages/lib/messages/pt-BR.json"
}, },
{ {
"language": "zh-Hant-TW", "language": "zh-Hant-TW",
"path": "./apps/web/locales/zh-Hant-TW.json" "path": "./packages/lib/messages/zh-Hant-TW.json"
}, },
{ {
"language": "pt-PT", "language": "pt-PT",
"path": "./apps/web/locales/pt-PT.json" "path": "./packages/lib/messages/pt-PT.json"
} }
], ],
"forceMode": "OVERRIDE" "forceMode": "OVERRIDE"
+1 -7
View File
@@ -1,10 +1,4 @@
{ {
"javascript.updateImportsOnFileMove.enabled": "always",
"sonarlint.connectedMode.project": {
"connectionId": "formbricks",
"projectKey": "formbricks_formbricks"
},
"typescript.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib"
"typescript.updateImportsOnFileMove.enabled": "always"
} }
+1 -1
View File
@@ -3,7 +3,7 @@ Copyright (c) 2024 Formbricks GmbH
Portions of this software are licensed as follows: Portions of this software are licensed as follows:
- All content that resides under the "apps/web/modules/ee" directory of this repository, if these directories exist, is licensed under the license defined in "apps/web/modules/ee/LICENSE". - All content that resides under the "apps/web/modules/ee" directory of this repository, if these directories exist, is licensed under the license defined in "apps/web/modules/ee/LICENSE".
- All content that resides under the "packages/js/", "packages/android/", "packages/ios/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages. - All content that resides under the "packages/js/", "packages/react-native/", "packages/android/", "packages/ios/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component. - All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below. - Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
+2
View File
@@ -0,0 +1,2 @@
EXPO_PUBLIC_APP_URL=http://192.168.0.197:3000
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=cm5p0cs7r000819182b32j0a1
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ["@formbricks/eslint-config/react.js"],
parserOptions: {
project: "tsconfig.json",
tsconfigRootDir: __dirname,
},
};
+35
View File
@@ -0,0 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
View File
+35
View File
@@ -0,0 +1,35 @@
{
"expo": {
"android": {
"adaptiveIcon": {
"backgroundColor": "#ffffff",
"foregroundImage": "./assets/adaptive-icon.png"
}
},
"assetBundlePatterns": ["**/*"],
"icon": "./assets/icon.png",
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Take pictures for certain activities.",
"NSMicrophoneUsageDescription": "Need microphone access for recording videos.",
"NSPhotoLibraryUsageDescription": "Select pictures for certain activities."
},
"supportsTablet": true
},
"jsEngine": "hermes",
"name": "react-native-demo",
"newArchEnabled": true,
"orientation": "portrait",
"slug": "react-native-demo",
"splash": {
"backgroundColor": "#ffffff",
"image": "./assets/splash.png",
"resizeMode": "contain"
},
"userInterfaceStyle": "light",
"version": "1.0.0",
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+6
View File
@@ -0,0 +1,6 @@
module.exports = function babel(api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
};
};
+7
View File
@@ -0,0 +1,7 @@
import { registerRootComponent } from "expo";
import { LogBox } from "react-native";
import App from "./src/app";
registerRootComponent(App);
LogBox.ignoreAllLogs();
+21
View File
@@ -0,0 +1,21 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const path = require("node:path");
const { getDefaultConfig } = require("expo/metro-config");
// Find the workspace root, this can be replaced with `find-yarn-workspace-root`
const workspaceRoot = path.resolve(__dirname, "../..");
const projectRoot = __dirname;
const config = getDefaultConfig(projectRoot);
// 1. Watch all files within the monorepo
config.watchFolders = [workspaceRoot];
// 2. Let Metro know where to resolve packages, and in what order
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;
module.exports = config;
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@formbricks/demo-react-native",
"version": "1.0.0",
"main": "./index.js",
"scripts": {
"dev": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject",
"clean": "rimraf .turbo node_modules .expo"
},
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/react-native": "workspace:*",
"@react-native-async-storage/async-storage": "2.1.0",
"expo": "52.0.28",
"expo-status-bar": "2.0.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.6",
"react-native-webview": "13.12.5"
},
"devDependencies": {
"@babel/core": "7.26.0",
"@types/react": "18.3.18",
"typescript": "5.7.2"
},
"private": true
}
+117
View File
@@ -0,0 +1,117 @@
import { StatusBar } from "expo-status-bar";
import React, { type JSX } from "react";
import { Button, LogBox, StyleSheet, Text, View } from "react-native";
import Formbricks, {
logout,
setAttribute,
setAttributes,
setLanguage,
setUserId,
track,
} from "@formbricks/react-native";
LogBox.ignoreAllLogs();
export default function App(): JSX.Element {
if (!process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID) {
throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required");
}
if (!process.env.EXPO_PUBLIC_APP_URL) {
throw new Error("EXPO_PUBLIC_APP_URL is required");
}
return (
<View style={styles.container}>
<Text>Formbricks React Native SDK Demo</Text>
<View
style={{
display: "flex",
flexDirection: "column",
gap: 10,
}}>
<Button
title="Trigger Code Action"
onPress={() => {
track("code").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error tracking event:", error);
});
}}
/>
<Button
title="Set User Id"
onPress={() => {
setUserId("random-user-id").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user id:", error);
});
}}
/>
<Button
title="Set User Attributess (multiple)"
onPress={() => {
setAttributes({
testAttr: "attr-test",
testAttr2: "attr-test-2",
testAttr3: "attr-test-3",
testAttr4: "attr-test-4",
}).catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user attributes:", error);
});
}}
/>
<Button
title="Set User Attributes (single)"
onPress={() => {
setAttribute("testSingleAttr", "testSingleAttr").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user attributes:", error);
});
}}
/>
<Button
title="Logout"
onPress={() => {
logout().catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error logging out:", error);
});
}}
/>
<Button
title="Set Language (de)"
onPress={() => {
setLanguage("de").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting language:", error);
});
}}
/>
</View>
<StatusBar style="auto" />
<Formbricks
appUrl={process.env.EXPO_PUBLIC_APP_URL as string}
environmentId={process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID as string}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
+6
View File
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true
},
"extends": "expo/tsconfig.base"
}
+5
View File
@@ -0,0 +1,5 @@
NEXT_PUBLIC_FORMBRICKS_API_HOST=http://localhost:3000
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=YOUR_ENVIRONMENT_ID
# Copy the environment ID for the URL of your Formbricks App and
# paste it above to connect your Formbricks App with the Demo App.
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ["@formbricks/eslint-config/next.js"],
parserOptions: {
project: "tsconfig.json",
tsconfigRootDir: __dirname,
},
};
+36
View File
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+13
View File
@@ -0,0 +1,13 @@
import { Sidebar } from "./sidebar";
export function LayoutApp({ children }: { children: React.ReactNode }): React.JSX.Element {
return (
<div className="min-h-full">
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
<Sidebar />
</div>
<div className="flex flex-1 flex-col lg:pl-64">{children}</div>
</div>
);
}
+65
View File
@@ -0,0 +1,65 @@
import {
ClockIcon,
CogIcon,
CreditCardIcon,
FileBarChartIcon,
HelpCircleIcon,
HomeIcon,
ScaleIcon,
ShieldCheckIcon,
UsersIcon,
} from "lucide-react";
import { classNames } from "../lib/utils";
const navigation = [
{ name: "Home", href: "#", icon: HomeIcon, current: true },
{ name: "History", href: "#", icon: ClockIcon, current: false },
{ name: "Balances", href: "#", icon: ScaleIcon, current: false },
{ name: "Cards", href: "#", icon: CreditCardIcon, current: false },
{ name: "Recipients", href: "#", icon: UsersIcon, current: false },
{ name: "Reports", href: "#", icon: FileBarChartIcon, current: false },
];
const secondaryNavigation = [
{ name: "Settings", href: "#", icon: CogIcon },
{ name: "Help", href: "#", icon: HelpCircleIcon },
{ name: "Privacy", href: "#", icon: ShieldCheckIcon },
];
export function Sidebar(): React.JSX.Element {
return (
<div className="flex flex-grow flex-col overflow-y-auto bg-cyan-700 pb-4 pt-5">
<nav
className="mt-5 flex flex-1 flex-col divide-y divide-cyan-800 overflow-y-auto"
aria-label="Sidebar">
<div className="space-y-1 px-2">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className={classNames(
item.current ? "bg-cyan-800 text-white" : "text-cyan-100 hover:bg-cyan-600 hover:text-white",
"group flex items-center rounded-md px-2 py-2 text-sm font-medium leading-6"
)}
aria-current={item.current ? "page" : undefined}>
<item.icon className="mr-4 h-6 w-6 flex-shrink-0 text-cyan-200" aria-hidden="true" />
{item.name}
</a>
))}
</div>
<div className="mt-6 pt-6">
<div className="space-y-1 px-2">
{secondaryNavigation.map((item) => (
<a
key={item.name}
href={item.href}
className="group flex items-center rounded-md px-2 py-2 text-sm font-medium leading-6 text-cyan-100 hover:bg-cyan-600 hover:text-white">
<item.icon className="mr-4 h-6 w-6 text-cyan-200" aria-hidden="true" />
{item.name}
</a>
))}
</div>
</div>
</nav>
</div>
);
}
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+3
View File
@@ -0,0 +1,3 @@
export function classNames(...classes: string[]): string {
return classes.filter(Boolean).join(" ");
}
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
+17
View File
@@ -0,0 +1,17 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "tailwindui.com",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
};
export default nextConfig;
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@formbricks/demo",
"version": "0.1.0",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
"dev": "next dev -p 3002 --turbopack",
"go": "next dev -p 3002 --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@formbricks/js": "workspace:*",
"lucide-react": "0.468.0",
"next": "15.2.3",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*"
}
}
+20
View File
@@ -0,0 +1,20 @@
import type { AppProps } from "next/app";
import Head from "next/head";
import "../globals.css";
export default function App({ Component, pageProps }: AppProps): React.JSX.Element {
return (
<>
<Head>
<title>Demo App</title>
</Head>
{(!process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID ||
!process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) && (
<div className="w-full bg-red-500 p-3 text-center text-sm text-white">
Please set Formbricks environment variables in apps/demo/.env
</div>
)}
<Component {...pageProps} />
</>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { Head, Html, Main, NextScript } from "next/document";
export default function Document(): React.JSX.Element {
return (
<Html lang="en" className="h-full bg-slate-50">
<Head />
<body className="h-full">
<Main />
<NextScript />
</body>
</Html>
);
}
+359
View File
@@ -0,0 +1,359 @@
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import formbricks from "@formbricks/js";
import fbsetup from "../public/fb-setup.png";
declare const window: Window;
export default function AppPage(): React.JSX.Element {
const [darkMode, setDarkMode] = useState(false);
const router = useRouter();
const userId = "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
const userAttributes = {
"Attribute 1": "one",
"Attribute 2": "two",
"Attribute 3": "three",
};
useEffect(() => {
if (darkMode) {
document.body.classList.add("dark");
} else {
document.body.classList.remove("dark");
}
}, [darkMode]);
useEffect(() => {
const initFormbricks = () => {
// enable Formbricks debug mode by adding formbricksDebug=true GET parameter
const addFormbricksDebugParam = (): void => {
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has("formbricksDebug")) {
urlParams.set("formbricksDebug", "true");
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
window.history.replaceState({}, "", newUrl);
}
};
addFormbricksDebugParam();
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
void formbricks.setup({
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
appUrl: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
});
}
// Connect next.js router to Formbricks
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const handleRouteChange = formbricks.registerRouteChange;
router.events.on("routeChangeComplete", () => {
void handleRouteChange();
});
return () => {
router.events.off("routeChangeComplete", () => {
void handleRouteChange();
});
};
}
};
initFormbricks();
}, [router.events]);
return (
<div className="min-h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col justify-between md:flex-row">
<div className="flex flex-col items-center gap-2 sm:flex-row">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks In-product Survey Demo App
</h1>
<p className="text-slate-700 dark:text-slate-300">
This app helps you test your app surveys. You can create and test user actions, create and
update user attributes, etc.
</p>
</div>
</div>
<button
type="button"
className="mt-2 rounded-lg bg-slate-200 px-6 py-1 dark:bg-slate-700 dark:text-slate-100"
onClick={() => {
setDarkMode(!darkMode);
}}>
{darkMode ? "Toggle Light Mode" : "Toggle Dark Mode"}
</button>
</div>
<div className="my-4 grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">1. Setup .env</h3>
<p className="text-slate-700 dark:text-slate-300">
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
</p>
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
<div className="mt-4 flex-col items-start text-sm text-slate-700 sm:flex sm:items-center sm:text-base dark:text-slate-300">
<p className="mb-1 sm:mb-0 sm:mr-2">You&apos;re connected with env:</p>
<div className="flex items-center">
<strong className="w-32 truncate sm:w-auto">
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
</strong>
<span className="relative ml-2 flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75" />
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500" />
</span>
</div>
</div>
</div>
<div className="mt-4 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">2. Widget Logs</h3>
<p className="text-slate-700 dark:text-slate-300">
Look at the logs to understand how the widget works.{" "}
<strong className="dark:text-white">Open your browser console</strong> to see the logs.
</p>
</div>
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold dark:text-white">
Set a user ID / pull data from Formbricks app
</h3>
<p className="text-slate-700 dark:text-slate-300">
On formbricks.setUserId() the user state will <strong>be fetched from Formbricks</strong> and
the local state gets <strong>updated with the user state</strong>.
</p>
<button
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
type="button"
onClick={() => {
void formbricks.setUserId(userId);
}}>
Set user ID
</button>
<p className="text-xs text-slate-700 dark:text-slate-300">
If you made a change in Formbricks app and it does not seem to work, hit &apos;Reset&apos; and
try again.
</p>
</div>
<div className="p-6">
<div>
<button
type="button"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
No-Code Action
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends a{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions"
rel="noopener noreferrer"
className="underline dark:text-blue-500"
target="_blank">
No Code Action
</a>{" "}
as long as you created it beforehand in the Formbricks App.{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions"
rel="noopener noreferrer"
target="_blank"
className="underline dark:text-blue-500">
Here are instructions on how to do it.
</a>
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
onClick={() => {
void formbricks.setAttribute("Plan", "Free");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Plan to &apos;Free&apos;
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
attribute
</a>{" "}
&apos;Plan&apos; to &apos;Free&apos;. If the attribute does not exist, it creates it.
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
onClick={() => {
void formbricks.setAttribute("Plan", "Paid");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Plan to &apos;Paid&apos;
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
attribute
</a>{" "}
&apos;Plan&apos; to &apos;Paid&apos;. If the attribute does not exist, it creates it.
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
onClick={() => {
void formbricks.setEmail("test@web.com");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Email
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
user email
</a>{" "}
&apos;test@web.com&apos;
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
onClick={() => {
void formbricks.setAttributes(userAttributes);
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Multiple Attributes
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
user attributes
</a>{" "}
to &apos;one&apos;, &apos;two&apos;, &apos;three&apos;.
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
onClick={() => {
void formbricks.setLanguage("de");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Language to &apos;de&apos;
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/general-features/multi-language-surveys"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
language
</a>{" "}
to &apos;de&apos;.
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
void formbricks.track("code");
}}>
Code Action
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends a{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions"
rel="noopener noreferrer"
className="underline dark:text-blue-500"
target="_blank">
Code Action
</a>{" "}
as long as you created it beforehand in the Formbricks App.{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions"
rel="noopener noreferrer"
target="_blank"
className="underline dark:text-blue-500">
Here are instructions on how to do it.
</a>
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
void formbricks.logout();
}}>
Logout
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button logs out the user and syncs the local state with Formbricks. (Only works if a
userId is set)
</p>
</div>
</div>
</div>
</div>
</div>
);
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

+13
View File
@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
darkMode: "class",
theme: {
extend: {},
},
plugins: [require("@tailwindcss/forms")],
};
+5
View File
@@ -0,0 +1,5 @@
{
"exclude": ["node_modules"],
"extends": "@formbricks/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
+22 -20
View File
@@ -11,28 +11,30 @@
"clean": "rimraf .turbo node_modules dist storybook-static" "clean": "rimraf .turbo node_modules dist storybook-static"
}, },
"dependencies": { "dependencies": {
"eslint-plugin-react-refresh": "0.4.20", "eslint-plugin-react-refresh": "0.4.16",
"react": "19.1.0", "react": "19.0.0",
"react-dom": "19.1.0" "react-dom": "19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "3.2.6", "@chromatic-com/storybook": "3.2.2",
"@storybook/addon-a11y": "8.6.12", "@formbricks/config-typescript": "workspace:*",
"@storybook/addon-essentials": "8.6.12", "@storybook/addon-a11y": "8.4.7",
"@storybook/addon-interactions": "8.6.12", "@storybook/addon-essentials": "8.4.7",
"@storybook/addon-links": "8.6.12", "@storybook/addon-interactions": "8.4.7",
"@storybook/addon-onboarding": "8.6.12", "@storybook/addon-links": "8.4.7",
"@storybook/blocks": "8.6.12", "@storybook/addon-onboarding": "8.4.7",
"@storybook/react": "8.6.12", "@storybook/blocks": "8.4.7",
"@storybook/react-vite": "8.6.12", "@storybook/react": "8.4.7",
"@storybook/test": "8.6.12", "@storybook/react-vite": "8.4.7",
"@typescript-eslint/eslint-plugin": "8.31.1", "@storybook/test": "8.4.7",
"@typescript-eslint/parser": "8.31.1", "@typescript-eslint/eslint-plugin": "8.18.0",
"@vitejs/plugin-react": "4.4.1", "@typescript-eslint/parser": "8.18.0",
"esbuild": "0.25.2", "@vitejs/plugin-react": "4.3.4",
"eslint-plugin-storybook": "0.12.0", "esbuild": "0.25.1",
"eslint-plugin-storybook": "0.11.1",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"storybook": "8.6.12", "storybook": "8.4.7",
"vite": "6.3.5" "tsup": "8.3.5",
"vite": "6.0.12"
} }
} }
-17
View File
@@ -1,20 +1,3 @@
module.exports = { module.exports = {
extends: ["@formbricks/eslint-config/legacy-next.js"], extends: ["@formbricks/eslint-config/legacy-next.js"],
ignorePatterns: ["**/package.json", "**/tsconfig.json"],
overrides: [
{
files: ["locales/*.json"],
plugins: ["i18n-json"],
rules: {
"i18n-json/identical-keys": [
"error",
{
filePath: require("path").join(__dirname, "locales", "en-US.json"),
checkExtraKeys: false,
checkMissingKeys: true,
},
],
},
},
],
}; };
+1 -1
View File
@@ -50,4 +50,4 @@ uploads/
.sentryclirc .sentryclirc
# SAML Preloaded Connections # SAML Preloaded Connections
saml-connection/ saml-connection/
+29 -78
View File
@@ -1,4 +1,4 @@
FROM node:22-alpine3.21 AS base FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
# #
## step 1: Prune monorepo ## step 1: Prune monorepo
@@ -18,32 +18,17 @@ FROM node:22-alpine3.21 AS base
FROM base AS installer FROM base AS installer
# Enable corepack and prepare pnpm # Enable corepack and prepare pnpm
RUN npm install --ignore-scripts -g corepack@latest RUN npm install -g corepack@latest
RUN corepack enable RUN corepack enable
RUN corepack prepare pnpm@9.15.0 --activate
# Install necessary build tools and compilers # Install necessary build tools and compilers
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3 RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq
# BuildKit secret handling without hardcoded fallback values ARG NEXT_PUBLIC_SENTRY_DSN
# This approach relies entirely on secrets passed from GitHub Actions ARG SENTRY_AUTH_TOKEN
RUN echo '#!/bin/sh' > /tmp/read-secrets.sh && \
echo 'if [ -f "/run/secrets/database_url" ]; then' >> /tmp/read-secrets.sh && \
echo ' export DATABASE_URL=$(cat /run/secrets/database_url)' >> /tmp/read-secrets.sh && \
echo 'else' >> /tmp/read-secrets.sh && \
echo ' echo "DATABASE_URL secret not found. Build may fail if this is required."' >> /tmp/read-secrets.sh && \
echo 'fi' >> /tmp/read-secrets.sh && \
echo 'if [ -f "/run/secrets/encryption_key" ]; then' >> /tmp/read-secrets.sh && \
echo ' export ENCRYPTION_KEY=$(cat /run/secrets/encryption_key)' >> /tmp/read-secrets.sh && \
echo 'else' >> /tmp/read-secrets.sh && \
echo ' echo "ENCRYPTION_KEY secret not found. Build may fail if this is required."' >> /tmp/read-secrets.sh && \
echo 'fi' >> /tmp/read-secrets.sh && \
echo 'exec "$@"' >> /tmp/read-secrets.sh && \
chmod +x /tmp/read-secrets.sh
# Increase Node.js memory limit as a regular build argument # Increase Node.js memory limit
ARG NODE_OPTIONS="--max_old_space_size=4096" # ENV NODE_OPTIONS="--max_old_space_size=4096"
ENV NODE_OPTIONS=${NODE_OPTIONS}
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app
@@ -60,13 +45,10 @@ COPY . .
RUN touch apps/web/.env RUN touch apps/web/.env
# Install the dependencies # Install the dependencies
RUN pnpm install --ignore-scripts RUN pnpm install
# Build the project using our secret reader script # Build the project
# This mounts the secrets only during this build step without storing them in layers RUN NODE_OPTIONS="--max_old_space_size=4096" pnpm build --filter=@formbricks/web...
RUN --mount=type=secret,id=database_url \
--mount=type=secret,id=encryption_key \
/tmp/read-secrets.sh pnpm build --filter=@formbricks/web...
# Extract Prisma version # Extract Prisma version
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
@@ -76,79 +58,48 @@ RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_ver
# #
FROM base AS runner FROM base AS runner
RUN npm install --ignore-scripts -g corepack@latest RUN npm install -g corepack@latest
RUN corepack enable RUN corepack enable
RUN corepack prepare pnpm@9.15.0 --activate
RUN apk add --no-cache curl \ RUN apk add --no-cache curl \
&& apk add --no-cache supercronic \ && apk add --no-cache supercronic \
# && addgroup --system --gid 1001 nodejs \ # && addgroup --system --gid 1001 nodejs \
&& addgroup -S nextjs \ && adduser --system --uid 1001 nextjs
&& adduser -S -u 1001 -G nextjs nextjs
WORKDIR /home/nextjs WORKDIR /home/nextjs
# Ensure no write permissions are assigned to the copied resources
COPY --from=installer /app/apps/web/.next/standalone ./
RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./
COPY --from=installer /app/apps/web/next.config.mjs . COPY --from=installer /app/apps/web/next.config.mjs .
RUN chmod 644 ./next.config.mjs
COPY --from=installer /app/apps/web/package.json . COPY --from=installer /app/apps/web/package.json .
RUN chmod 644 ./package.json # Leverage output traces to reduce image size
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
RUN chown -R nextjs:nextjs ./apps/web/.next/static && chmod -R 755 ./apps/web/.next/static COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
COPY --from=installer /app/apps/web/public ./apps/web/public # Copy Prisma-specific generated files
RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
COPY --from=installer /app/packages/database/migration ./packages/database/migration
RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
COPY --from=installer /app/packages/database/src ./packages/database/src
RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
COPY --from=installer /prisma_version.txt .
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
COPY /docker/cronjobs /app/docker/cronjobs COPY /docker/cronjobs /app/docker/cronjobs
RUN chmod -R 755 /app/docker/cronjobs
# Copy required dependencies
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2 COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
RUN chmod -R 755 ./node_modules/@noble/hashes
COPY --from=installer /app/node_modules/zod ./node_modules/zod COPY --from=installer /app/node_modules/zod ./node_modules/zod
RUN chmod -R 755 ./node_modules/zod
RUN npm install --ignore-scripts -g tsx typescript prisma RUN npm install -g tsx typescript prisma pino-pretty
EXPOSE 3000 EXPOSE 3000
ENV HOSTNAME "0.0.0.0" ENV HOSTNAME "0.0.0.0"
ENV NODE_ENV="production" ENV NODE_ENV="production"
USER nextjs # USER nextjs
# Prepare volume for uploads # Prepare volume for uploads
RUN mkdir -p /home/nextjs/apps/web/uploads/ RUN mkdir -p /home/nextjs/apps/web/uploads/
@@ -1,11 +1,11 @@
"use client"; "use client";
import { cn } from "@/lib/cn";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react"; import { useTranslate } from "@tolgee/react";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment"; import { TEnvironment } from "@formbricks/types/environment";
import { TProjectConfigChannel } from "@formbricks/types/project"; import { TProjectConfigChannel } from "@formbricks/types/project";
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions"; import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
@@ -1,12 +1,12 @@
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks"; import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { WEBAPP_URL } from "@/lib/constants";
import { getEnvironment } from "@/lib/environment/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server"; import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
interface ConnectPageProps { interface ConnectPageProps {
params: Promise<{ params: Promise<{
@@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => {
channel={channel} channel={channel}
/> />
<Button <Button
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
asChild> asChild>
<Link href={`/environments/${environment.id}`}> <Link href={`/environments/${environment.id}`}>
@@ -1,7 +1,7 @@
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { AuthorizationError } from "@formbricks/types/errors"; import { AuthorizationError } from "@formbricks/types/errors";
const OnboardingLayout = async (props) => { const OnboardingLayout = async (props) => {
@@ -1,4 +1,4 @@
import { replaceQuestionPresetPlaceholders } from "@/lib/utils/templates"; import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
import { TProject } from "@formbricks/types/project"; import { TProject } from "@formbricks/types/project";
import { TXMTemplate } from "@formbricks/types/templates"; import { TXMTemplate } from "@formbricks/types/templates";
@@ -1,13 +1,8 @@
import { import { getDefaultEndingCard } from "@/app/lib/templates";
buildCTAQuestion,
buildNPSQuestion,
buildOpenTextQuestion,
buildRatingQuestion,
getDefaultEndingCard,
} from "@/app/lib/survey-builder";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { TFnType } from "@tolgee/react"; import { TFnType } from "@tolgee/react";
import { logger } from "@formbricks/logger"; import { logger } from "@formbricks/logger";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates"; import { TXMTemplate } from "@formbricks/types/templates";
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => { export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
@@ -31,26 +26,35 @@ const npsSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t), ...getXMSurveyDefault(t),
name: t("templates.nps_survey_name"), name: t("templates.nps_survey_name"),
questions: [ questions: [
buildNPSQuestion({ {
headline: t("templates.nps_survey_question_1_headline"), id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: { default: t("templates.nps_survey_question_1_headline") },
required: true, required: true,
lowerLabel: t("templates.nps_survey_question_1_lower_label"), lowerLabel: { default: t("templates.nps_survey_question_1_lower_label") },
upperLabel: t("templates.nps_survey_question_1_upper_label"), upperLabel: { default: t("templates.nps_survey_question_1_upper_label") },
isColorCodingEnabled: true, isColorCodingEnabled: true,
t, },
}), {
buildOpenTextQuestion({ id: createId(),
headline: t("templates.nps_survey_question_2_headline"), type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.nps_survey_question_2_headline") },
required: false, required: false,
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
buildOpenTextQuestion({ },
headline: t("templates.nps_survey_question_3_headline"), },
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.nps_survey_question_3_headline") },
required: false, required: false,
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
},
},
], ],
}; };
}; };
@@ -63,8 +67,9 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey, ...defaultSurvey,
name: t("templates.star_rating_survey_name"), name: t("templates.star_rating_survey_name"),
questions: [ questions: [
buildRatingQuestion({ {
id: reusableQuestionIds[0], id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [ logic: [
{ {
id: createId(), id: createId(),
@@ -97,15 +102,16 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
], ],
range: 5, range: 5,
scale: "number", scale: "number",
headline: t("templates.star_rating_survey_question_1_headline"), headline: { default: t("templates.star_rating_survey_question_1_headline") },
required: true, required: true,
lowerLabel: t("templates.star_rating_survey_question_1_lower_label"), lowerLabel: { default: t("templates.star_rating_survey_question_1_lower_label") },
upperLabel: t("templates.star_rating_survey_question_1_upper_label"), upperLabel: { default: t("templates.star_rating_survey_question_1_upper_label") },
t, isColorCodingEnabled: false,
}), },
buildCTAQuestion({ {
id: reusableQuestionIds[1], id: reusableQuestionIds[1],
html: t("templates.star_rating_survey_question_2_html"), html: { default: t("templates.star_rating_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA,
logic: [ logic: [
{ {
id: createId(), id: createId(),
@@ -132,23 +138,25 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
], ],
}, },
], ],
headline: t("templates.star_rating_survey_question_2_headline"), headline: { default: t("templates.star_rating_survey_question_2_headline") },
required: true, required: true,
buttonUrl: "https://formbricks.com/github", buttonUrl: "https://formbricks.com/github",
buttonLabel: t("templates.star_rating_survey_question_2_button_label"), buttonLabel: { default: t("templates.star_rating_survey_question_2_button_label") },
buttonExternal: true, buttonExternal: true,
t, },
}), {
buildOpenTextQuestion({
id: reusableQuestionIds[2], id: reusableQuestionIds[2],
headline: t("templates.star_rating_survey_question_3_headline"), type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.star_rating_survey_question_3_headline") },
required: true, required: true,
subheader: t("templates.star_rating_survey_question_3_subheader"), subheader: { default: t("templates.star_rating_survey_question_3_subheader") },
buttonLabel: t("templates.star_rating_survey_question_3_button_label"), buttonLabel: { default: t("templates.star_rating_survey_question_3_button_label") },
placeholder: t("templates.star_rating_survey_question_3_placeholder"), placeholder: { default: t("templates.star_rating_survey_question_3_placeholder") },
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
},
},
], ],
}; };
}; };
@@ -161,8 +169,9 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey, ...defaultSurvey,
name: t("templates.csat_survey_name"), name: t("templates.csat_survey_name"),
questions: [ questions: [
buildRatingQuestion({ {
id: reusableQuestionIds[0], id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [ logic: [
{ {
id: createId(), id: createId(),
@@ -195,14 +204,15 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
], ],
range: 5, range: 5,
scale: "smiley", scale: "smiley",
headline: t("templates.csat_survey_question_1_headline"), headline: { default: t("templates.csat_survey_question_1_headline") },
required: true, required: true,
lowerLabel: t("templates.csat_survey_question_1_lower_label"), lowerLabel: { default: t("templates.csat_survey_question_1_lower_label") },
upperLabel: t("templates.csat_survey_question_1_upper_label"), upperLabel: { default: t("templates.csat_survey_question_1_upper_label") },
t, isColorCodingEnabled: false,
}), },
buildOpenTextQuestion({ {
id: reusableQuestionIds[1], id: reusableQuestionIds[1],
type: TSurveyQuestionTypeEnum.OpenText,
logic: [ logic: [
{ {
id: createId(), id: createId(),
@@ -229,20 +239,25 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
], ],
}, },
], ],
headline: t("templates.csat_survey_question_2_headline"), headline: { default: t("templates.csat_survey_question_2_headline") },
required: false, required: false,
placeholder: t("templates.csat_survey_question_2_placeholder"), placeholder: { default: t("templates.csat_survey_question_2_placeholder") },
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
buildOpenTextQuestion({ },
},
{
id: reusableQuestionIds[2], id: reusableQuestionIds[2],
headline: t("templates.csat_survey_question_3_headline"), type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.csat_survey_question_3_headline") },
required: false, required: false,
placeholder: t("templates.csat_survey_question_3_placeholder"), placeholder: { default: t("templates.csat_survey_question_3_placeholder") },
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
},
},
], ],
}; };
}; };
@@ -252,22 +267,28 @@ const cessSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t), ...getXMSurveyDefault(t),
name: t("templates.cess_survey_name"), name: t("templates.cess_survey_name"),
questions: [ questions: [
buildRatingQuestion({ {
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
range: 5, range: 5,
scale: "number", scale: "number",
headline: t("templates.cess_survey_question_1_headline"), headline: { default: t("templates.cess_survey_question_1_headline") },
required: true, required: true,
lowerLabel: t("templates.cess_survey_question_1_lower_label"), lowerLabel: { default: t("templates.cess_survey_question_1_lower_label") },
upperLabel: t("templates.cess_survey_question_1_upper_label"), upperLabel: { default: t("templates.cess_survey_question_1_upper_label") },
t, isColorCodingEnabled: false,
}), },
buildOpenTextQuestion({ {
headline: t("templates.cess_survey_question_2_headline"), id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.cess_survey_question_2_headline") },
required: true, required: true,
placeholder: t("templates.cess_survey_question_2_placeholder"), placeholder: { default: t("templates.cess_survey_question_2_placeholder") },
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
},
},
], ],
}; };
}; };
@@ -280,8 +301,9 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey, ...defaultSurvey,
name: t("templates.smileys_survey_name"), name: t("templates.smileys_survey_name"),
questions: [ questions: [
buildRatingQuestion({ {
id: reusableQuestionIds[0], id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [ logic: [
{ {
id: createId(), id: createId(),
@@ -314,15 +336,16 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
], ],
range: 5, range: 5,
scale: "smiley", scale: "smiley",
headline: t("templates.smileys_survey_question_1_headline"), headline: { default: t("templates.smileys_survey_question_1_headline") },
required: true, required: true,
lowerLabel: t("templates.smileys_survey_question_1_lower_label"), lowerLabel: { default: t("templates.smileys_survey_question_1_lower_label") },
upperLabel: t("templates.smileys_survey_question_1_upper_label"), upperLabel: { default: t("templates.smileys_survey_question_1_upper_label") },
t, isColorCodingEnabled: false,
}), },
buildCTAQuestion({ {
id: reusableQuestionIds[1], id: reusableQuestionIds[1],
html: t("templates.smileys_survey_question_2_html"), html: { default: t("templates.smileys_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA,
logic: [ logic: [
{ {
id: createId(), id: createId(),
@@ -349,23 +372,25 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
], ],
}, },
], ],
headline: t("templates.smileys_survey_question_2_headline"), headline: { default: t("templates.smileys_survey_question_2_headline") },
required: true, required: true,
buttonUrl: "https://formbricks.com/github", buttonUrl: "https://formbricks.com/github",
buttonLabel: t("templates.smileys_survey_question_2_button_label"), buttonLabel: { default: t("templates.smileys_survey_question_2_button_label") },
buttonExternal: true, buttonExternal: true,
t, },
}), {
buildOpenTextQuestion({
id: reusableQuestionIds[2], id: reusableQuestionIds[2],
headline: t("templates.smileys_survey_question_3_headline"), type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.smileys_survey_question_3_headline") },
required: true, required: true,
subheader: t("templates.smileys_survey_question_3_subheader"), subheader: { default: t("templates.smileys_survey_question_3_subheader") },
buttonLabel: t("templates.smileys_survey_question_3_button_label"), buttonLabel: { default: t("templates.smileys_survey_question_3_button_label") },
placeholder: t("templates.smileys_survey_question_3_placeholder"), placeholder: { default: t("templates.smileys_survey_question_3_placeholder") },
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
},
},
], ],
}; };
}; };
@@ -375,26 +400,37 @@ const enpsSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t), ...getXMSurveyDefault(t),
name: t("templates.enps_survey_name"), name: t("templates.enps_survey_name"),
questions: [ questions: [
buildNPSQuestion({ {
headline: t("templates.enps_survey_question_1_headline"), id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: {
default: t("templates.enps_survey_question_1_headline"),
},
required: false, required: false,
lowerLabel: t("templates.enps_survey_question_1_lower_label"), lowerLabel: { default: t("templates.enps_survey_question_1_lower_label") },
upperLabel: t("templates.enps_survey_question_1_upper_label"), upperLabel: { default: t("templates.enps_survey_question_1_upper_label") },
isColorCodingEnabled: true, isColorCodingEnabled: true,
t, },
}), {
buildOpenTextQuestion({ id: createId(),
headline: t("templates.enps_survey_question_2_headline"), type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.enps_survey_question_2_headline") },
required: false, required: false,
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
buildOpenTextQuestion({ },
headline: t("templates.enps_survey_question_3_headline"), },
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.enps_survey_question_3_headline") },
required: false, required: false,
inputType: "text", inputType: "text",
t, charLimit: {
}), enabled: false,
},
},
], ],
}; };
}; };
@@ -1,7 +1,4 @@
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList"; import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
import { getEnvironment } from "@/lib/environment/service";
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
import { getUser } from "@/lib/user/service";
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper"; import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
@@ -10,6 +7,9 @@ import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import Link from "next/link"; import Link from "next/link";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service";
import { getUser } from "@formbricks/lib/user/service";
interface XMTemplatePageProps { interface XMTemplatePageProps {
params: Promise<{ params: Promise<{
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
<XMTemplateList project={project} user={user} environmentId={environment.id} /> <XMTemplateList project={project} user={user} environmentId={environment.id} />
{projects.length >= 2 && ( {projects.length >= 2 && (
<Button <Button
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
asChild> asChild>
<Link href={`/environments/${environment.id}/surveys`}> <Link href={`/environments/${environment.id}/surveys`}>
@@ -1,12 +1,12 @@
"use server"; "use server";
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding"; import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
import { cache } from "@/lib/cache";
import { teamCache } from "@/lib/cache/team"; import { teamCache } from "@/lib/cache/team";
import { validateInputs } from "@/lib/utils/validate";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react"; import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database"; import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors"; import { DatabaseError } from "@formbricks/types/errors";
@@ -1,8 +1,7 @@
"use client"; "use client";
import { formbricksLogout } from "@/app/lib/formbricks";
import FBLogo from "@/images/formbricks-wordmark.svg"; import FBLogo from "@/images/formbricks-wordmark.svg";
import { cn } from "@/lib/cn";
import { capitalizeFirstLetter } from "@/lib/utils/strings";
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal"; import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
import { ProfileAvatar } from "@/modules/ui/components/avatars"; import { ProfileAvatar } from "@/modules/ui/components/avatars";
import { import {
@@ -25,6 +24,8 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TOrganization } from "@formbricks/types/organizations"; import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user"; import { TUser } from "@formbricks/types/user";
@@ -111,7 +112,7 @@ export const LandingSidebar = ({
{/* Dropdown Items */} {/* Dropdown Items */}
{dropdownNavigation.map((link) => ( {dropdownNavigation.map((link) => (
<Link id={link.href} href={link.href} target={link.target} className="flex w-full items-center"> <Link href={link.href} target={link.target} className="flex w-full items-center">
<DropdownMenuItem> <DropdownMenuItem>
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} /> <link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
{link.label} {link.label}
@@ -124,6 +125,7 @@ export const LandingSidebar = ({
<DropdownMenuItem <DropdownMenuItem
onClick={async () => { onClick={async () => {
await signOut({ callbackUrl: "/auth/login" }); await signOut({ callbackUrl: "/auth/login" });
await formbricksLogout();
}} }}
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}> icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
{t("common.logout")} {t("common.logout")}
@@ -1,9 +1,9 @@
import { getEnvironments } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getUserProjects } from "@/lib/project/service";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getUserProjects } from "@formbricks/lib/project/service";
const LandingLayout = async (props) => { const LandingLayout = async (props) => {
const params = await props.params; const params = await props.params;
@@ -1,11 +1,11 @@
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar"; import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
import { getOrganizationsByUserId } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils"; import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server"; import { getTranslate } from "@/tolgee/server";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
const Page = async (props) => { const Page = async (props) => {
const params = await props.params; const params = await props.params;
@@ -1,19 +1,19 @@
import { canUserAccessOrganization } from "@/lib/organization/auth";
import { getOrganization } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import "@testing-library/jest-dom/vitest"; import "@testing-library/jest-dom/vitest";
import { act, cleanup, render, screen } from "@testing-library/react"; import { act, cleanup, render, screen } from "@testing-library/react";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import React from "react"; import React from "react";
import { beforeEach, describe, expect, test, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { TOrganization } from "@formbricks/types/organizations"; import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user"; import { TUser } from "@formbricks/types/user";
import ProjectOnboardingLayout from "./layout"; import ProjectOnboardingLayout from "./layout";
// Mock all the modules and functions that this layout uses: // Mock all the modules and functions that this layout uses:
vi.mock("@/lib/constants", () => ({ vi.mock("@formbricks/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false, IS_FORMBRICKS_CLOUD: false,
POSTHOG_API_KEY: "mock-posthog-api-key", POSTHOG_API_KEY: "mock-posthog-api-key",
POSTHOG_HOST: "mock-posthog-host", POSTHOG_HOST: "mock-posthog-host",
@@ -42,13 +42,13 @@ vi.mock("next-auth", () => ({
vi.mock("next/navigation", () => ({ vi.mock("next/navigation", () => ({
redirect: vi.fn(), redirect: vi.fn(),
})); }));
vi.mock("@/lib/organization/auth", () => ({ vi.mock("@formbricks/lib/organization/auth", () => ({
canUserAccessOrganization: vi.fn(), canUserAccessOrganization: vi.fn(),
})); }));
vi.mock("@/lib/organization/service", () => ({ vi.mock("@formbricks/lib/organization/service", () => ({
getOrganization: vi.fn(), getOrganization: vi.fn(),
})); }));
vi.mock("@/lib/user/service", () => ({ vi.mock("@formbricks/lib/user/service", () => ({
getUser: vi.fn(), getUser: vi.fn(),
})); }));
vi.mock("@/tolgee/server", () => ({ vi.mock("@/tolgee/server", () => ({
@@ -71,7 +71,7 @@ describe("ProjectOnboardingLayout", () => {
cleanup(); cleanup();
}); });
test("redirects to /auth/login if there is no session", async () => { it("redirects to /auth/login if there is no session", async () => {
// Mock no session // Mock no session
vi.mocked(getServerSession).mockResolvedValueOnce(null); vi.mocked(getServerSession).mockResolvedValueOnce(null);
@@ -85,7 +85,7 @@ describe("ProjectOnboardingLayout", () => {
expect(layoutElement).toBeUndefined(); expect(layoutElement).toBeUndefined();
}); });
test("throws an error if user does not exist", async () => { it("throws an error if user does not exist", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ vi.mocked(getServerSession).mockResolvedValueOnce({
user: { id: "user-123" }, user: { id: "user-123" },
}); });
@@ -99,7 +99,7 @@ describe("ProjectOnboardingLayout", () => {
).rejects.toThrow("common.user_not_found"); ).rejects.toThrow("common.user_not_found");
}); });
test("throws AuthorizationError if user cannot access organization", async () => { it("throws AuthorizationError if user cannot access organization", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } }); vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser); vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false); vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
@@ -112,7 +112,7 @@ describe("ProjectOnboardingLayout", () => {
).rejects.toThrow("common.not_authorized"); ).rejects.toThrow("common.not_authorized");
}); });
test("throws an error if organization does not exist", async () => { it("throws an error if organization does not exist", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } }); vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser); vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true); vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
@@ -126,7 +126,7 @@ describe("ProjectOnboardingLayout", () => {
).rejects.toThrow("common.organization_not_found"); ).rejects.toThrow("common.organization_not_found");
}); });
test("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => { it("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
// Provide valid data // Provide valid data
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } }); vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser); vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
@@ -1,13 +1,13 @@
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify"; import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { IS_POSTHOG_CONFIGURED } from "@/lib/constants";
import { canUserAccessOrganization } from "@/lib/organization/auth";
import { getOrganization } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { ToasterClient } from "@/modules/ui/components/toaster-client"; import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getTranslate } from "@/tolgee/server"; import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors"; import { AuthorizationError } from "@formbricks/types/errors";
const ProjectOnboardingLayout = async (props) => { const ProjectOnboardingLayout = async (props) => {
@@ -1,5 +1,4 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { getUserProjects } from "@/lib/project/service";
import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
@@ -7,6 +6,7 @@ import { getTranslate } from "@/tolgee/server";
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react"; import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getUserProjects } from "@formbricks/lib/project/service";
interface ChannelPageProps { interface ChannelPageProps {
params: Promise<{ params: Promise<{
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
<OnboardingOptionsContainer options={channelOptions} /> <OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && ( {projects.length >= 1 && (
<Button <Button
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
asChild> asChild>
<Link href={"/"}> <Link href={"/"}>
@@ -1,12 +1,12 @@
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { getOrganization } from "@/lib/organization/service";
import { getOrganizationProjectsCount } from "@/lib/project/service";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils"; import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import { getTranslate } from "@/tolgee/server"; import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
const OnboardingLayout = async (props) => { const OnboardingLayout = async (props) => {
const params = await props.params; const params = await props.params;
@@ -1,5 +1,4 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { getUserProjects } from "@/lib/project/service";
import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
@@ -7,6 +6,7 @@ import { getTranslate } from "@/tolgee/server";
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react"; import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getUserProjects } from "@formbricks/lib/project/service";
interface ModePageProps { interface ModePageProps {
params: Promise<{ params: Promise<{
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
<OnboardingOptionsContainer options={channelOptions} /> <OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && ( {projects.length >= 1 && (
<Button <Button
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
asChild> asChild>
<Link href={"/"}> <Link href={"/"}>
@@ -2,7 +2,6 @@
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions"; import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
import { previewSurvey } from "@/app/lib/templates"; import { previewSurvey } from "@/app/lib/templates";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team"; import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal"; import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
@@ -27,6 +26,7 @@ import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { import {
TProjectConfigChannel, TProjectConfigChannel,
TProjectConfigIndustry, TProjectConfigIndustry,
@@ -225,7 +225,7 @@ export const ProjectSettings = ({
alt="Logo" alt="Logo"
width={256} width={256}
height={56} height={56}
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1" className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
/> />
)} )}
<p className="text-sm text-slate-400">{t("common.preview")}</p> <p className="text-sm text-slate-400">{t("common.preview")}</p>
@@ -1,7 +1,5 @@
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding"; import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings"; import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
import { getUserProjects } from "@/lib/project/service";
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
@@ -10,6 +8,8 @@ import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { getUserProjects } from "@formbricks/lib/project/service";
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project"; import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
interface ProjectSettingsPageProps { interface ProjectSettingsPageProps {
@@ -65,7 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
/> />
{projects.length >= 1 && ( {projects.length >= 1 && (
<Button <Button
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
asChild> asChild>
<Link href={"/"}> <Link href={"/"}>
@@ -1,120 +1,191 @@
import { getEnvironment } from "@/lib/environment/service"; import "@testing-library/jest-dom/vitest";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils"; import { act, cleanup, render, screen } from "@testing-library/react";
import { cleanup, render, screen } from "@testing-library/react"; import { getServerSession } from "next-auth";
import { Session } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { afterEach, describe, expect, test, vi } from "vitest"; import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { TEnvironment } from "@formbricks/types/environment"; import { TEnvironment } from "@formbricks/types/environment";
import { AuthorizationError } from "@formbricks/types/errors";
import { TOrganization } from "@formbricks/types/organizations"; import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user"; import { TUser } from "@formbricks/types/user";
import SurveyEditorEnvironmentLayout from "./layout"; import SurveyEditorEnvironmentLayout from "./layout";
// Mock sub-components to render identifiable elements // mock all dependencies
vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({
EnvironmentIdBaseLayout: ({ children, environmentId }: any) => ( vi.mock("@formbricks/lib/constants", () => ({
<div data-testid="EnvironmentIdBaseLayout"> IS_FORMBRICKS_CLOUD: false,
{environmentId} POSTHOG_API_KEY: "mock-posthog-api-key",
{children} POSTHOG_HOST: "mock-posthog-host",
</div> IS_POSTHOG_CONFIGURED: true,
), ENCRYPTION_KEY: "mock-encryption-key",
})); ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
vi.mock("@/modules/ui/components/dev-environment-banner", () => ({ GITHUB_ID: "mock-github-id",
DevEnvironmentBanner: ({ environment }: any) => ( GITHUB_SECRET: "test-githubID",
<div data-testid="DevEnvironmentBanner">{environment.id}</div> GOOGLE_CLIENT_ID: "test-google-client-id",
), GOOGLE_CLIENT_SECRET: "test-google-client-secret",
AZUREAD_CLIENT_ID: "test-azuread-client-id",
AZUREAD_CLIENT_SECRET: "test-azure",
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
OIDC_DISPLAY_NAME: "test-oidc-display-name",
OIDC_CLIENT_ID: "test-oidc-client-id",
OIDC_ISSUER: "test-oidc-issuer",
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
WEBAPP_URL: "test-webapp-url",
IS_PRODUCTION: false,
})); }));
// Mocks for dependencies vi.mock("next-auth", () => ({
vi.mock("@/modules/environments/lib/utils", () => ({ getServerSession: vi.fn(),
environmentIdLayoutChecks: vi.fn(),
}));
vi.mock("@/lib/environment/service", () => ({
getEnvironment: vi.fn(),
})); }));
vi.mock("next/navigation", () => ({ vi.mock("next/navigation", () => ({
redirect: vi.fn(), redirect: vi.fn(),
})); }));
vi.mock("@formbricks/lib/environment/auth", () => ({
hasUserEnvironmentAccess: vi.fn(),
}));
vi.mock("@formbricks/lib/environment/service", () => ({
getEnvironment: vi.fn(),
}));
vi.mock("@formbricks/lib/organization/service", () => ({
getOrganizationByEnvironmentId: vi.fn(),
}));
vi.mock("@formbricks/lib/user/service", () => ({
getUser: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: vi.fn(() => {
return (key: string) => key; // trivial translator returning the key
}),
}));
// mock child components rendered by the layout:
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
FormbricksClient: () => <div data-testid="formbricks-client" />,
}));
vi.mock("@/app/(app)/environments/[environmentId]/components/PosthogIdentify", () => ({
PosthogIdentify: () => <div data-testid="posthog-identify" />,
}));
vi.mock("@/modules/ui/components/toaster-client", () => ({
ToasterClient: () => <div data-testid="mock-toaster" />,
}));
vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
DevEnvironmentBanner: ({ environment }: { environment: TEnvironment }) => (
<div data-testid="dev-environment-banner">{environment?.id || "no-env"}</div>
),
}));
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
ResponseFilterProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="mock-response-filter-provider">{children}</div>
),
}));
describe("SurveyEditorEnvironmentLayout", () => { describe("SurveyEditorEnvironmentLayout", () => {
afterEach(() => { beforeEach(() => {
cleanup(); cleanup();
vi.clearAllMocks(); vi.clearAllMocks();
}); });
test("renders successfully when environment is found", async () => { it("redirects to /auth/login if there is no session", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ // Mock no session
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test vi.mocked(getServerSession).mockResolvedValueOnce(null);
session: { user: { id: "user1" } } as Session,
user: { id: "user1", email: "user1@example.com" } as TUser,
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
});
vi.mocked(getEnvironment).mockResolvedValueOnce({ id: "env1" } as TEnvironment);
const result = await SurveyEditorEnvironmentLayout({ const layoutElement = await SurveyEditorEnvironmentLayout({
params: Promise.resolve({ environmentId: "env1" }), params: { environmentId: "env-123" },
children: <div data-testid="child">Survey Editor Content</div>, children: <div data-testid="child-content">Hello!</div>,
}); });
render(result); expect(redirect).toHaveBeenCalledWith("/auth/login");
// No JSX is returned after redirect
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveTextContent("env1"); expect(layoutElement).toBeUndefined();
expect(screen.getByTestId("DevEnvironmentBanner")).toHaveTextContent("env1");
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
}); });
test("throws an error when environment is not found", async () => { it("throws error if user does not exist in DB", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
t: ((key: string) => key) as any, vi.mocked(getUser).mockResolvedValueOnce(null); // user not found
session: { user: { id: "user1" } } as Session,
user: { id: "user1", email: "user1@example.com" } as TUser, await expect(
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization, SurveyEditorEnvironmentLayout({
}); params: { environmentId: "env-123" },
children: <div data-testid="child-content">Hello!</div>,
})
).rejects.toThrow("common.user_not_found");
});
it("throws AuthorizationError if user does not have environment access", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
await expect(
SurveyEditorEnvironmentLayout({
params: { environmentId: "env-123" },
children: <div>Child</div>,
})
).rejects.toThrow(AuthorizationError);
});
it("throws if no organization is found", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
await expect(
SurveyEditorEnvironmentLayout({
params: { environmentId: "env-123" },
children: <div data-testid="child-content">Hello from children!</div>,
})
).rejects.toThrow("common.organization_not_found");
});
it("throws if no environment is found", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
vi.mocked(getEnvironment).mockResolvedValueOnce(null); vi.mocked(getEnvironment).mockResolvedValueOnce(null);
await expect( await expect(
SurveyEditorEnvironmentLayout({ SurveyEditorEnvironmentLayout({
params: Promise.resolve({ environmentId: "env1" }), params: { environmentId: "env-123" },
children: <div>Content</div>, children: <div>Child</div>,
}) })
).rejects.toThrow("common.environment_not_found"); ).rejects.toThrow("common.environment_not_found");
}); });
test("calls redirect when session is null", async () => { it("renders environment layout if everything is valid", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ // Provide all valid data
t: ((key: string) => key) as any, vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
session: undefined as unknown as Session, vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
user: undefined as unknown as TUser, vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization, vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
}); vi.mocked(getEnvironment).mockResolvedValueOnce({
vi.mocked(redirect).mockImplementationOnce(() => { id: "env-123",
throw new Error("Redirect called"); name: "My Test Environment",
} as unknown as TEnvironment);
// Because it's an async server component, we typically wrap in act(...)
let layoutElement: React.ReactNode;
await act(async () => {
layoutElement = await SurveyEditorEnvironmentLayout({
params: { environmentId: "env-123" },
children: <div data-testid="child-content">Hello from children!</div>,
});
render(layoutElement);
}); });
await expect( // Now confirm we got the child plus all the mocked sub-components
SurveyEditorEnvironmentLayout({ expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children!");
params: Promise.resolve({ environmentId: "env1" }), expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
children: <div>Content</div>, expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
}) expect(screen.getByTestId("mock-toaster")).toBeInTheDocument();
).rejects.toThrow("Redirect called"); expect(screen.getByTestId("mock-response-filter-provider")).toBeInTheDocument();
}); expect(screen.getByTestId("dev-environment-banner")).toHaveTextContent("env-123");
test("throws error if user is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: { user: { id: "user1" } } as Session,
user: undefined as unknown as TUser,
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
});
vi.mocked(redirect).mockImplementationOnce(() => {
throw new Error("Redirect called");
});
await expect(
SurveyEditorEnvironmentLayout({
params: Promise.resolve({ environmentId: "env1" }),
children: <div>Content</div>,
})
).rejects.toThrow("common.user_not_found");
}); });
}); });
@@ -1,24 +1,46 @@
import { getEnvironment } from "@/lib/environment/service"; import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils"; import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner"; import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout"; import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors";
const SurveyEditorEnvironmentLayout = async (props) => { const SurveyEditorEnvironmentLayout = async (props) => {
const params = await props.params; const params = await props.params;
const { children } = props; const { children } = props;
const { t, session, user, organization } = await environmentIdLayoutChecks(params.environmentId); const t = await getTranslate();
const session = await getServerSession(authOptions);
if (!session) { if (!session?.user) {
return redirect(`/auth/login`); return redirect(`/auth/login`);
} }
const user = await getUser(session.user.id);
if (!user) { if (!user) {
throw new Error(t("common.user_not_found")); throw new Error(t("common.user_not_found"));
} }
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
if (!hasAccess) {
throw new AuthorizationError(t("common.not_authorized"));
}
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
const environment = await getEnvironment(params.environmentId); const environment = await getEnvironment(params.environmentId);
if (!environment) { if (!environment) {
@@ -26,16 +48,23 @@ const SurveyEditorEnvironmentLayout = async (props) => {
} }
return ( return (
<EnvironmentIdBaseLayout <ResponseFilterProvider>
environmentId={params.environmentId} <PosthogIdentify
session={session} session={session}
user={user} user={user}
organization={organization}> environmentId={params.environmentId}
organizationId={organization.id}
organizationName={organization.name}
organizationBilling={organization.billing}
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
/>
<FormbricksClient userId={user.id} email={user.email} />
<ToasterClient />
<div className="flex h-screen flex-col"> <div className="flex h-screen flex-col">
<DevEnvironmentBanner environment={environment} /> <DevEnvironmentBanner environment={environment} />
<div className="h-full overflow-y-auto bg-slate-50">{children}</div> <div className="h-full overflow-y-auto bg-slate-50">{children}</div>
</div> </div>
</EnvironmentIdBaseLayout> </ResponseFilterProvider>
); );
}; };
@@ -0,0 +1,77 @@
import { render } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import formbricks from "@formbricks/js";
import { FormbricksClient } from "./FormbricksClient";
// Mock next/navigation hooks.
vi.mock("next/navigation", () => ({
usePathname: () => "/test-path",
useSearchParams: () => new URLSearchParams("foo=bar"),
}));
// Mock the environment variables.
vi.mock("@formbricks/lib/env", () => ({
env: {
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: "env-test",
NEXT_PUBLIC_FORMBRICKS_API_HOST: "https://api.test.com",
},
}));
// Mock the flag that enables Formbricks.
vi.mock("@/app/lib/formbricks", () => ({
formbricksEnabled: true,
}));
// Mock the Formbricks SDK module.
vi.mock("@formbricks/js", () => ({
__esModule: true,
default: {
setup: vi.fn(),
setUserId: vi.fn(),
setEmail: vi.fn(),
registerRouteChange: vi.fn(),
},
}));
describe("FormbricksClient", () => {
afterEach(() => {
vi.clearAllMocks();
});
test("calls setup, setUserId, setEmail and registerRouteChange on mount when enabled", () => {
const mockSetup = vi.spyOn(formbricks, "setup");
const mockSetUserId = vi.spyOn(formbricks, "setUserId");
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
render(<FormbricksClient userId="user-123" email="test@example.com" />);
// Expect the first effect to call setup and assign the provided user details.
expect(mockSetup).toHaveBeenCalledWith({
environmentId: "env-test",
appUrl: "https://api.test.com",
});
expect(mockSetUserId).toHaveBeenCalledWith("user-123");
expect(mockSetEmail).toHaveBeenCalledWith("test@example.com");
// And the second effect should always register the route change when Formbricks is enabled.
expect(mockRegisterRouteChange).toHaveBeenCalled();
});
test("does not call setup, setUserId, or setEmail if userId is not provided yet still calls registerRouteChange", () => {
const mockSetup = vi.spyOn(formbricks, "setup");
const mockSetUserId = vi.spyOn(formbricks, "setUserId");
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
render(<FormbricksClient userId="" email="test@example.com" />);
// Since userId is falsy, the first effect should not call setup or assign user details.
expect(mockSetup).not.toHaveBeenCalled();
expect(mockSetUserId).not.toHaveBeenCalled();
expect(mockSetEmail).not.toHaveBeenCalled();
// The second effect only checks formbricksEnabled, so registerRouteChange should be called.
expect(mockRegisterRouteChange).toHaveBeenCalled();
});
});
@@ -0,0 +1,32 @@
"use client";
import { formbricksEnabled } from "@/app/lib/formbricks";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import formbricks from "@formbricks/js";
import { env } from "@formbricks/lib/env";
export const FormbricksClient = ({ userId, email }: { userId: string; email: string }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (formbricksEnabled && userId) {
formbricks.setup({
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
appUrl: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
});
formbricks.setUserId(userId);
formbricks.setEmail(email);
}
}, [userId, email]);
useEffect(() => {
if (formbricksEnabled) {
formbricks.registerRouteChange();
}
}, [pathname, searchParams]);
return null;
};
@@ -1,5 +1,5 @@
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { cn } from "@/lib/cn"; import { cn } from "@formbricks/lib/cn";
export const LoadingCard = ({ export const LoadingCard = ({
title, title,
@@ -1,34 +0,0 @@
import { SingleContactPage } from "@/modules/ee/contacts/[contactId]/page";
import { describe, expect, test, vi } from "vitest";
import Page from "./page";
// mock constants
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
ENCRYPTION_KEY: "test",
ENTERPRISE_LICENSE_KEY: "test",
GITHUB_ID: "test",
GITHUB_SECRET: "test",
GOOGLE_CLIENT_ID: "test",
GOOGLE_CLIENT_SECRET: "test",
AZUREAD_CLIENT_ID: "mock-azuread-client-id",
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
AZUREAD_TENANT_ID: "mock-azuread-tenant-id",
OIDC_CLIENT_ID: "mock-oidc-client-id",
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
OIDC_ISSUER: "mock-oidc-issuer",
OIDC_DISPLAY_NAME: "mock-oidc-display-name",
OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm",
WEBAPP_URL: "mock-webapp-url",
IS_PRODUCTION: true,
FB_LOGO_URL: "https://example.com/mock-logo.png",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "mock-smtp-port",
IS_POSTHOG_CONFIGURED: true,
}));
describe("Contact Page Re-export", () => {
test("should re-export SingleContactPage", () => {
expect(Page).toBe(SingleContactPage);
});
});
@@ -1,15 +0,0 @@
import { ContactsPage } from "@/modules/ee/contacts/page";
import { describe, expect, test, vi } from "vitest";
import Page from "./page";
// Mock the actual ContactsPage component
vi.mock("@/modules/ee/contacts/page", () => ({
ContactsPage: () => <div data-testid="contacts-page">Mock Contacts Page</div>,
}));
describe("Contacts Page Re-export", () => {
test("should re-export ContactsPage from the EE module", () => {
// Assert that the default export 'Page' is the same as the mocked 'ContactsPage'
expect(Page).toBe(ContactsPage);
});
});
@@ -1,18 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, test, vi } from "vitest";
import SegmentsPageWrapper from "./page";
vi.mock("@/modules/ee/contacts/segments/page", () => ({
SegmentsPage: vi.fn(() => <div>SegmentsPageMock</div>),
}));
describe("SegmentsPageWrapper", () => {
afterEach(() => {
cleanup();
});
test("renders the SegmentsPage component", () => {
render(<SegmentsPageWrapper params={{ environmentId: "test-env" } as any} />);
expect(screen.getByText("SegmentsPageMock")).toBeInTheDocument();
});
});
@@ -1,8 +1,5 @@
"use server"; "use server";
import { getOrganization } from "@/lib/organization/service";
import { getOrganizationProjectsCount } from "@/lib/project/service";
import { updateUser } from "@/lib/user/service";
import { authenticatedActionClient } from "@/lib/utils/action-client"; import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { import {
@@ -11,6 +8,9 @@ import {
} from "@/modules/ee/license-check/lib/utils"; } from "@/modules/ee/license-check/lib/utils";
import { createProject } from "@/modules/projects/settings/lib/project"; import { createProject } from "@/modules/projects/settings/lib/project";
import { z } from "zod"; import { z } from "zod";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
import { updateUser } from "@formbricks/lib/user/service";
import { ZId } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors"; import { OperationNotAllowedError } from "@formbricks/types/errors";
import { ZProjectUpdateInput } from "@formbricks/types/project"; import { ZProjectUpdateInput } from "@formbricks/types/project";

Some files were not shown because too many files have changed in this diff Show More