diff --git a/apps/demo-react-native/.env.example b/apps/demo-react-native/.env.example new file mode 100644 index 0000000000..3a2d97bdc4 --- /dev/null +++ b/apps/demo-react-native/.env.example @@ -0,0 +1,2 @@ +EXPO_PUBLIC_API_HOST=http://192.168.178.20:3000 +EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=clzr04nkd000bcdl110j0ijyq diff --git a/apps/demo-react-native/.eslintrc.js b/apps/demo-react-native/.eslintrc.js new file mode 100644 index 0000000000..4d8dbbccec --- /dev/null +++ b/apps/demo-react-native/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ["@formbricks/eslint-config/react.js"], + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir: __dirname, + }, +}; diff --git a/apps/demo-react-native/.gitignore b/apps/demo-react-native/.gitignore new file mode 100644 index 0000000000..05647d55c7 --- /dev/null +++ b/apps/demo-react-native/.gitignore @@ -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 diff --git a/apps/demo-react-native/.npmrc b/apps/demo-react-native/.npmrc new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/demo-react-native/app.json b/apps/demo-react-native/app.json new file mode 100644 index 0000000000..b7701d49b3 --- /dev/null +++ b/apps/demo-react-native/app.json @@ -0,0 +1,34 @@ +{ + "expo": { + "name": "react-native-demo", + "slug": "react-native-demo", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "jsEngine": "hermes", + "assetBundlePatterns": ["**/*"], + "ios": { + "supportsTablet": true, + "infoPlist": { + "NSCameraUsageDescription": "Take pictures for certain activities.", + "NSPhotoLibraryUsageDescription": "Select pictures for certain activities.", + "NSMicrophoneUsageDescription": "Need microphone access for recording videos." + } + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} diff --git a/apps/demo-react-native/assets/adaptive-icon.png b/apps/demo-react-native/assets/adaptive-icon.png new file mode 100644 index 0000000000..03d6f6b6c6 Binary files /dev/null and b/apps/demo-react-native/assets/adaptive-icon.png differ diff --git a/apps/demo-react-native/assets/favicon.png b/apps/demo-react-native/assets/favicon.png new file mode 100644 index 0000000000..e75f697b18 Binary files /dev/null and b/apps/demo-react-native/assets/favicon.png differ diff --git a/apps/demo-react-native/assets/icon.png b/apps/demo-react-native/assets/icon.png new file mode 100644 index 0000000000..a0b1526fc7 Binary files /dev/null and b/apps/demo-react-native/assets/icon.png differ diff --git a/apps/demo-react-native/assets/splash.png b/apps/demo-react-native/assets/splash.png new file mode 100644 index 0000000000..0e89705a94 Binary files /dev/null and b/apps/demo-react-native/assets/splash.png differ diff --git a/apps/demo-react-native/babel.config.js b/apps/demo-react-native/babel.config.js new file mode 100644 index 0000000000..29433509d7 --- /dev/null +++ b/apps/demo-react-native/babel.config.js @@ -0,0 +1,6 @@ +module.exports = function babel(api) { + api.cache(true); + return { + presets: ["babel-preset-expo"], + }; +}; diff --git a/apps/demo-react-native/index.js b/apps/demo-react-native/index.js new file mode 100644 index 0000000000..c2ccbfc1d6 --- /dev/null +++ b/apps/demo-react-native/index.js @@ -0,0 +1,7 @@ +import { registerRootComponent } from "expo"; +import { LogBox } from "react-native"; +import App from "./src/app"; + +registerRootComponent(App); + +LogBox.ignoreAllLogs(); diff --git a/apps/demo-react-native/metro.config.js b/apps/demo-react-native/metro.config.js new file mode 100644 index 0000000000..6bd167c023 --- /dev/null +++ b/apps/demo-react-native/metro.config.js @@ -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; diff --git a/apps/demo-react-native/package.json b/apps/demo-react-native/package.json new file mode 100644 index 0000000000..186d52e822 --- /dev/null +++ b/apps/demo-react-native/package.json @@ -0,0 +1,28 @@ +{ + "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:*", + "expo": "^51.0.26", + "expo-status-bar": "~1.12.1", + "react": "^18.2.0", + "react-native": "^0.74.4", + "react-native-webview": "13.8.6" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@types/react": "~18.2.79", + "typescript": "^5.3.3" + }, + "private": true +} diff --git a/apps/demo-react-native/src/app.tsx b/apps/demo-react-native/src/app.tsx new file mode 100644 index 0000000000..829fe1637c --- /dev/null +++ b/apps/demo-react-native/src/app.tsx @@ -0,0 +1,52 @@ +import { StatusBar } from "expo-status-bar"; +import { Button, LogBox, StyleSheet, Text, View } from "react-native"; +import Formbricks, { 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_API_HOST) { + throw new Error("EXPO_PUBLIC_API_HOST is required"); + } + + const config = { + environmentId: process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID, + apiHost: process.env.EXPO_PUBLIC_API_HOST, + userId: "random user id", + attributes: { + language: "en", + testAttr: "attr-test", + }, + }; + + return ( + + Formbricks React Native SDK Demo + + +

