Compare commits

..

2 Commits

Author SHA1 Message Date
review-agent-prime[bot]
2071486674 Edit packages/lib/user/service.ts 2024-01-30 10:46:09 +00:00
Matthias Nannt
bcf039030e feat: add support for customer-io formbricks users sync 2024-01-30 11:42:13 +01:00
36 changed files with 155 additions and 501 deletions

View File

@@ -42,6 +42,7 @@ body:
- First time: Please read our [introductory blog post](https://formbricks.com/blog/join-the-formtribe)
- All UI components are in the package `formbricks/ui`
- Run `pnpm go` to find a demo app to test in-app surveys at `localhost:3002`
- Everything is type-safe.
- We use **chatGPT** to help refactor code.
- Everything is type-safe
- We use **chatGPT** to help refactor code. Use our [Formbricks ✨ megaprompt ✨](https://github.com/formbricks/formbricks/blob/main/megaprompt.md) to create the right
context before you write your prompt.
- Anything unclear? [Ask in Discord](https://formbricks.com/discord)

View File

@@ -1,132 +0,0 @@
name: ECS
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches:
- main
workflow_dispatch: # Add manual trigger support
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: formbricks-experimental
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Generate Random NEXTAUTH_SECRET
run: |
SECRET=$(openssl rand -hex 32)
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
- name: Generate Random ENCRYPTION_KEY
run: |
SECRET=$(openssl rand -hex 32)
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Depot CLI
uses: depot/setup-action@v1
# https://github.com/sigstore/cosign-installer
- name: Install cosign
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
with:
cosign-release: "v2.1.1"
# https://github.com/docker/login-action
- name: Log into registry
uses: docker/login-action@v3 # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: depot/build-push-action@v1
env:
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
with:
project: tw0fqmsx3c
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: .
file: ./apps/web/Dockerfile
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
DATABASE_URL=${{ env.DATABASE_URL }}
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}
NEXT_PUBLIC_SENTRY_DSN=${{ env.NEXT_PUBLIC_SENTRY_DSN }}
- name: Sign the published Docker image
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}${DIGEST}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition prod-webapp-ecs-service --query taskDefinition > task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: prod-webapp-container
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: prod-webapp-ecs-service
cluster: prod-core-infra-ecs-cluster
wait-for-service-stability: true

View File

@@ -126,6 +126,8 @@ Formbricks has a hosted cloud offering with a generous free plan to get you up a
Formbricks is available Open-Source under AGPLv3 license. You can host Formbricks on your own servers using Docker without a subscription.
(In the future we may develop additional features that aren't in the free Open-Source version).
If you opt for self-hosting Formbricks, here are a few options to consider:
#### Docker

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -1,113 +0,0 @@
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import NewsletterSurveyType from './choose-survey-type.webp';
import NewsletterSurveyEmbedCode from './embed-survey-code-in-your-email.webp';
import NewsletterSurveyEmbedPrompt from './embed-survey-prompt.webp';
import NewsletterSurveyEditor from './improve-newsletter-content-editor-formbricks.webp';
import NewsletterSurvey from './improve-newsletter-content-survey-location.webp';
export const metadata = {
title: "Measure email content quality with Formbricks",
description:
"Measuring the content quality of both transactional and marketing email is a key element for improving customer communication.",
};
#### Best Practices
# Improve Email Content
Email remains the predominant way to communicate with your customers. Measure the effectiveness to improve your offering.
## Purpose
Measuring the content quality of both transactional and marketing email is a key element for improving customer communication.
## Preview
<DemoPreview template="Improve Newsletter Content" />
## Formbricks Approach
- Embed the survey into your email so its part of the newsletter.
- Use link prefilling to store the answer users clicked on in the email.
- Dynamic user identification to append reader's email for personalized profiles and follow ups.
## Installation
To embed the newsletter survey into your email, follow these steps:
1. Create new 'Improve Newsletter Content' survey at [app.formbricks.com](https://app.formbricks.com/)
2. Select how you where you want to display the survey.
3. Copy the embed code anywhere you want in your newsletter.
### 1. Create new 'Improve Newsletter Content' Survey
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Then, create a new survey and look for the "Improve Newsletter Content" template:
<Image
src={NewsletterSurvey}
alt="Create Improve Newsletter Content by template"
quality="100"
className="rounded-lg max-w-full sm:max-w-3xl"
/>
### 2. Customize Survey questions
Customize survey questions, emojis or stars however you like:
<Image
src={NewsletterSurveyEditor}
alt="Edit Improve Newsletter Content template"
quality="100"
className="rounded-lg max-w-full sm:max-w-3xl"
/>
### 3. Configure Survey Settings
When you are done customizing your survey questions, navigate to the Settings tab and choose the type of survey you want. You need to choose Link Survey:
<Image
src={NewsletterSurveyType}
alt="Choose survey type"
quality="100"
className="rounded-lg max-w-full sm:max-w-3xl"
/>
### 4. Choose how you want to embed your survey
After publishing your survey, a modal that prompts you to embed your survey will pop up.
<Image
src={NewsletterSurveyEmbedPrompt}
alt="Embed newsletter survey"
quality="100"
className="rounded-lg max-w-full sm:max-w-3xl"
/>
Select the Embed Survey card and you will be directed to another modal, where the first embed option displayed will be to embed the survey in an email.
### 5. Copy code to embed the survey in your newsletter
Click the button with the “View Embed Code” text at the top right corner of the modal and simply paste the HTML code for your survey anywhere you want it in your newsletter. You can see the preview in the below image:
<Image
src={NewsletterSurveyEmbedCode}
alt="Embed survey code"
quality="100"
className="rounded-lg max-w-full sm:max-w-3xl"
/>
And you're done! Send a test email to yourself and try it out 🤓
## Learn about data prefilling
<Note>
## How does data prefilling work?
Learn about how link prefilling and user identification maximize your insights in [this detailed guide](/blog/how-smart-writers-use-formbricks-open-source-tool-to-measure-the-quality-of-their-newsletter-content).
</Note>
### &nbsp;
# Thats it! 🎉

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,88 +0,0 @@
import Image from "next/image";
import HomePage from "./home-page.webp";
import SurveyQuestions from "./survey-questions.webp";
import SurveySettings from "./survey-settings.webp";
import SurveyResponseOptions from "./survey-response-options.webp";
import SurveyPublished from "./survey-published.webp";
export const metadata = {
title: "Formbricks Quickstart Guide: Link Surveys Made Easier & Faster",
description:
"Formbricks is the easiest way to create and manage link surveys. This quickstart guide will show you how to create your first link survey in under 5 minutes.",
};
#### Getting Started
# Quickstart
Link Surveys make it easy for your users to give you feedback. They are a great way to get feedback from your users, without interrupting their workflow. This quickstart guide will show you how to create your first link survey in under 5 minutes.
## Create a free Formbricks Cloud account
While you can [self-host](/docs/self-hosting/deployment) Formbricks, but the quickest and easiest way to get started is with the free Cloud plan. Just [sign up here](https://app.formbricks.com/auth/signup) and click through the onboarding, until youre here:
<Image
src={HomePage}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
/>
Choose one of the pre-created templates to get started. Well choose the **Product Market Fit** template for this quickstart guide.
## Create your first survey
On clicking the template, youll be forwarded to the survey editor. Here you can edit the survey questions and settings. For the sake of simplicity, we'll keep the questions as they are and move to the survey settings.
<Image
src={SurveyQuestions}
alt="Survey Editor opens up in the Formbricks App"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Click on the **Settings** tab to edit the survey settings.
## Configure your survey settings
Formbricks packs a lot of useful functionality out of the box. You can:
- Close the survey on a specidic date
- After a number of response
- Redirect users to a URL after they completed the survey
- Protect survey with a Pin
- ... and much more!
<Image
src={SurveyResponseOptions}
alt="Survey response configuration for link survey"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Style your survey
Style your survey to your need. You can keep it simplistic or use animated backgrounds. You can change the main color and soon you'll be able to fully control the appearance of the survey.
<Image
src={SurveySettings}
alt="UI & View configuration for link survey"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Publish your survey
Once youre happy with the survey settings, hit **Publish** and youll be forwarded to the Summary Page. This is where youll find the responses to this survey.
<Image
src={SurveyPublished}
alt="Survey published successfully and received link to share with users"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Share your survey
Congratulations! Your survey is now published and ready to be shared with your users. You can share the survey link via email, SMS, or any other channel.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -202,7 +202,6 @@ export const navigation: Array<NavGroup> = [
{
title: "Link Surveys",
links: [
{ title: "Quickstart", href: "/docs/link-surveys/quickstart" },
{ title: "Data Prefilling", href: "/docs/link-surveys/data-prefilling" },
{ title: "Identify Users", href: "/docs/link-surveys/user-identification" },
{ title: "Single Use Links", href: "/docs/link-surveys/single-use-links" },
@@ -219,7 +218,6 @@ export const navigation: Array<NavGroup> = [
{ title: "Feature Chaser", href: "/docs/best-practices/feature-chaser" },
{ title: "Feedback Box", href: "/docs/best-practices/feedback-box" },
{ title: "Docs Feedback", href: "/docs/best-practices/docs-feedback" },
{ title: "Improve Email Content", href: "/docs/best-practices/improve-email-content" },
],
},
{

View File

@@ -1,6 +1,6 @@
import { createId } from "@paralleldrive/cuid2";
import { TSurveyHiddenFields, TSurveyQuestionType } from "@formbricks/types/surveys";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import {
AppPieChartIcon,
@@ -38,11 +38,6 @@ const welcomeCardDefault = {
showResponseCount: false,
};
const hiddenFieldsDefault: TSurveyHiddenFields = {
enabled: true,
fieldIds: [],
};
export const customSurvey: TTemplate = {
name: "Start from scratch",
description: "Create a survey without template.",
@@ -1229,58 +1224,6 @@ export const templates: TTemplate[] = [
},
},
},
{
name: "Improve Newsletter Content",
category: "Growth",
objectives: ["increase_conversion", "sharpen_marketing_messaging"],
description: "Find out how your subscribers like your newsletter content.",
preset: {
name: "Improve Newsletter Content",
welcomeCard: welcomeCardDefault,
questions: [
{
id: createId(),
type: TSurveyQuestionType.Rating,
logic: [
{ value: "5", condition: "equals", destination: "l2q1chqssong8n0xwaagyl8g" },
{ value: "5", condition: "lessThan", destination: "k3s6gm5ivkc5crpycdbpzkpa" },
],
range: 5,
scale: "smiley",
headline: "How would you rate this weeks newsletter?",
required: true,
subheader: "",
lowerLabel: "Meh",
upperLabel: "Great",
},
{
id: "k3s6gm5ivkc5crpycdbpzkpa",
type: TSurveyQuestionType.OpenText,
logic: [
{ condition: "submitted", destination: "end" },
{ condition: "skipped", destination: "end" },
],
headline: "What would have made this weeks newsletter more helpful?",
required: false,
placeholder: "Type your answer here...",
inputType: "text",
},
{
id: "l2q1chqssong8n0xwaagyl8g",
html: '<p class="fb-editor-paragraph" dir="ltr"><span>Who thinks like you? You\'d do us a huge favor if you\'d share this weeks episode with your brain friend!</span></p>',
type: TSurveyQuestionType.CTA,
headline: "Thanks! ❤️ Spread the love with ONE friend.",
required: false,
buttonUrl: "https://formbricks.com",
buttonLabel: "Happy to help!",
buttonExternal: true,
dismissButtonLabel: "Find your own friends",
},
],
thankYouCard: thankYouCardDefault,
hiddenFields: hiddenFieldsDefault,
},
},
];
export const findTemplateByName = (name: string): TTemplate | undefined => {

View File

@@ -79,15 +79,6 @@ export default function BestPracticeNavigation() {
description: "Give users the chance to share feedback in a single click.",
category: "Boost Retention",
},
{
name: "Improve Newsletter Cotent",
href: "/improve-newsletter-content",
status: true,
icon: FeedbackIcon,
description: "Improve your newsletter content by showing this survey to your readers.",
category: "Boost Retention",
},
];
return (

View File

@@ -155,6 +155,11 @@ const nextConfig = {
destination: "/docs/self-hosting/migration-guide",
permanent: true,
},
{
source: "/cla",
destination: "https://formbricks.com/clmyhzfrymr4ko00hycsg1tvx",
permanent: true,
},
{
source: "/docs/contributing/gitpod",
destination: "/docs/contributing/setup#gitpod",

View File

@@ -23,8 +23,6 @@ ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
ARG ENCRYPTION_KEY
ENV ENCRYPTION_KEY=$ENCRYPTION_KEY
ARG NEXT_PUBLIC_SENTRY_DSN
# Set the working directory
WORKDIR /app
@@ -78,4 +76,4 @@ CMD supercronic -quiet /app/docker/cronjobs & \
else \
echo "ERROR: Please set a value for NEXTAUTH_SECRET in your docker compose variables!" >&2; \
exit 1; \
fi
fi

View File

@@ -217,44 +217,29 @@ export default function MultipleChoiceMultiForm({
{question.choices &&
question.choices.map((choice, choiceIdx) => (
<div key={choiceIdx} className="inline-flex w-full items-center">
<div className="flex w-full space-x-2">
<Input
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
id={choice.id}
name={choice.id}
value={choice.label}
className={cn(choice.id === "other" && "border-dashed")}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
onBlur={() => {
const duplicateLabel = findDuplicateLabel();
if (duplicateLabel) {
setisInvalidValue(duplicateLabel);
} else if (findEmptyLabel()) {
setisInvalidValue("");
} else {
setisInvalidValue(null);
}
}}
isInvalid={
(isInvalidValue === "" && choice.label.trim() === "") ||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
<Input
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
id={choice.id}
name={choice.id}
value={choice.label}
className={cn(choice.id === "other" && "border-dashed")}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
onBlur={() => {
const duplicateLabel = findDuplicateLabel();
if (duplicateLabel) {
setisInvalidValue(duplicateLabel);
} else if (findEmptyLabel()) {
setisInvalidValue("");
} else {
setisInvalidValue(null);
}
/>
{choice.id === "other" && (
<Input
id="otherInputLabel"
name="otherInputLabel"
value={question.otherOptionPlaceholder ?? "Please specify"}
placeholder={question.otherOptionPlaceholder ?? "Please specify"}
className={cn(choice.id === "other" && "border-dashed")}
onChange={(e) => {
if (e.target.value.trim() == "") e.target.value = "";
updateQuestion(questionIdx, { otherOptionPlaceholder: e.target.value });
}}
/>
)}
</div>
}}
isInvalid={
(isInvalidValue === "" && choice.label.trim() === "") ||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
}
/>
{question.choices && question.choices.length > 2 && (
<TrashIcon
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"

View File

@@ -216,45 +216,30 @@ export default function MultipleChoiceSingleForm({
<div className="mt-2 space-y-2" id="choices">
{question.choices &&
question.choices.map((choice, choiceIdx) => (
<div key={choiceIdx} className="flex w-full items-center">
<div className="flex w-full space-x-2">
<Input
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
id={choice.id}
name={choice.id}
value={choice.label}
className={cn(choice.id === "other" && "border-dashed")}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
onBlur={() => {
const duplicateLabel = findDuplicateLabel();
if (duplicateLabel) {
setisInvalidValue(duplicateLabel);
} else if (findEmptyLabel()) {
setisInvalidValue("");
} else {
setisInvalidValue(null);
}
}}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
isInvalid={
(isInvalidValue === "" && choice.label.trim() === "") ||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
<div key={choiceIdx} className="inline-flex w-full items-center">
<Input
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
id={choice.id}
name={choice.id}
value={choice.label}
className={cn(choice.id === "other" && "border-dashed")}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
onBlur={() => {
const duplicateLabel = findDuplicateLabel();
if (duplicateLabel) {
setisInvalidValue(duplicateLabel);
} else if (findEmptyLabel()) {
setisInvalidValue("");
} else {
setisInvalidValue(null);
}
/>
{choice.id === "other" && (
<Input
id="otherInputLabel"
name="otherInputLabel"
value={question.otherOptionPlaceholder ?? "Please specify"}
placeholder={question.otherOptionPlaceholder ?? "Please specify"}
className={cn(choice.id === "other" && "border-dashed")}
onChange={(e) => {
if (e.target.value.trim() == "") e.target.value = "";
updateQuestion(questionIdx, { otherOptionPlaceholder: e.target.value });
}}
/>
)}
</div>
}}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
isInvalid={
(isInvalidValue === "" && choice.label.trim() === "") ||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
}
/>
{question.choices && question.choices.length > 2 && (
<TrashIcon
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"

View File

@@ -2200,6 +2200,7 @@ export const templates: TTemplate[] = [
},
{
name: "Improve Newsletter Content",
category: "Growth",
objectives: ["increase_conversion", "sharpen_marketing_messaging"],
description: "Find out how your subscribers like your newsletter content.",

View File

@@ -2,7 +2,11 @@ import { NextResponse } from "next/server";
import { prisma } from "@formbricks/database";
import { EMAIL_VERIFICATION_DISABLED, INVITE_DISABLED, SIGNUP_ENABLED } from "@formbricks/lib/constants";
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@formbricks/lib/emails/emails";
import {
sendGettingStartedEmail,
sendInviteAcceptedEmail,
sendVerificationEmail,
} from "@formbricks/lib/emails/emails";
import { env } from "@formbricks/lib/env.mjs";
import { deleteInvite } from "@formbricks/lib/invite/service";
import { verifyInviteToken } from "@formbricks/lib/jwt";
@@ -50,6 +54,8 @@ export async function POST(request: Request) {
if (!EMAIL_VERIFICATION_DISABLED) {
await sendVerificationEmail(user);
} else {
await sendGettingStartedEmail(user);
}
await sendInviteAcceptedEmail(invite.creator.name, user.name, invite.creator.email);
@@ -81,8 +87,9 @@ export async function POST(request: Request) {
// send verification email amd return user
if (!EMAIL_VERIFICATION_DISABLED) {
await sendVerificationEmail(user);
} else {
await sendGettingStartedEmail(user);
}
return NextResponse.json(user);
} catch (e) {
if (e.code === "P2002") {

View File

@@ -15,11 +15,7 @@ export default async function Home() {
redirect("/auth/login");
}
if (!session?.user) {
return <ClientLogout />;
}
if (!ONBOARDING_DISABLED && !session.user.onboardingCompleted) {
if (!ONBOARDING_DISABLED && session?.user && !session?.user?.onboardingCompleted) {
return redirect(`/onboarding`);
}

View File

@@ -27,7 +27,7 @@ export default function LegalFooter({
return (
<div
className={`absolute bottom-0 h-12 w-full`}
className={`fixed bottom-0 h-12 w-full`}
style={{
backgroundColor: `${bgColor}`,
}}>

View File

@@ -135,7 +135,7 @@ export default function LinkSurvey({
return (
<>
<ContentWrapper className="my-12 h-full w-full p-0 md:max-w-md">
<ContentWrapper className="h-full w-full p-0 md:max-w-md">
{isPreview && (
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
<div />

View File

@@ -195,7 +195,7 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
}
return survey ? (
<div className="relative">
<div>
<MediaBackground survey={survey}>
<LinkSurvey
survey={survey}

43
megaprompt.md Normal file
View File

@@ -0,0 +1,43 @@
Forget all previous instructions. You are the coding assistant "Formbricks Assistant".
- You help programmers find a solution to their questions and coding challenges. You give very brief and concise answers unless I the user prompt you to elaborate.
- Formbricks is the open-source go-to solution for in-product micro-surveys that is supercharging our users product experience!
- Formbricks uses Typescript, Next.Js, Next-auth, Prisma, TailwindCss, Radix UI
- When you are asked to generate documentation please have a playful but succinct writing style and return everything in escaped markdown.
- This is the prisma schema:
enum PipelineTriggers { responseCreated, responseUpdated, responseFinished }
model Webhook { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), url String, environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade), environmentId String, triggers PipelineTriggers[] }
model Attribute { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), attributeClass AttributeClass @relation(fields: [attributeClassId], references: [id], onDelete: Cascade), attributeClassId String, person Person @relation(fields: [personId], references: [id], onDelete: Cascade), personId String, value String, @@unique([attributeClassId, personId]) }
enum AttributeType { code, noCode, automatic }
model AttributeClass { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), name String, description String?, type AttributeType, environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade), environmentId String, attributes Attribute[], attributeFilters SurveyAttributeFilter[], @@unique([name, environmentId]) }
model Person { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade), environmentId String, responses Response[], sessions Session[], attributes Attribute[], displays Display[] }
model Response { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), finished Boolean @default(false), survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade), surveyId String, person Person? @relation(fields: [personId], references: [id], onDelete: Cascade), personId String?, data Json @default("{}"), meta Json @default("{}") }
enum SurveyStatus { draft, inProgress, paused, completed, archived }
enum DisplayStatus { seen, responded }
model Display { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade), surveyId String, person Person? @relation(fields: [personId], references: [id], onDelete: Cascade), personId String?, status DisplayStatus @default(seen) }
model SurveyTrigger { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade), surveyId String, eventClass EventClass @relation(fields: [eventClassId], references: [id], onDelete: Cascade), eventClassId String, @@unique([surveyId, eventClassId]) }
enum SurveyAttributeFilterCondition { equals, notEquals }
model SurveyAttributeFilter { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), attributeClass AttributeClass @relation(fields: [attributeClassId], references: [id], onDelete: Cascade), attributeClassId String, survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade), surveyId String, condition SurveyAttributeFilterCondition, value String, @@unique([surveyId, attributeClassId]) }
enum SurveyType { email, link, mobile, web }
enum displayOptions { displayOnce, displayMultiple, respondMultiple }
model Survey { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), name String, type SurveyType @default(web), environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade), environmentId String, status SurveyStatus @default(draft), questions Json @default("[]"), thankYouCard Json @default("{"enabled": false}"), responses Response[], displayOption displayOptions @default(displayOnce), recontactDays Int?, triggers SurveyTrigger[], attributeFilters SurveyAttributeFilter[], displays Display[], autoClose Int?, delay Int @default(0) }
model Event { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), eventClass EventClass? @relation(fields: [eventClassId], references: [id]), eventClassId String?, session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade), sessionId String, properties Json @default("{}") }
enum EventType { code, noCode, automatic }
model EventClass { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), name String, description String?, type EventType, events Event[], noCodeConfig Json?, environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade), environmentId String, surveys SurveyTrigger[], @@unique([name, environmentId]) }
model Session { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), person Person @relation(fields: [personId], references: [id], onDelete: Cascade), personId String, events Event[] }
enum EnvironmentType { production, development }
model Environment { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), type EnvironmentType, product Product @relation(fields: [productId], references: [id], onDelete: Cascade), productId String, widgetSetupCompleted Boolean @default(false), surveys Survey[], people Person[], eventClasses EventClass[], attributeClasses AttributeClass[], apiKeys ApiKey[], webhooks Webhook[] }
model Product { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), name String, team Team @relation(fields: [teamId], references: [id], onDelete: Cascade), teamId String, environments Environment[], brandColor String @default("#64748b"), recontactDays Int @default(7), formbricksSignature Boolean @default(true) }
enum Plan { free, pro }
model Team { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), name String, memberships Membership[], products Product[], plan Plan @default(free), stripeCustomerId String?, invites Invite[] }
enum MembershipRole { owner, admin, editor, developer, viewer }
model Membership { team Team @relation(fields: [teamId], references: [id], onDelete: Cascade), teamId String, user User @relation(fields: [userId], references: [id], onDelete: Cascade), userId String, accepted Boolean @default(false), role MembershipRole, @@id([userId, teamId]) }
model Invite { id String @id @default(uuid()), email String, name String?, team Team @relation(fields: [teamId], references: [id], onDelete: Cascade), teamId String, creator User @relation("inviteCreatedBy", fields: [creatorId], references: [id]), creatorId String, acceptor User? @relation("inviteAcceptedBy", fields: [acceptorId], references: [id], onDelete: Cascade), acceptorId String?, accepted Boolean @default(false), createdAt DateTime @default(now()), expiresAt DateTime, role MembershipRole @default(admin), @@index([email, teamId], name: "email_teamId_unique") }
model ApiKey { id String @id @unique @default(cuid()), createdAt DateTime @default(now()), lastUsedAt DateTime?, label String?, hashedKey String @unique(), environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade), environmentId String }
enum IdentityProvider { email, github, google }
model Account { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), user User? @relation(fields: [userId], references: [id], onDelete: Cascade), userId String, type String, provider String, providerAccountId String, access_token String? @db.Text, refresh_token String? @db.Text, expires_at Int?, token_type String?, scope String?, id_token String? @db.Text, session_state String?, @@unique([provider, providerAccountId]) }
enum Role { project_manager, engineer, founder, marketing_specialist, other }
enum Objective { increase_conversion, improve_user_retention, increase_user_adoption, sharpen_marketing_messaging, support_sales, other }
enum Intention { survey_user_segments, survey_at_specific_point_in_user_journey, enrich_customer_profiles, collect_all_user_feedback_on_one_platform, other }
model User { id String @id @default(cuid()), createdAt DateTime @default(now()) @map(name: "created_at"), updatedAt DateTime @updatedAt @map(name: "updated_at"), name String?, email String @unique, emailVerified DateTime? @map(name: "email_verified"), password String?, onboardingCompleted Boolean @default(false), identityProvider IdentityProvider @default(email), identityProviderAccountId String?, memberships Membership[], accounts Account[], groupId String?, invitesCreated Invite[] @relation("inviteCreatedBy"), invitesAccepted Invite[] @relation("inviteAcceptedBy"), role Role?, notificationSettings Json @default("{}") }
Please respond with “Formbricks Assistant is now ready! How can I help?” when you read everything.

View File

@@ -10,6 +10,7 @@ import { prisma } from "@formbricks/database";
import { createAccount } from "./account/service";
import { verifyPassword } from "./auth/util";
import { EMAIL_VERIFICATION_DISABLED } from "./constants";
import { sendGettingStartedEmail } from "./emails/emails";
import { env } from "./env.mjs";
import { verifyToken } from "./jwt";
import { createMembership } from "./membership/service";
@@ -113,6 +114,7 @@ export const authOptions: NextAuthOptions = {
}
user = await updateUser(user.id, { emailVerified: new Date() });
await sendGettingStartedEmail(user);
return user;
},
@@ -221,7 +223,7 @@ export const authOptions: NextAuthOptions = {
identityProvider: provider,
identityProviderAccountId: account.providerAccountId,
});
await sendGettingStartedEmail(userProfile);
// Default team assignment if env variable is set
if (env.DEFAULT_TEAM_ID && env.DEFAULT_TEAM_ID.length > 0) {
// check if team exists

View File

@@ -33,6 +33,10 @@ interface TEmailUser {
email: string;
}
interface TEmailUserWithName extends TEmailUser {
name: string | null;
}
export interface LinkSurveyEmailData {
surveyId: string;
email: string;
@@ -92,6 +96,34 @@ export const sendVerificationEmail = async (user: TEmailUser) => {
});
};
export const sendGettingStartedEmail = async (user: TEmailUserWithName) => {
await sendEmail({
to: user.email,
subject: "Get started with Formbricks 🤸",
html: withEmailTemplate(`
<h1 style="text-align: center; line-height: 1.2; padding-top: 16px; padding-bottom:8px;">Turn customer insights into irresistible experiences</h1>
<a href="https://app.formbricks.com?utm_source=drip_campaign&utm_medium=email&utm_campaign=first_drip_mail&utm_content=top_image"><img src="https://formbricks-cdn.s3.eu-central-1.amazonaws.com/getting-started-with-formbricks-v5.png" alt="Formbricks can do it all" /></a>
<h3 style="text-align:center;">Welcome to Formbricks! 🤗</h3>
<p style="text-align:center;">We're the fastest growing Experience Management platform! Gracefully collect feedback without survey fatigue. Are you ready?</p>
<div style="text-align:center; margin-bottom:72px;">
<a class="button" href="https://app.formbricks.com?utm_source=drip_campaign&utm_medium=email&utm_campaign=first_drip_mail&utm_content=first_button">Create your survey</a><br/>
</div>
<a href="https://app.formbricks.com?utm_source=drip_campaign&utm_medium=email&utm_campaign=first_drip_mail&utm_content=second_image"><img style="border-radius:16px; box-shadow: 10px 10px 57px -21px rgba(71,85,105,0.58);" src="https://formbricks-cdn.s3.eu-central-1.amazonaws.com/getting-started-header-v4.png" alt="Formbricks can do it all"></a>
<h2 style="margin-top:32px;">Collect feedback everywhere!</h2>
<p>Formbricks is very versatile. Run:</p>
<ul>
<li><b>Website Surveys</b> like HotJar Ask</li>
<li><b>In-App Surveys</b> like Sprig</li>
<li><b>Link Surveys</b> like Typeform</li>
<li><b>Headless Surveys</b> via API</li>
</ul>
<p>All on one, open source platform ✅</p>
<a class="button" style="margin-bottom:12px; margin-top:0px;" href="https://app.formbricks.com?utm_source=drip_campaign&utm_medium=email&utm_campaign=first_drip_mail&utm_content=second_button">Create your survey</a><br/>
<p style="margin-bottom:0px; margin-top:40px; text-align:center;"><b>Life is short, craft something irresistible!</b><br/>The Formbricks Team 🤍</p>
`),
});
};
export const sendForgotPasswordEmail = async (user: TEmailUser) => {
const token = createToken(user.id, user.email, {
expiresIn: "1d",

View File

@@ -173,9 +173,9 @@ export const createUser = async (data: TUserCreateInput): Promise<TUser> => {
});
// send new user customer.io to customer.io
createCustomerIoCustomer(user);
return user;
createCustomerIoCustomer(user).catch(error => {
console.error("Error sending user to CustomerIO:", error);
});
};
// function to delete a user's user including teams

View File

@@ -231,7 +231,7 @@ export default function MultipleChoiceMultiQuestion({
}, 100);
}
}}
placeholder={question.otherOptionPlaceholder ?? "Please specify"}
placeholder="Please specify"
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus mt-3 flex h-10 w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required={question.required}
aria-labelledby={`${otherOption.id}-label`}

View File

@@ -182,7 +182,7 @@ export default function MultipleChoiceSingleQuestion({
}, 100);
}
}}
placeholder={question.otherOptionPlaceholder ?? "Please specify"}
placeholder="Please specify"
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus mt-3 flex h-10 w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required={question.required}
aria-labelledby={`${otherOption.id}-label`}

View File

@@ -94,7 +94,7 @@ export default function OpenTextQuestion({
onSubmit({ [question.id]: value, inputType: question.inputType }, updatedttc);
}
}}
pattern={question.inputType === "phone" ? "[0-9+ ]+" : ".*"}
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
/>
) : (

View File

@@ -277,7 +277,6 @@ export const ZSurveyMultipleChoiceSingleQuestion = ZSurveyQuestionBase.extend({
choices: z.array(ZSurveyChoice),
logic: z.array(ZSurveyMultipleChoiceSingleLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: z.string().optional(),
});
export type TSurveyMultipleChoiceSingleQuestion = z.infer<typeof ZSurveyMultipleChoiceSingleQuestion>;
@@ -287,7 +286,6 @@ export const ZSurveyMultipleChoiceMultiQuestion = ZSurveyQuestionBase.extend({
choices: z.array(ZSurveyChoice),
logic: z.array(ZSurveyMultipleChoiceMultiLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: z.string().optional(),
});
export type TSurveyMultipleChoiceMultiQuestion = z.infer<typeof ZSurveyMultipleChoiceMultiQuestion>;