diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index c2b32d296504..18f3035f160f 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -2104,6 +2104,7 @@ checksums: environments/workspace/look/show_powered_by_formbricks: a0e96edadec8ef326423feccc9d06be7 environments/workspace/look/styling_updated_successfully: b8b74b50dde95abcd498633e9d0c891f environments/workspace/look/suggest_colors: ddc4543b416ab774007b10a3434343cd + environments/workspace/look/suggested_colors_applied_please_save: 226fa70af5efc8ffa0a3755909c8163e environments/workspace/look/theme: 21fe00b7a518089576fb83c08631107a environments/workspace/look/theme_settings_description: 9fc45322818c3774ab4a44ea14d7836e environments/workspace/tags/add: 87c4a663507f2bcbbf79934af8164e13 diff --git a/apps/web/lib/utils/validate.ts b/apps/web/lib/utils/validate.ts index 06cb149cf524..7031da202149 100644 --- a/apps/web/lib/utils/validate.ts +++ b/apps/web/lib/utils/validate.ts @@ -12,11 +12,18 @@ export function validateInputs[]>( for (const [value, schema] of pairs) { const inputValidation = schema.safeParse(value); if (!inputValidation.success) { + const zodDetails = inputValidation.error.issues + .map((issue) => { + const path = issue?.path?.join(".") ?? ""; + return `${path}${issue.message}`; + }) + .join("; "); + logger.error( inputValidation.error, `Validation failed for ${JSON.stringify(value).substring(0, 100)} and ${JSON.stringify(schema)}` ); - throw new ValidationError("Validation failed"); + throw new ValidationError(`Validation failed: ${zodDetails}`); } parsedData.push(inputValidation.data); } diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index e7c198506db9..9a3083283244 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "\"Powered by Formbricks\"-Signatur anzeigen", "styling_updated_successfully": "Styling erfolgreich aktualisiert", "suggest_colors": "Farben vorschlagen", + "suggested_colors_applied_please_save": "Vorgeschlagene Farben erfolgreich generiert. Drücke \"Speichern\", um die Änderungen zu übernehmen.", "theme": "Theme", "theme_settings_description": "Erstelle ein Style-Theme für alle Umfragen. Du kannst für jede Umfrage individuelles Styling aktivieren." }, diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index a4b5a45633df..fb6250156b8a 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Show “Powered by Formbricks” Signature", "styling_updated_successfully": "Styling updated successfully", "suggest_colors": "Suggest colors", + "suggested_colors_applied_please_save": "Suggested colors generated successfully. Press \"Save\" to persist the changes.", "theme": "Theme", "theme_settings_description": "Create a style theme for all surveys. You can enable custom styling for each survey." }, diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index 781303f9996e..523bf17b0217 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Mostrar firma 'Powered by Formbricks'", "styling_updated_successfully": "Estilo actualizado correctamente", "suggest_colors": "Sugerir colores", + "suggested_colors_applied_please_save": "Colores sugeridos generados correctamente. Pulsa \"Guardar\" para conservar los cambios.", "theme": "Tema", "theme_settings_description": "Crea un tema de estilo para todas las encuestas. Puedes activar el estilo personalizado para cada encuesta." }, diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index 85200ca24fe9..38cadb6e9367 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Afficher la signature « Propulsé par Formbricks »", "styling_updated_successfully": "Style mis à jour avec succès", "suggest_colors": "Suggérer des couleurs", + "suggested_colors_applied_please_save": "Couleurs suggérées générées avec succès. Appuyez sur « Enregistrer » pour conserver les modifications.", "theme": "Thème", "theme_settings_description": "Créez un thème de style pour toutes les enquêtes. Vous pouvez activer un style personnalisé pour chaque enquête." }, diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index 443b6192bd44..9fa18f822d15 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Az „A gépházban: Formbricks” aláírás megjelenítése", "styling_updated_successfully": "A stílus sikeresen frissítve", "suggest_colors": "Színek ajánlása", + "suggested_colors_applied_please_save": "A javasolt színek sikeresen generálva. Nyomd meg a \"Mentés\" gombot a változtatások véglegesítéséhez.", "theme": "Téma", "theme_settings_description": "Stílustéma létrehozása az összes kérdőívhez. Egyéni stílust engedélyezhet minden egyes kérdőívhez." }, diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index c94addb0def4..c5f83af67ca7 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "「Powered by Formbricks」署名を表示", "styling_updated_successfully": "スタイルを正常に更新しました", "suggest_colors": "カラーを提案", + "suggested_colors_applied_please_save": "推奨カラーが正常に生成されました。変更を保存するには「保存」を押してください。", "theme": "テーマ", "theme_settings_description": "すべてのアンケート用のスタイルテーマを作成します。各アンケートでカスタムスタイルを有効にできます。" }, diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index 41d73405b7ff..de434eb92d10 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Toon 'Powered by Formbricks' handtekening", "styling_updated_successfully": "Styling succesvol bijgewerkt", "suggest_colors": "Kleuren voorstellen", + "suggested_colors_applied_please_save": "Voorgestelde kleuren succesvol gegenereerd. Druk op \"Opslaan\" om de wijzigingen te behouden.", "theme": "Thema", "theme_settings_description": "Maak een stijlthema voor alle enquêtes. Je kunt aangepaste styling inschakelen voor elke enquête." }, diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 07c3a510dc38..91483066a4b2 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'", "styling_updated_successfully": "Estilo atualizado com sucesso", "suggest_colors": "Sugerir cores", + "suggested_colors_applied_please_save": "Cores sugeridas geradas com sucesso. Pressione \"Salvar\" para manter as alterações.", "theme": "Tema", "theme_settings_description": "Crie um tema de estilo para todas as pesquisas. Você pode ativar estilo personalizado para cada pesquisa." }, diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 8d62c2df9b83..cb475dc375d8 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'", "styling_updated_successfully": "Estilo atualizado com sucesso", "suggest_colors": "Sugerir cores", + "suggested_colors_applied_please_save": "Cores sugeridas geradas com sucesso. Pressiona \"Guardar\" para manter as alterações.", "theme": "Tema", "theme_settings_description": "Crie um tema de estilo para todos os inquéritos. Pode ativar estilos personalizados para cada inquérito." }, diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 8be82c27311b..51e979d261f2 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Afișează semnătura „Powered by Formbricks”", "styling_updated_successfully": "Stilizarea a fost actualizată cu succes", "suggest_colors": "Sugerează culori", + "suggested_colors_applied_please_save": "Culorile sugerate au fost generate cu succes. Apasă pe „Salvează” pentru a păstra modificările.", "theme": "Temă", "theme_settings_description": "Creează o temă de stil pentru toate sondajele. Poți activa stilizare personalizată pentru fiecare sondaj." }, diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index ae9ae663f8d5..6640bd6e8b99 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Показывать подпись «Работает на Formbricks»", "styling_updated_successfully": "Стили успешно обновлены", "suggest_colors": "Предложить цвета", + "suggested_colors_applied_please_save": "Рекомендованные цвета успешно сгенерированы. Нажми «Сохранить», чтобы применить изменения.", "theme": "Тема", "theme_settings_description": "Создайте стиль для всех опросов. Вы можете включить индивидуальное оформление для каждого опроса." }, diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index 67c3e6dbc57b..519b345fbe89 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "Visa 'Powered by Formbricks'-signatur", "styling_updated_successfully": "Stiluppdatering lyckades", "suggest_colors": "Föreslå färger", + "suggested_colors_applied_please_save": "Föreslagna färger har skapats. Tryck på \"Spara\" för att spara ändringarna.", "theme": "Tema", "theme_settings_description": "Skapa ett stilmall för alla undersökningar. Du kan aktivera anpassad stil för varje undersökning." }, diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 5e60067020b7..2469d5819ec9 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "显示“Powered by Formbricks”标识", "styling_updated_successfully": "样式更新成功", "suggest_colors": "推荐颜色", + "suggested_colors_applied_please_save": "已成功生成推荐配色。请点击“保存”以保留更改。", "theme": "主题", "theme_settings_description": "为所有问卷创建一个样式主题。你可以为每个问卷启用自定义样式。" }, diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index dd2125827c94..07d3db7264b3 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -2221,6 +2221,7 @@ "show_powered_by_formbricks": "顯示「Powered by Formbricks」標記", "styling_updated_successfully": "樣式已成功更新", "suggest_colors": "建議顏色", + "suggested_colors_applied_please_save": "已成功產生建議色彩。請按「儲存」以保存變更。", "theme": "主題", "theme_settings_description": "為所有調查建立樣式主題。您可以為每個調查啟用自訂樣式。" }, diff --git a/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/route.ts b/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/route.ts index 7da349faa285..7cccf236a7c6 100644 --- a/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/route.ts +++ b/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/route.ts @@ -2,13 +2,26 @@ import { NextRequest, userAgent } from "next/server"; import { logger } from "@formbricks/logger"; import { TContactAttributesInput } from "@formbricks/types/contact-attribute"; import { ZEnvironmentId } from "@formbricks/types/environment"; -import { ResourceNotFoundError } from "@formbricks/types/errors"; +import { ResourceNotFoundError, ValidationError } from "@formbricks/types/errors"; import { TJsPersonState } from "@formbricks/types/js"; import { responses } from "@/app/lib/api/response"; import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { updateUser } from "./lib/update-user"; +const handleError = (err: unknown, url: string): { response: Response } => { + if (err instanceof ResourceNotFoundError) { + return { response: responses.notFoundResponse(err.resourceType, err.resourceId) }; + } + + if (err instanceof ValidationError) { + return { response: responses.badRequestResponse(err.message, undefined, true) }; + } + + logger.error({ error: err, url }, "Error in POST /api/v1/client/[environmentId]/user"); + return { response: responses.internalServerErrorResponse("Unable to fetch user state", true) }; +}; + export const OPTIONS = async (): Promise => { return responses.successResponse( {}, @@ -123,16 +136,7 @@ export const POST = withV1ApiWrapper({ response: responses.successResponse(responseJson, true), }; } catch (err) { - if (err instanceof ResourceNotFoundError) { - return { - response: responses.notFoundResponse(err.resourceType, err.resourceId), - }; - } - - logger.error({ error: err, url: req.url }, "Error in POST /api/v1/client/[environmentId]/user"); - return { - response: responses.internalServerErrorResponse(err.message ?? "Unable to fetch person state", true), - }; + return handleError(err, req.url); } }, }); diff --git a/apps/web/modules/ee/contacts/lib/validate-attribute-type.test.ts b/apps/web/modules/ee/contacts/lib/validate-attribute-type.test.ts index 3115ea47fe69..9d75d6d9f6e4 100644 --- a/apps/web/modules/ee/contacts/lib/validate-attribute-type.test.ts +++ b/apps/web/modules/ee/contacts/lib/validate-attribute-type.test.ts @@ -13,22 +13,14 @@ describe("validateAndParseAttributeValue", () => { } }); - test("converts numbers to string", () => { + test("rejects number values (SDK must pass actual strings)", () => { const result = validateAndParseAttributeValue(42, "string", "testKey"); - expect(result.valid).toBe(true); - if (result.valid) { - expect(result.parsedValue.value).toBe("42"); - expect(result.parsedValue.valueNumber).toBeNull(); - } - }); - - test("converts Date to ISO string", () => { - const date = new Date("2024-01-15T10:30:00.000Z"); - const result = validateAndParseAttributeValue(date, "string", "testKey"); - expect(result.valid).toBe(true); - if (result.valid) { - expect(result.parsedValue.value).toBe("2024-01-15T10:30:00.000Z"); - expect(result.parsedValue.valueDate).toBeNull(); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error.code).toBe("string_type_mismatch"); + expect(result.error.params.key).toBe("testKey"); + expect(result.error.params.type).toBe("number"); + expect(formatValidationError(result.error)).toContain("received a number"); } }); }); diff --git a/apps/web/modules/ee/contacts/lib/validate-attribute-type.ts b/apps/web/modules/ee/contacts/lib/validate-attribute-type.ts index 52fe9ada6373..96c718843f3b 100644 --- a/apps/web/modules/ee/contacts/lib/validate-attribute-type.ts +++ b/apps/web/modules/ee/contacts/lib/validate-attribute-type.ts @@ -27,15 +27,6 @@ export type TAttributeValidationResult = error: TAttributeValidationError; }; -/** - * Converts any value to a string representation - */ -const convertToString = (value: TRawValue): string => { - if (value instanceof Date) return value.toISOString(); - if (typeof value === "number") return String(value); - return value; -}; - /** * Gets a human-readable type name for error messages */ @@ -45,16 +36,28 @@ const getTypeName = (value: TRawValue): string => { }; /** - * Validates and parses a string type attribute + * Validates and parses a string type attribute. */ -const validateStringType = (value: TRawValue): TAttributeValidationResult => ({ - valid: true, - parsedValue: { - value: convertToString(value), - valueNumber: null, - valueDate: null, - }, -}); +const validateStringType = (value: TRawValue, attributeKey: string): TAttributeValidationResult => { + if (typeof value === "string") { + return { + valid: true, + parsedValue: { + value, + valueNumber: null, + valueDate: null, + }, + }; + } + + return { + valid: false, + error: { + code: "string_type_mismatch", + params: { key: attributeKey, type: getTypeName(value) }, + }, + }; +}; /** * Validates and parses a number type attribute. @@ -170,13 +173,13 @@ export const validateAndParseAttributeValue = ( ): TAttributeValidationResult => { switch (expectedDataType) { case "string": - return validateStringType(value); + return validateStringType(value, attributeKey); case "number": return validateNumberType(value, attributeKey); case "date": return validateDateType(value, attributeKey); default: - return validateStringType(value); + return validateStringType(value, attributeKey); } }; @@ -185,6 +188,8 @@ export const validateAndParseAttributeValue = ( * Used for API/SDK responses. */ const VALIDATION_ERROR_TEMPLATES: Record = { + string_type_mismatch: + "Attribute '{key}' expects a string but received a {type}. Pass an actual string value.", number_type_mismatch: "Attribute '{key}' expects a number but received a string. Pass an actual number value (e.g., 123 instead of \"123\").", date_invalid: "Attribute '{key}' expects a valid date. Received: Invalid Date", diff --git a/apps/web/modules/projects/settings/look/components/theme-styling.tsx b/apps/web/modules/projects/settings/look/components/theme-styling.tsx index 66518ce9dcce..8821273784fe 100644 --- a/apps/web/modules/projects/settings/look/components/theme-styling.tsx +++ b/apps/web/modules/projects/settings/look/components/theme-styling.tsx @@ -100,7 +100,7 @@ export const ThemeStyling = ({ form.setValue(key as keyof TProjectStyling, value, { shouldDirty: true }); } - toast.success(t("environments.workspace.look.styling_updated_successfully")); + toast.success(t("environments.workspace.look.suggested_colors_applied_please_save")); setConfirmSuggestColorsOpen(false); }; diff --git a/apps/web/modules/survey/editor/components/styling-view.tsx b/apps/web/modules/survey/editor/components/styling-view.tsx index 8f9f8b1a397f..c512d1732027 100644 --- a/apps/web/modules/survey/editor/components/styling-view.tsx +++ b/apps/web/modules/survey/editor/components/styling-view.tsx @@ -94,7 +94,7 @@ export const StylingView = ({ form.setValue(key as keyof TSurveyStyling, value, { shouldDirty: true }); } - toast.success(t("environments.workspace.look.styling_updated_successfully")); + toast.success(t("environments.workspace.look.suggested_colors_applied_please_save")); setConfirmSuggestColorsOpen(false); }; diff --git a/apps/web/modules/ui/components/input-combo-box/index.tsx b/apps/web/modules/ui/components/input-combo-box/index.tsx index fd46bff1676d..957c0246de5e 100644 --- a/apps/web/modules/ui/components/input-combo-box/index.tsx +++ b/apps/web/modules/ui/components/input-combo-box/index.tsx @@ -226,7 +226,7 @@ export const InputCombobox: React.FC = ({ tabIndex={0} aria-controls="options" aria-expanded={open} - className={cn("flex h-full w-full cursor-pointer items-center justify-end bg-white pr-2", { + className={cn("flex w-full cursor-pointer items-center justify-end bg-white pr-2 h-10", { "w-10 justify-center pr-0": withInput && inputType !== "dropdown", "pointer-events-none": isClearing, })}> diff --git a/apps/web/modules/ui/components/preview-survey/index.tsx b/apps/web/modules/ui/components/preview-survey/index.tsx index 6623987b54a4..a91388e1d070 100644 --- a/apps/web/modules/ui/components/preview-survey/index.tsx +++ b/apps/web/modules/ui/components/preview-survey/index.tsx @@ -225,10 +225,10 @@ export const PreviewSurvey = ({ )}> {previewMode === "mobile" && ( <> -

+

Preview

-
+
) : (
-
+
{!styling.isLogoHidden && ( )} {previewMode === "desktop" && ( -
+
@@ -373,7 +373,7 @@ export const PreviewSurvey = ({ styling={styling} ContentRef={ContentRef as React.RefObject} isEditorView> -
+
{!styling.isLogoHidden && ( - Pre-churn flow coming soon We’re currently building full-screen survey - pop-ups. You’ll be able to prevent users from closing the survey unless they - respond to it. It’s certainly debatable if you want that but you could force - them to click through the survey before letting them cancel 🤷 - - ### 5. Select Action in the “When to ask” card ![Select feedback button action](/images/xm-and-surveys/xm/best-practices/cancel-subscription/select-action.webp) diff --git a/docs/xm-and-surveys/xm/best-practices/improve-trial-cr.mdx b/docs/xm-and-surveys/xm/best-practices/improve-trial-cr.mdx index 7e69e14d07c6..b9017ffcc7e9 100644 --- a/docs/xm-and-surveys/xm/best-practices/improve-trial-cr.mdx +++ b/docs/xm-and-surveys/xm/best-practices/improve-trial-cr.mdx @@ -46,13 +46,7 @@ _Want to change the button color? Adjust it in the project settings!_ Save, and move over to the **Audience** tab. -### 3. Pre-segment your audience (coming soon) - - -### Filter by Attribute Coming Soon - -We're working on pre-segmenting users by attributes. This manual will be updated in the coming days. - +### 3. Pre-segment your audience Pre-segmentation isn't needed for this survey since you likely want to target all users who cancel their trial. You can use a specific user action, like clicking **Cancel Trial**, to show the survey only to users trying your product. @@ -62,13 +56,13 @@ How you trigger your survey depends on your product. There are two options: - **Trigger by Page view:** If you have a page like `/trial-cancelled` for users who cancel their trial subscription, create a user action with the type "Page View." Select "Limit to specific pages" and apply URL filters with these settings: -![Change text content](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-pageurl.webp) +![Add page URL action](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-pageurl.webp) Whenever a user visits this page, the survey will be displayed ✅ - **Trigger by Button Click:** In a different case, you have a “Cancel Trial" button in your app. You can setup a user Action with the `Inner Text`: -![Change text content](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-innertext.webp) +![Add inner text action](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-innertext.webp) Please have a look at our complete [Actions manual](/xm-and-surveys/surveys/website-app-surveys/actions) if you have questions. diff --git a/docs/xm-and-surveys/xm/best-practices/interview-prompt.mdx b/docs/xm-and-surveys/xm/best-practices/interview-prompt.mdx index 84352684d480..61a472b25407 100644 --- a/docs/xm-and-surveys/xm/best-practices/interview-prompt.mdx +++ b/docs/xm-and-surveys/xm/best-practices/interview-prompt.mdx @@ -54,13 +54,7 @@ In the button settings you have to make sure it is set to “External URL”. In Save, and move over to the “Audience” tab. -### 3. Pre-segment your audience (coming soon) - - - ## Filter by attribute coming soon. We're working on pre-segmenting users by - - attributes. We will update this manual in the next few days. - +### 3. Pre-segment your audience Once you clicked over to the “Audience” tab you can change the settings. In the **Who To Send** card, select “Filter audience by attribute”. This allows you to only show the prompt to a specific segment of your user base.