If you made a change in Formbricks app and it does not seem to work, hit 'Reset' and try again. diff --git a/apps/docs/app/app-surveys/framework-guides/components/Libraries.tsx b/apps/docs/app/app-surveys/framework-guides/components/Libraries.tsx index 1e36f9f798..33d8f064e5 100644 --- a/apps/docs/app/app-surveys/framework-guides/components/Libraries.tsx +++ b/apps/docs/app/app-surveys/framework-guides/components/Libraries.tsx @@ -31,12 +31,18 @@ const libraries = [ description: "Simply add us to your router change and sit back!", logo: logoVueJs, }, + { + href: "#react-native", + name: "React Native", + description: "Easily integrate our SDK with your React Native app for seamless survey support!", + logo: logoReactJs, + }, ]; export const Libraries = () => { return (

-
+
{libraries.map((library) => ( { Refer to our [Example VueJs project](https://github.com/formbricks/examples/tree/main/vuejs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! +## React Native + +Install the Formbricks React Native SDK using one of the package managers, i.e., npm, pnpm, or yarn. + + + +```shell {{ title: 'npm' }} +npm install @formbricks/react-native +``` +```shell {{ title: 'pnpm' }} +pnpm add @formbricks/react-native +``` +```shell {{ title: 'yarn' }} +yarn add @formbricks/react-native +``` + + + +Now, update your App.js/App.tsx file to initialize Formbricks: + + + +```js +// other imports +import Formbricks from "@formbricks/react-native"; + +const config = { + environmentId: "", + apiHost: "", + userId: "", +}; + +export default function App() { + return ( + <> + {/* Your app content */} + + + ); +} +``` + + + +### Required customizations to be made + + + + Formbricks Environment ID. + + + URL of the hosted Formbricks instance. + + + User ID of the user who has active session. + + + +--- + ## Validate your setup Once you have completed the steps above, you can validate your setup by checking the **Setup Checklist** in the Settings. Your widget status indicator should go from this: diff --git a/apps/docs/app/app-surveys/quickstart/page.mdx b/apps/docs/app/app-surveys/quickstart/page.mdx index be44997c3b..fb1a1ac7aa 100644 --- a/apps/docs/app/app-surveys/quickstart/page.mdx +++ b/apps/docs/app/app-surveys/quickstart/page.mdx @@ -19,10 +19,12 @@ export const metadata = { # Quickstart -App surveys have 6-10x better conversion rates than emailed out surveys. This tutorial explains how to run an app survey in your web app in 10 to 15 minutes. Let’s go! +App surveys have 6-10x better conversion rates than emailed surveys. This tutorial explains how to run a survey in both your web app and mobile app (React Native) in just 10 to 15 minutes. Let’s go! - App Surveys are ideal for websites that **have a user authentication** system. If you are looking to run surveys on your public facing website, head over to the [Website Surveys Quickstart Guide](/website-surveys/quickstart). + App Surveys are ideal for websites that **have a user authentication** system. If you are looking to run + surveys on your public facing website, head over to the [Website Surveys Quickstart + Guide](/website-surveys/quickstart). 1. **Create a free Formbricks Cloud account**: While you can [self-host](/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 you'll be guided to our onboarding like below: @@ -35,7 +37,7 @@ App surveys have 6-10x better conversion rates than emailed out surveys. This tu src={I1} alt="Choose website survey from survey type" quality="100" - className="max-w-full rounded-lg sm:max-w-3xl " + className="max-w-full rounded-lg sm:max-w-3xl" /> 2. **Connect your App/Website**: Once you get through a couple of onboarding steps, you’ll be asked to connect your app or website. This is where you’ll find the code snippet for both HTML as well as the npm package which you need to embed in your app: diff --git a/apps/docs/app/developer-docs/app-survey-rn-sdk/page.mdx b/apps/docs/app/developer-docs/app-survey-rn-sdk/page.mdx new file mode 100644 index 0000000000..f078f20322 --- /dev/null +++ b/apps/docs/app/developer-docs/app-survey-rn-sdk/page.mdx @@ -0,0 +1,127 @@ +import { MdxImage } from "@/components/MdxImage"; + +export const metadata = { + title: "Formbricks App Survey SDK", + description: + "An overview of all available methods & how to integrate Formbricks App Surveys for frontend developers in web applications. Learn the key methods, configuration settings, and best practices.", +}; + +#### Developer Docs + +# React Native SDK: App Survey + +### Overview + +The Formbricks React Native SDK can be used for seamlessly integrating App Surveys into your React Native Apps. In this section, we'll explore how to leverage the SDK for **app** surveys. It’s available on npm [here](https://www.npmjs.com/package/@formbricks/js/). + +### Install + + + + +```js {{ title: 'npm' }} +npm install @formbricks/react-native +``` + +```js {{ title: 'yarn' }} +yarn add @formbricks/react-native +``` + +```js {{ title: 'pnpm' }} +pnpm add @formbricks/react-native +``` + + + + +## Methods + +### Initialize Formbricks + +In your React Native app, initialize the Formbricks React Native Client for app surveys where you pass the userId (creates a user if not existing in Formbricks) to attribute & target the user based on their actions. + + + + +```javascript +// other imports +import Formbricks from "@formbricks/react-native"; + +const config = { + environmentId: "", + apiHost: "", + userId: "", +}; + +export default function App() { + return ( + <> + {/* Your app content */} + + + ); +} +``` + + + + +The moment you initialise Formbricks, your user will start seeing surveys that get triggered on simpler actions such as on New Session. + +### Set Attribute + +You can set custom attributes for the identified user. This can be helpful for segmenting users based on specific characteristics or properties. To learn how to set custom user attributes, please check out our [User Attributes Guide](/app-surveys/user-identification). + + + + +```js +formbricks.setAttribute("Plan", "Paid"); +``` + + + + +### Track Action + +Track user actions to trigger surveys based on user interactions, such as button clicks or scrolling: + + + + +```js +formbricks.track("Clicked on Claim"); +``` + + + + +### Logout + +To log out and deinitialize Formbricks, use the formbricks.logout() function. This action clears the current initialization configuration and erases stored frontend information, such as the surveys a user has viewed or completed. It's an important step when a user logs out of your application or when you want to reset Formbricks. + + + + +```js +formbricks.logout(); +``` + + + + +After calling formbricks.logout(), you'll need to reinitialize Formbricks before using any of its features again. Ensure that you properly reinitialize Formbricks to avoid unexpected errors or behavior in your application. + +### Reset + +Reset the current instance and fetch the latest surveys and state again: + + + + +```js +formbricks.reset(); +``` + + + diff --git a/apps/docs/app/developer-docs/contributing/get-started/page.mdx b/apps/docs/app/developer-docs/contributing/get-started/page.mdx index c97265afa1..87e3f5deac 100644 --- a/apps/docs/app/developer-docs/contributing/get-started/page.mdx +++ b/apps/docs/app/developer-docs/contributing/get-started/page.mdx @@ -106,7 +106,7 @@ sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env -- For Mac +- For Mac diff --git a/apps/docs/app/developer-docs/rest-api/page.mdx b/apps/docs/app/developer-docs/rest-api/page.mdx index 7bb30057da..7783a07063 100644 --- a/apps/docs/app/developer-docs/rest-api/page.mdx +++ b/apps/docs/app/developer-docs/rest-api/page.mdx @@ -39,7 +39,7 @@ We currently have the following Management API methods exposed and below is thei - [Me API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#79e08365-641d-4b2d-aea2-9a855e0438ec) - Retrieve Account Information - [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#cffc27a6-dafb-428f-8ea7-5165bedb911e) - List and Delete People - [Response API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#e544ec0d-8b30-4e33-8d35-2441cb40d676) - List, List by Survey, Update, and Delete Responses -- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, and Delete Surveys +- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, generate multiple suId and Delete Surveys - [Webhook API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c) - List, Create, and Delete Webhooks ## How to Generate an API key diff --git a/apps/docs/lib/navigation.ts b/apps/docs/lib/navigation.ts index b34c080d5b..d7439a1f12 100644 --- a/apps/docs/lib/navigation.ts +++ b/apps/docs/lib/navigation.ts @@ -139,8 +139,9 @@ export const navigation: Array = [ { title: "Zapier", href: "/developer-docs/integrations/zapier" }, ], }, - { title: "SDK: App Survey", href: "/developer-docs/app-survey-sdk" }, - { title: "SDK: Website Survey", href: "/developer-docs/website-survey-sdk" }, + { title: "JS SDK: App Survey", href: "/developer-docs/app-survey-sdk" }, + { title: "RN SDK: App Survey", href: "/developer-docs/app-survey-rn-sdk" }, + { title: "JS SDK: Website Survey", href: "/developer-docs/website-survey-sdk" }, { title: "SDK: Formbricks API", href: "/developer-docs/api-sdk" }, { title: "REST API", href: "/developer-docs/rest-api" }, { title: "Webhooks", href: "/developer-docs/webhooks" }, diff --git a/apps/web/app/(app)/(onboarding)/organizations/actions.ts b/apps/web/app/(app)/(onboarding)/organizations/actions.ts index 9d165e84e1..a3e3befaa8 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/actions.ts +++ b/apps/web/app/(app)/(onboarding)/organizations/actions.ts @@ -6,7 +6,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { INVITE_DISABLED } from "@formbricks/lib/constants"; import { inviteUser } from "@formbricks/lib/invite/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { AuthenticationError } from "@formbricks/types/errors"; import { ZMembershipRole } from "@formbricks/types/memberships"; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts index c54d52cffb..47ed02dc8d 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts @@ -21,7 +21,7 @@ import { import { surveyCache } from "@formbricks/lib/survey/cache"; import { loadNewSegmentInSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { ZActionClassInput } from "@formbricks/types/action-classes"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { ZBaseFilters, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment"; import { ZSurvey } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx index ed3d435408..e9f3d4e943 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx @@ -8,7 +8,7 @@ interface ColorSurveyBgProps { } export const ColorSurveyBg = ({ handleBgChange, colors, background }: ColorSurveyBgProps) => { - const [color, setColor] = useState(background || "#ffff"); + const [color, setColor] = useState(background || "#FFFFFF"); const handleBg = (x: string) => { setColor(x); @@ -23,7 +23,7 @@ export const ColorSurveyBg = ({ handleBgChange, colors, background }: ColorSurve {colors.map((x) => { return (
-

{endingCard.type === "endScreen" ? "🙏" : "↪️"}

+
+ {endingCard.type === "endScreen" ? ( + + ) : ( + + )} +
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx index 4b7258e488..30e6d3ea6d 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx @@ -1,6 +1,7 @@ "use client"; import * as Collapsible from "@radix-ui/react-collapsible"; +import { Hand } from "lucide-react"; import { usePathname } from "next/navigation"; import { useState } from "react"; import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor"; @@ -66,7 +67,7 @@ export const EditWelcomeCard = ({ "flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none", isInvalid ? "bg-red-400" : "bg-white group-hover:bg-slate-50" )}> -

+
{ + const updateSurvey = (data: TSurveyHiddenFields, currentFieldId?: string) => { + const questions = [...localSurvey.questions]; + + // Remove recall info from question headlines + if (currentFieldId) { + questions.forEach((question) => { + for (const [languageCode, headline] of Object.entries(question.headline)) { + if (headline.includes(`recall:${currentFieldId}`)) { + const recallInfo = extractRecallInfo(headline); + if (recallInfo) { + question.headline[languageCode] = headline.replace(recallInfo, ""); + } + } + } + }); + } + setLocalSurvey({ ...localSurvey, + questions, hiddenFields: { ...localSurvey.hiddenFields, ...data, @@ -53,7 +72,7 @@ export const HiddenFieldsCard = ({ open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50", "flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none" )}> -

🥷

+
{ - updateSurvey({ - enabled: true, - fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId), - }); + updateSurvey( + { + enabled: true, + fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId), + }, + fieldId + ); }} tagId={fieldId} tagName={fieldId} diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx index db4add1b33..c2e710a9aa 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx @@ -18,7 +18,7 @@ const tabs: Tab[] = [ { id: "styling", label: "Styling", - icon: , + icon: , }, { id: "settings", @@ -46,7 +46,7 @@ export const QuestionsAudienceTabs = ({ }, [isStylingTabVisible]); return ( -
+
{responseCount > 0 && ( -
+
@@ -318,7 +320,9 @@ export const SurveyMenuBar = ({ -

{cautionText}

+

+ {cautionText} +

)}
@@ -332,6 +336,7 @@ export const SurveyMenuBar = ({ + )} + + {mode === "edit" && variable && ( + + )} +
+ + {isNameError &&

{errors.name?.message}

} + + +
+ ); +}; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/lib/minimalSurvey.ts b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/lib/minimalSurvey.ts index 428eab5de9..4e4920f56e 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/lib/minimalSurvey.ts +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/lib/minimalSurvey.ts @@ -37,4 +37,5 @@ export const minimalSurvey: TSurvey = { languages: [], showLanguageSwitch: false, isVerifyEmailEnabled: false, + variables: [], }; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx index 1de26ec9ec..9349a84cbe 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx @@ -1,6 +1,9 @@ import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironment } from "@formbricks/lib/environment/service"; +import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; +import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { getUser } from "@formbricks/lib/user/service"; import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product"; @@ -43,6 +46,15 @@ const Page = async ({ params, searchParams }: SurveyTemplateProps) => { if (!environment) { throw new Error("Environment not found"); } + const currentUserMembership = await getMembershipByUserIdOrganizationId( + session?.user.id, + product.organizationId + ); + const { isViewer } = getAccessFlags(currentUserMembership?.role); + + if (isViewer) { + return redirect(`/environments/${environment.id}/surveys`); + } const prefilledFilters = [product.config.channel, product.config.industry, searchParams.role ?? null]; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/actions.ts index 2ccdc320af..a9ebeacc9d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/actions.ts @@ -9,7 +9,7 @@ import { } from "@formbricks/lib/organization/utils"; import { getSegmentsByAttributeClassName } from "@formbricks/lib/segment/service"; import { ZAttributeClass } from "@formbricks/types/attribute-classes"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; const ZGetSegmentsByAttributeClassAction = z.object({ environmentId: ZId, diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/actions.ts index 6174f60f57..071378add9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/actions.ts @@ -5,7 +5,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { getOrganizationIdFromPersonId } from "@formbricks/lib/organization/utils"; import { deletePerson } from "@formbricks/lib/person/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; const ZPersonDeleteAction = z.object({ personId: ZId, diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/actions.ts index fac8a1012c..a998ef92fc 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/actions.ts @@ -5,7 +5,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { getOrganizationIdFromSegmentId } from "@formbricks/lib/organization/utils"; import { deleteSegment, updateSegment } from "@formbricks/lib/segment/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment"; const ZDeleteBasicSegmentAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts index a5d1fc8a86..76e7b33e6c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts @@ -8,7 +8,7 @@ import { createMembership } from "@formbricks/lib/membership/service"; import { createOrganization } from "@formbricks/lib/organization/service"; import { createProduct } from "@formbricks/lib/product/service"; import { updateUser } from "@formbricks/lib/user/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { OperationNotAllowedError } from "@formbricks/types/errors"; import { ZProductUpdateInput } from "@formbricks/types/product"; import { TUserNotificationSettings } from "@formbricks/types/user"; diff --git a/apps/web/app/(app)/environments/[environmentId]/actions/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions/actions.ts index 953797f02e..3ff5965856 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions/actions.ts @@ -7,7 +7,7 @@ import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { getOrganizationIdFromActionClassId } from "@formbricks/lib/organization/utils"; import { getSurveysByActionClassId } from "@formbricks/lib/survey/service"; import { ZActionClassInput } from "@formbricks/types/action-classes"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { ResourceNotFoundError } from "@formbricks/types/errors"; const ZDeleteActionClassAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx index ec311d0236..d3238597cf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx @@ -102,6 +102,7 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En environment={environment} environments={environments} currentProductChannel={currentProductChannel} + membershipRole={currentUserMembership?.role} />
{children}
diff --git a/apps/web/app/(app)/environments/[environmentId]/components/TopControlBar.tsx b/apps/web/app/(app)/environments/[environmentId]/components/TopControlBar.tsx index 02055cb50a..bcd7c99fd2 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/TopControlBar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/TopControlBar.tsx @@ -2,15 +2,22 @@ import { TopControlButtons } from "@/app/(app)/environments/[environmentId]/comp import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { TEnvironment } from "@formbricks/types/environment"; +import { TMembershipRole } from "@formbricks/types/memberships"; import { TProductConfigChannel } from "@formbricks/types/product"; interface SideBarProps { environment: TEnvironment; environments: TEnvironment[]; currentProductChannel: TProductConfigChannel; + membershipRole?: TMembershipRole; } -export const TopControlBar = ({ environment, environments, currentProductChannel }: SideBarProps) => { +export const TopControlBar = ({ + environment, + environments, + currentProductChannel, + membershipRole, +}: SideBarProps) => { return (
@@ -28,6 +35,7 @@ export const TopControlBar = ({ environment, environments, currentProductChannel environment={environment} environments={environments} isFormbricksCloud={IS_FORMBRICKS_CLOUD} + membershipRole={membershipRole} />
diff --git a/apps/web/app/(app)/environments/[environmentId]/components/TopControlButtons.tsx b/apps/web/app/(app)/environments/[environmentId]/components/TopControlButtons.tsx index b52e7d722f..c8d10049a4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/TopControlButtons.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/TopControlButtons.tsx @@ -5,18 +5,21 @@ import { CircleUserIcon, MessageCircleQuestionIcon, PlusIcon } from "lucide-reac import { useRouter } from "next/navigation"; import formbricks from "@formbricks/js/app"; import { TEnvironment } from "@formbricks/types/environment"; +import { TMembershipRole } from "@formbricks/types/memberships"; import { Button } from "@formbricks/ui/Button"; interface TopControlButtonsProps { environment: TEnvironment; environments: TEnvironment[]; isFormbricksCloud: boolean; + membershipRole?: TMembershipRole; } export const TopControlButtons = ({ environment, environments, isFormbricksCloud, + membershipRole, }: TopControlButtonsProps) => { const router = useRouter(); return ( @@ -44,16 +47,18 @@ export const TopControlButtons = ({ }}> - + {membershipRole && membershipRole !== "viewer" ? ( + + ) : null}
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts index a962579a6f..beeba66026 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts @@ -5,7 +5,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service"; import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { ZIntegrationInput } from "@formbricks/types/integration"; const ZCreateOrUpdateIntegrationAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts index 3bbbb6a732..b96fcb68e0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts @@ -5,7 +5,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils"; import { getSlackChannels } from "@formbricks/lib/slack/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; const ZRefreshChannelsAction = z.object({ environmentId: ZId, diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts index 380ceaa288..4d0d0f1c40 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts @@ -9,7 +9,7 @@ import { } from "@formbricks/lib/organization/utils"; import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service"; import { testEndpoint } from "@formbricks/lib/webhook/utils"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { ZWebhookInput } from "@formbricks/types/webhooks"; const ZCreateWebhookAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/product/actions.ts b/apps/web/app/(app)/environments/[environmentId]/product/actions.ts index c9ec85c4c2..d610f773a7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/product/actions.ts @@ -5,7 +5,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { getOrganizationIdFromProductId } from "@formbricks/lib/organization/utils"; import { updateProduct } from "@formbricks/lib/product/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { ZProductUpdateInput } from "@formbricks/types/product"; const ZUpdateProductAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/product/api-keys/actions.ts b/apps/web/app/(app)/environments/[environmentId]/product/api-keys/actions.ts index 0ee2a1c848..225f5785ec 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/api-keys/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/product/api-keys/actions.ts @@ -9,7 +9,7 @@ import { getOrganizationIdFromEnvironmentId, } from "@formbricks/lib/organization/utils"; import { ZApiKeyCreateInput } from "@formbricks/types/api-keys"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; const ZDeleteApiKeyAction = z.object({ id: ZId, diff --git a/apps/web/app/(app)/environments/[environmentId]/product/general/actions.ts b/apps/web/app/(app)/environments/[environmentId]/product/general/actions.ts index 75818d75a3..712518245d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/general/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/product/general/actions.ts @@ -5,7 +5,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { getOrganizationIdFromProductId } from "@formbricks/lib/organization/utils"; import { deleteProduct, getProducts } from "@formbricks/lib/product/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; const ZProductDeleteAction = z.object({ productId: ZId, diff --git a/apps/web/app/(app)/environments/[environmentId]/product/look/components/EditLogo.tsx b/apps/web/app/(app)/environments/[environmentId]/product/look/components/EditLogo.tsx index a935e2a401..908d9b1849 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/look/components/EditLogo.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/look/components/EditLogo.tsx @@ -120,7 +120,8 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) => allowedFileExtensions={["png", "jpeg", "jpg"]} environmentId={environmentId} onFileUpload={(files: string[]) => { - setLogoUrl(files[0]), setIsEditing(true); + setLogoUrl(files[0]); + setIsEditing(true); }} /> )} diff --git a/apps/web/app/(app)/environments/[environmentId]/product/look/components/ThemeStylingPreviewSurvey.tsx b/apps/web/app/(app)/environments/[environmentId]/product/look/components/ThemeStylingPreviewSurvey.tsx index 1d7682b08c..7d525f502d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/look/components/ThemeStylingPreviewSurvey.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/look/components/ThemeStylingPreviewSurvey.tsx @@ -108,8 +108,6 @@ export const ThemeStylingPreviewSurvey = ({ setQuestionId(survey?.questions[0]?.id); }; - const onFileUpload = async (file: File) => file.name; - const isAppSurvey = previewType === "app" || previewType === "website"; const scrollToEditLogoSection = () => { @@ -168,7 +166,7 @@ export const ThemeStylingPreviewSurvey = ({ survey={{ ...survey, type: "app" }} isBrandingEnabled={product.inAppSurveyBranding} isRedirectDisabled={true} - onFileUpload={onFileUpload} + onFileUpload={async (file) => file.name} styling={product.styling} isCardBorderVisible={!highlightBorderColor} languageCode="default" @@ -190,7 +188,7 @@ export const ThemeStylingPreviewSurvey = ({ survey={{ ...survey, type: "link" }} isBrandingEnabled={product.linkSurveyBranding} isRedirectDisabled={true} - onFileUpload={onFileUpload} + onFileUpload={async (file) => file.name} responseCount={42} styling={product.styling} languageCode="default" diff --git a/apps/web/app/(app)/environments/[environmentId]/product/tags/actions.ts b/apps/web/app/(app)/environments/[environmentId]/product/tags/actions.ts index a3b821a9f5..cb37fa90da 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/tags/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/product/tags/actions.ts @@ -5,7 +5,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { getOrganizationIdFromTagId } from "@formbricks/lib/organization/utils"; import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; const ZDeleteTagAction = z.object({ tagId: ZId, diff --git a/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx index 6d5b65cbac..30b4ec1e7e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx @@ -57,7 +57,7 @@ const Page = async ({ params }) => { productChannel={currentProductChannel} /> - + { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts index 1b14aa6a31..7c1b0b4f19 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts @@ -8,7 +8,7 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient"; import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; import { getOrganizationIdFromSurveyId } from "@formbricks/lib/organization/utils"; import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; -import { ZId } from "@formbricks/types/environment"; +import { ZId } from "@formbricks/types/common"; import { ResourceNotFoundError } from "@formbricks/types/errors"; const ZSendEmbedSurveyPreviewEmailAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx index a03a3368e4..59359edfe5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx @@ -39,7 +39,7 @@ export const MultipleChoiceSummary = ({ setFilter, }: MultipleChoiceSummaryProps) => { const [visibleOtherResponses, setVisibleOtherResponses] = useState(10); - + const otherValue = questionSummary.question.choices.find((choice) => choice.id === "other")?.label.default; // sort by count and transform to array const results = Object.values(questionSummary.choices).sort((a, b) => { if (a.others) return 1; // Always put a after b if a has 'others' @@ -77,7 +77,9 @@ export const MultipleChoiceSummary = ({ questionSummary.question.id, questionSummary.question.headline, questionSummary.question.type, - questionSummary.type === "multipleChoiceSingle" ? "Includes either" : "Includes all", + questionSummary.type === "multipleChoiceSingle" || otherValue === result.value + ? "Includes either" + : "Includes all", [result.value] ) }> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx index b7ac80352c..2f8ceb5ab9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx @@ -59,7 +59,7 @@ export const OpenTextSummary = ({ survey={survey} attributeClasses={attributeClasses} /> - + {isAiEnabled && }
{activeTab === "insights" ? ( diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx index e2a4680ec6..9bc3d4f971 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx @@ -70,13 +70,14 @@ export const SurveyAnalysisCTA = ({ onClick={() => { setOpenShareSurveyModal(true); }}> - + Share + )} {!isViewer && (