chore: remove ios and android package from monorepo (#6533)

This commit is contained in:
Matti Nannt
2025-09-11 14:57:55 +02:00
committed by GitHub
parent 933723f1fe
commit 22d4952a40
168 changed files with 11 additions and 9426 deletions

View File

@@ -264,7 +264,6 @@
"multiple_languages": "Mehrsprachigkeit",
"name": "Name",
"new": "Neu",
"new_survey": "Neue Umfrage",
"new_version_available": "Formbricks {version} ist da. Jetzt aktualisieren!",
"next": "Weiter",
"no_background_image_found": "Kein Hintergrundbild gefunden.",
@@ -342,7 +341,6 @@
"save": "Speichern",
"save_changes": "Änderungen speichern",
"saving": "Speichern",
"scheduled": "Geplant",
"search": "Suchen",
"security": "Sicherheit",
"segment": "Segment",
@@ -382,7 +380,6 @@
"survey_live": "Umfrage live",
"survey_not_found": "Umfrage nicht gefunden",
"survey_paused": "Umfrage pausiert.",
"survey_scheduled": "Umfrage geplant.",
"survey_type": "Umfragetyp",
"surveys": "Umfragen",
"switch_to": "Wechseln zu {environment}",
@@ -763,47 +760,21 @@
"unable_to_delete_api_key": "API-Schlüssel kann nicht gelöscht werden"
},
"app-connection": {
"api_host_description": "Dies ist die URL deines Formbricks Backends.",
"app_connection": "App-Verbindung",
"app_connection_description": "Verbinde deine App mit Formbricks.",
"cache_update_delay_description": "Wenn du Aktualisierungen an Umfragen, Kontakten, Aktionen oder anderen Daten vornimmst, kann es bis zu 5 Minuten dauern, bis diese Änderungen in deiner lokalen App, die das Formbricks SDK verwendet, angezeigt werden. Diese Verzögerung ist auf eine Einschränkung unseres aktuellen Caching-Systems zurückzuführen. Wir arbeiten aktiv an einer Überarbeitung des Cache und werden in Formbricks 4.0 eine Lösung veröffentlichen.",
"cache_update_delay_title": "Änderungen werden aufgrund von Caching nach 5 Minuten angezeigt",
"check_out_the_docs": "Schau dir die Docs an.",
"dive_into_the_docs": "Tauch in die Docs ein.",
"does_your_widget_work": "Funktioniert dein Widget?",
"environment_id": "Deine Umgebungs-ID",
"environment_id_description": "Diese ID identifiziert eindeutig diese Formbricks Umgebung.",
"environment_id_description_with_environment_id": "Wird verwendet, um die richtige Umgebung zu identifizieren: {environmentId} ist deine.",
"formbricks_sdk": "Formbricks SDK",
"formbricks_sdk_connected": "Formbricks SDK ist verbunden",
"formbricks_sdk_not_connected": "Formbricks SDK ist noch nicht verbunden.",
"formbricks_sdk_not_connected_description": "Verbinde deine Website oder App mit Formbricks",
"have_a_problem": "Hast Du ein Problem?",
"how_to_setup": "Wie einrichten",
"how_to_setup_description": "Befolge diese Schritte, um das Formbricks Widget in deiner App einzurichten.",
"identifying_your_users": "deine Nutzer identifizieren",
"if_you_are_planning_to": "Wenn Du planst zu",
"insert_this_code_into_the": "Füge diesen Code in die",
"need_a_more_detailed_setup_guide_for": "Brauche eine detailliertere Anleitung für",
"not_working": "Klappt nicht?",
"open_an_issue_on_github": "Eine Issue auf GitHub öffnen",
"open_the_browser_console_to_see_the_logs": "Öffne die Browser Konsole, um die Logs zu sehen.",
"receiving_data": "Daten werden empfangen \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Erneut prüfen",
"scroll_to_the_top": "Scroll nach oben!",
"setup_alert_description": "Befolge dieses Schritt-für-Schritt-Tutorial, um deine App oder Website in weniger als 5 Minuten zu verbinden.",
"setup_alert_title": "Wie man verbindet",
"step_1": "Schritt 1: Installiere mit pnpm, npm oder yarn",
"step_2": "Schritt 2: Widget initialisieren",
"step_2_description": "Importiere Formbricks und initialisiere das Widget in deiner Komponente (z.B. App.tsx):",
"step_3": "Schritt 3: Debug-Modus",
"switch_on_the_debug_mode_by_appending": "Schalte den Debug-Modus ein, indem Du anhängst",
"tag_of_your_app": "Tag deiner App",
"to_the_url_where_you_load_the": "URL, wo Du die lädst",
"want_to_learn_how_to_add_user_attributes": "Willst Du lernen, wie man Attribute hinzufügt?",
"you_are_done": "Du bist fertig \uD83C\uDF89",
"you_can_set_the_user_id_with": "du kannst die Benutzer-ID festlegen mit",
"your_app_now_communicates_with_formbricks": "Deine App kommuniziert jetzt mit Formbricks - sie sendet Ereignisse und lädt Umfragen automatisch!"
"setup_alert_title": "Wie man verbindet"
},
"general": {
"cannot_delete_only_project": "Dies ist dein einziges Projekt, es kann nicht gelöscht werden. Erstelle zuerst ein neues Projekt.",
@@ -1256,9 +1227,7 @@
"automatically_close_survey_after": "Umfrage automatisch schließen nach",
"automatically_close_the_survey_after_a_certain_number_of_responses": "Schließe die Umfrage automatisch nach einer bestimmten Anzahl von Antworten.",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Schließe die Umfrage automatisch, wenn der Benutzer nach einer bestimmten Anzahl von Sekunden nicht antwortet.",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Schließt die Umfrage automatisch zu Beginn des Tages (UTC).",
"automatically_mark_the_survey_as_complete_after": "Umfrage automatisch als abgeschlossen markieren nach",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Umfrage automatisch zu Beginn des Tages (UTC) freigeben.",
"back_button_label": "Zurück\"- Button ",
"background_styling": "Hintergründe",
"brand_color": "Markenfarbe",
@@ -1306,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "Aktionen auswählen, die die Umfrage auslösen.",
"choose_where_to_run_the_survey": "Wähle, wo die Umfrage durchgeführt werden soll.",
"city": "Stadt",
"close_survey_on_date": "Umfrage am Datum schließen",
"close_survey_on_response_limit": "Umfrage bei Erreichen des Antwortlimits schließen",
"color": "Farbe",
"column_used_in_logic_error": "Diese Spalte wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
@@ -1514,7 +1482,6 @@
"redirect_thank_you_card": "Weiterleitung anlegen",
"redirect_to_url": "Zu URL weiterleiten",
"redirect_to_url_not_available_on_free_plan": "Umleitung zu URL ist im kostenlosen Plan nicht verfügbar",
"release_survey_on_date": "Umfrage an Datum veröffentlichen",
"remove_description": "Beschreibung entfernen",
"remove_translations": "Übersetzungen entfernen",
"require_answer": "Antwort erforderlich",
@@ -1858,7 +1825,6 @@
"survey_deleted_successfully": "Umfrage erfolgreich gelöscht",
"survey_duplicated_successfully": "Umfrage erfolgreich dupliziert",
"survey_duplication_error": "Duplizieren der Umfrage fehlgeschlagen",
"survey_status_tooltip": "Um den Umfragestatus zu aktualisieren, aktualisiere den Zeitplan in den Umfrageoptionen.",
"templates": {
"all_channels": "Alle Kanäle",
"all_industries": "Alle Branchen",

View File

@@ -264,7 +264,6 @@
"multiple_languages": "Multiple languages",
"name": "Name",
"new": "New",
"new_survey": "New Survey",
"new_version_available": "Formbricks {version} is here. Upgrade now!",
"next": "Next",
"no_background_image_found": "No background image found.",
@@ -342,7 +341,6 @@
"save": "Save",
"save_changes": "Save changes",
"saving": "Saving",
"scheduled": "Scheduled",
"search": "Search",
"security": "Security",
"segment": "Segment",
@@ -382,7 +380,6 @@
"survey_live": "Survey live",
"survey_not_found": "Survey not found",
"survey_paused": "Survey paused.",
"survey_scheduled": "Survey scheduled.",
"survey_type": "Survey Type",
"surveys": "Surveys",
"switch_to": "Switch to {environment}",
@@ -763,47 +760,21 @@
"unable_to_delete_api_key": "Unable to delete API Key"
},
"app-connection": {
"api_host_description": "This is the URL of your Formbricks backend.",
"app_connection": "App Connection",
"app_connection_description": "Connect your app to Formbricks.",
"cache_update_delay_description": "When you make updates to surveys, contacts, actions, or other data, it can take up to 5 minutes for those changes to appear in your local app running the Formbricks SDK. This delay is due to a limitation in our current caching system. Were actively reworking the cache and will release a fix in Formbricks 4.0.",
"cache_update_delay_title": "Changes will be reflected after 5 minutes due to caching",
"check_out_the_docs": "Check out the docs.",
"dive_into_the_docs": "Dive into the docs.",
"does_your_widget_work": "Does your widget work?",
"environment_id": "Your Environment ID",
"environment_id_description": "This id uniquely identifies this Formbricks environment.",
"environment_id_description_with_environment_id": "Used to identify the correct environment: {environmentId} is yours.",
"formbricks_sdk": "Formbricks SDK",
"formbricks_sdk_connected": "Formbricks SDK is connected",
"formbricks_sdk_not_connected": "Formbricks SDK is not yet connected.",
"formbricks_sdk_not_connected_description": "Connect your website or app with Formbricks",
"have_a_problem": "Have a problem?",
"how_to_setup": "How to setup",
"how_to_setup_description": "Follow these steps to setup the Formbricks widget within your app.",
"identifying_your_users": "identifying your users",
"if_you_are_planning_to": "If you are planning to",
"insert_this_code_into_the": "Insert this code into the",
"need_a_more_detailed_setup_guide_for": "Need a more detailed setup guide for",
"not_working": "Not working?",
"open_an_issue_on_github": "Open an issue on GitHub",
"open_the_browser_console_to_see_the_logs": "Open the browser console to see the logs.",
"receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Re-check",
"scroll_to_the_top": "Scroll to the top!",
"setup_alert_description": "Follow this step-by-step tutorial to connect your app or website in under 5 minutes.",
"setup_alert_title": "How to connect",
"step_1": "Step 1: Install with pnpm, npm or yarn",
"step_2": "Step 2: Initialize widget",
"step_2_description": "Import Formbricks and initialize the widget in your Component (e.g. App.tsx):",
"step_3": "Step 3: Debug mode",
"switch_on_the_debug_mode_by_appending": "Switch on the debug mode by appending",
"tag_of_your_app": "tag of your app",
"to_the_url_where_you_load_the": "to the URL where you load the",
"want_to_learn_how_to_add_user_attributes": "Want to learn how to add user attributes, custom events and more?",
"you_are_done": "You're done \uD83C\uDF89",
"you_can_set_the_user_id_with": "you can set the user id with",
"your_app_now_communicates_with_formbricks": "Your app now communicates with Formbricks - sending events, and loading surveys automatically!"
"setup_alert_title": "How to connect"
},
"general": {
"cannot_delete_only_project": "This is your only project, it cannot be deleted. Create a new project first.",
@@ -1256,9 +1227,7 @@
"automatically_close_survey_after": "Automatically close survey after",
"automatically_close_the_survey_after_a_certain_number_of_responses": "Automatically close the survey after a certain number of responses.",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Automatically close the survey if the user does not respond after certain number of seconds.",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Automatically closes the survey at the beginning of the day (UTC).",
"automatically_mark_the_survey_as_complete_after": "Automatically mark the survey as complete after",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Automatically release the survey at the beginning of the day (UTC).",
"back_button_label": "\"Back\" Button Label",
"background_styling": "Background Styling",
"brand_color": "Brand color",
@@ -1306,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "Choose the actions which trigger the survey.",
"choose_where_to_run_the_survey": "Choose where to run the survey.",
"city": "City",
"close_survey_on_date": "Close survey on date",
"close_survey_on_response_limit": "Close survey on response limit",
"color": "Color",
"column_used_in_logic_error": "This column is used in logic of question {questionIndex}. Please remove it from logic first.",
@@ -1514,7 +1482,6 @@
"redirect_thank_you_card": "Redirect thank you card",
"redirect_to_url": "Redirect to Url",
"redirect_to_url_not_available_on_free_plan": "Redirect To Url is not available on free plan",
"release_survey_on_date": "Release survey on date",
"remove_description": "Remove description",
"remove_translations": "Remove translations",
"require_answer": "Require Answer",
@@ -1858,7 +1825,6 @@
"survey_deleted_successfully": "Survey deleted successfully!",
"survey_duplicated_successfully": "Survey duplicated successfully.",
"survey_duplication_error": "Failed to duplicate the survey.",
"survey_status_tooltip": "To update the survey status, update the schedule and close setting in the survey response options.",
"templates": {
"all_channels": "All channels",
"all_industries": "All industries",

View File

@@ -264,7 +264,6 @@
"multiple_languages": "Plusieurs langues",
"name": "Nom",
"new": "Nouveau",
"new_survey": "Nouveau Sondage",
"new_version_available": "Formbricks {version} est là. Mettez à jour maintenant !",
"next": "Suivant",
"no_background_image_found": "Aucune image de fond trouvée.",
@@ -342,7 +341,6 @@
"save": "Enregistrer",
"save_changes": "Enregistrer les modifications",
"saving": "Sauvegarder",
"scheduled": "Programmé",
"search": "Recherche",
"security": "Sécurité",
"segment": "Segmenter",
@@ -382,7 +380,6 @@
"survey_live": "Sondage en direct",
"survey_not_found": "Sondage non trouvé",
"survey_paused": "Sondage en pause.",
"survey_scheduled": "Sondage programmé.",
"survey_type": "Type de sondage",
"surveys": "Enquêtes",
"switch_to": "Passer à {environment}",
@@ -763,47 +760,21 @@
"unable_to_delete_api_key": "Impossible de supprimer la clé API"
},
"app-connection": {
"api_host_description": "Ceci est l'URL de votre backend Formbricks.",
"app_connection": "Connexion d'application",
"app_connection_description": "Connectez votre application à Formbricks.",
"cache_update_delay_description": "Lorsque vous effectuez des mises à jour sur les sondages, contacts, actions ou autres données, cela peut prendre jusqu'à 5 minutes pour que ces modifications apparaissent dans votre application locale exécutant le SDK Formbricks. Ce délai est dû à une limitation de notre système de mise en cache actuel. Nous retravaillons activement le cache et publierons une correction dans Formbricks 4.0.",
"cache_update_delay_title": "Les modifications seront reflétées après 5 minutes en raison de la mise en cache",
"check_out_the_docs": "Consultez la documentation.",
"dive_into_the_docs": "Plongez dans la documentation.",
"does_your_widget_work": "Votre widget fonctionne-t-il ?",
"environment_id": "Votre identifiant d'environnement",
"environment_id_description": "Cet identifiant identifie de manière unique cet environnement Formbricks.",
"environment_id_description_with_environment_id": "Utilisé pour identifier l'environnement correct : {environmentId} est le vôtre.",
"formbricks_sdk": "SDK Formbricks",
"formbricks_sdk_connected": "Le SDK Formbricks est connecté",
"formbricks_sdk_not_connected": "Le SDK Formbricks n'est pas encore connecté.",
"formbricks_sdk_not_connected_description": "Connectez votre site web ou votre application à Formbricks.",
"have_a_problem": "Vous avez un problème ?",
"how_to_setup": "Comment configurer",
"how_to_setup_description": "Suivez ces étapes pour configurer le widget Formbricks dans votre application.",
"identifying_your_users": "identifier vos utilisateurs",
"if_you_are_planning_to": "Si vous prévoyez de",
"insert_this_code_into_the": "Insérez ce code dans le",
"need_a_more_detailed_setup_guide_for": "Besoin d'un guide d'installation plus détaillé pour",
"not_working": "Ça ne fonctionne pas ?",
"open_an_issue_on_github": "Ouvrir un problème sur GitHub",
"open_the_browser_console_to_see_the_logs": "Ouvrez la console du navigateur pour voir les journaux.",
"receiving_data": "Réception des données \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Re-vérifier",
"scroll_to_the_top": "Faites défiler vers le haut !",
"setup_alert_description": "Suivez ce tutoriel étape par étape pour connecter votre application ou site web en moins de 5 minutes.",
"setup_alert_title": "Comment connecter",
"step_1": "Étape 1 : Installer avec pnpm, npm ou yarn",
"step_2": "Étape 2 : Initialiser le widget",
"step_2_description": "Importez Formbricks et initialisez le widget dans votre composant (par exemple, App.tsx) :",
"step_3": "Étape 3 : Mode débogage",
"switch_on_the_debug_mode_by_appending": "Activez le mode débogage en ajoutant",
"tag_of_your_app": "étiquette de votre application",
"to_the_url_where_you_load_the": "vers l'URL où vous chargez le",
"want_to_learn_how_to_add_user_attributes": "Vous voulez apprendre à ajouter des attributs utilisateur, des événements personnalisés et plus encore ?",
"you_are_done": "Vous avez terminé \uD83C\uDF89",
"you_can_set_the_user_id_with": "vous pouvez définir l'ID utilisateur avec",
"your_app_now_communicates_with_formbricks": "Votre application communique désormais avec Formbricks - envoyant des événements et chargeant des enquêtes automatiquement !"
"setup_alert_title": "Comment connecter"
},
"general": {
"cannot_delete_only_project": "Ceci est votre seul projet, il ne peut pas être supprimé. Créez d'abord un nouveau projet.",
@@ -1256,9 +1227,7 @@
"automatically_close_survey_after": "Fermer automatiquement l'enquête après",
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fermer automatiquement l'enquête après un certain nombre de réponses.",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fermer automatiquement l'enquête si l'utilisateur ne répond pas après un certain nombre de secondes.",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Ferme automatiquement l'enquête au début de la journée (UTC).",
"automatically_mark_the_survey_as_complete_after": "Marquer automatiquement l'enquête comme terminée après",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Libérer automatiquement l'enquête au début de la journée (UTC).",
"back_button_label": "Label du bouton \"Retour''",
"background_styling": "Style de fond",
"brand_color": "Couleur de marque",
@@ -1306,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "Choisissez les actions qui déclenchent l'enquête.",
"choose_where_to_run_the_survey": "Choisissez où réaliser l'enquête.",
"city": "Ville",
"close_survey_on_date": "Clôturer l'enquête à la date",
"close_survey_on_response_limit": "Fermer l'enquête sur la limite de réponse",
"color": "Couleur",
"column_used_in_logic_error": "Cette colonne est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
@@ -1514,7 +1482,6 @@
"redirect_thank_you_card": "Carte de remerciement de redirection",
"redirect_to_url": "Rediriger vers l'URL",
"redirect_to_url_not_available_on_free_plan": "La redirection vers l'URL n'est pas disponible sur le plan gratuit.",
"release_survey_on_date": "Publier l'enquête à la date",
"remove_description": "Supprimer la description",
"remove_translations": "Supprimer les traductions",
"require_answer": "Réponse requise",
@@ -1858,7 +1825,6 @@
"survey_deleted_successfully": "Enquête supprimée avec succès !",
"survey_duplicated_successfully": "Enquête dupliquée avec succès.",
"survey_duplication_error": "Échec de la duplication de l'enquête.",
"survey_status_tooltip": "Pour mettre à jour le statut de l'enquête, mettez à jour le calendrier et fermez les paramètres dans les options de réponse à l'enquête.",
"templates": {
"all_channels": "Tous les canaux",
"all_industries": "Tous les secteurs",

View File

@@ -264,7 +264,6 @@
"multiple_languages": "多言語",
"name": "名前",
"new": "新規",
"new_survey": "新規フォーム",
"new_version_available": "Formbricks {version} が利用可能です。今すぐアップグレード!",
"next": "次へ",
"no_background_image_found": "背景画像が見つかりません。",
@@ -342,7 +341,6 @@
"save": "保存",
"save_changes": "変更を保存",
"saving": "保存中",
"scheduled": "スケジュール済み",
"search": "検索",
"security": "セキュリティ",
"segment": "セグメント",
@@ -382,7 +380,6 @@
"survey_live": "フォーム公開中",
"survey_not_found": "フォームが見つかりません",
"survey_paused": "フォームは一時停止中です。",
"survey_scheduled": "フォームはスケジュール済みです。",
"survey_type": "フォームの種類",
"surveys": "フォーム",
"switch_to": "{environment}に切り替え",
@@ -763,47 +760,21 @@
"unable_to_delete_api_key": "APIキーを削除できませんでした"
},
"app-connection": {
"api_host_description": "これはFormbricksバックエンドのURLです。",
"app_connection": "アプリ接続",
"app_connection_description": "あなたのアプリをFormbricksに接続します。",
"cache_update_delay_description": "フォーム・連絡先・アクションなどを更新してから、Formbricks SDK を実行中のローカルアプリに反映されるまで最大5分かかる場合があります。これは現在のキャッシュ方式の制限によるものです。私たちはキャッシュを改修中で、Formbricks 4.0 で修正を提供予定です。",
"cache_update_delay_title": "キャッシュのため変更の反映に最大5分かかります",
"check_out_the_docs": "ドキュメントを見る",
"dive_into_the_docs": "ドキュメントを詳しく読む",
"does_your_widget_work": "ウィジェットは動作していますか?",
"environment_id": "あなたのEnvironmentId",
"environment_id_description": "このIDはこのFormbricks環境を一意に識別します。",
"environment_id_description_with_environment_id": "正しい環境を識別するために使用します: {environmentId} があなたのIDです。",
"formbricks_sdk": "Formbricks SDK",
"formbricks_sdk_connected": "Formbricks SDK は接続されています",
"formbricks_sdk_not_connected": "Formbricks SDK はまだ接続されていません。",
"formbricks_sdk_not_connected_description": "あなたのウェブサイトまたはアプリをFormbricksに接続してください",
"have_a_problem": "問題がありますか?",
"how_to_setup": "セットアップ方法",
"how_to_setup_description": "アプリ内でFormbricksウィジェットを設定する手順に従ってください。",
"identifying_your_users": "ユーザーの識別",
"if_you_are_planning_to": "〜を計画している場合は",
"insert_this_code_into_the": "次のコードを挿入してください",
"need_a_more_detailed_setup_guide_for": "より詳細なセットアップガイドが必要ですか",
"not_working": "うまく動作しませんか?",
"open_an_issue_on_github": "GitHubでIssueを作成",
"open_the_browser_console_to_see_the_logs": "ブラウザのコンソールを開いてログを確認してください。",
"receiving_data": "データ受信中 \uD83D\uDC83\uD83D\uDD7A",
"recheck": "再チェック",
"scroll_to_the_top": "ページ上部に戻る",
"setup_alert_description": "5 分以内でアプリまたはウェブサイト を 接続する手順をステップバイステップ の チュートリアルに従ってください。",
"setup_alert_title": "接続方法",
"step_1": "ステップ1: pnpm / npm / yarnでインストール",
"step_2": "ステップ2: ウィジェットの初期化",
"step_2_description": "Formbricksをインポートし、コンポーネント例: App.tsxでウィジェットを初期化します。",
"step_3": "ステップ3: デバッグモード",
"switch_on_the_debug_mode_by_appending": "URLの末尾に以下を付与してデバッグモードを有効化",
"tag_of_your_app": "あなたのアプリのタグ",
"to_the_url_where_you_load_the": "読み込みを行うURLに",
"want_to_learn_how_to_add_user_attributes": "ユーザー属性、カスタムイベント等の追加方法を学びますか?",
"you_are_done": "完了です \uD83C\uDF89",
"you_can_set_the_user_id_with": "ユーザーIDは次のように設定できます",
"your_app_now_communicates_with_formbricks": "あなたのアプリはFormbricksと通信し、イベント送信やフォームの自動読込を行います"
"setup_alert_title": "接続方法"
},
"general": {
"cannot_delete_only_project": "これは唯一のプロジェクトのため削除できません。まず新しいプロジェクトを作成してください。",
@@ -1256,9 +1227,7 @@
"automatically_close_survey_after": "フォームを自動的に閉じる",
"automatically_close_the_survey_after_a_certain_number_of_responses": "一定の回答数に達した後にフォームを自動的に閉じます。",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "ユーザーが一定秒数応答しない場合、フォームを自動的に閉じます。",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "日UTCの開始時にフォームを自動的に閉じます。",
"automatically_mark_the_survey_as_complete_after": "フォームを自動的に完了としてマークする",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "日UTCの開始時にフォームを自動的にリリースします。",
"back_button_label": "「戻る」ボタンのラベル",
"background_styling": "背景のスタイル",
"brand_color": "ブランドカラー",
@@ -1306,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "フォームをトリガーするアクションを選択してください。",
"choose_where_to_run_the_survey": "フォームを実行する場所を選択してください。",
"city": "市区町村",
"close_survey_on_date": "日付でフォームを閉じる",
"close_survey_on_response_limit": "回答数の上限でフォームを閉じる",
"color": "色",
"column_used_in_logic_error": "この列は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
@@ -1514,7 +1482,6 @@
"redirect_thank_you_card": "サンクスクカードをリダイレクト",
"redirect_to_url": "URLにリダイレクト",
"redirect_to_url_not_available_on_free_plan": "URLへのリダイレクトは無料プランでは利用できません",
"release_survey_on_date": "日付でフォームをリリース",
"remove_description": "説明を削除",
"remove_translations": "翻訳を削除",
"require_answer": "回答を必須にする",
@@ -1858,7 +1825,6 @@
"survey_deleted_successfully": "フォームを正常に削除しました!",
"survey_duplicated_successfully": "フォームを正常に複製しました。",
"survey_duplication_error": "フォームの複製に失敗しました。",
"survey_status_tooltip": "フォームのステータスを更新するには、フォームの回答オプションでスケジュールとクローズ設定を更新してください。",
"templates": {
"all_channels": "すべてのチャネル",
"all_industries": "すべての業界",

View File

@@ -264,7 +264,6 @@
"multiple_languages": "Vários idiomas",
"name": "Nome",
"new": "Novo",
"new_survey": "Nova Pesquisa",
"new_version_available": "Formbricks {version} chegou. Atualize agora!",
"next": "Próximo",
"no_background_image_found": "Imagem de fundo não encontrada.",
@@ -342,7 +341,6 @@
"save": "Salvar",
"save_changes": "Salvar alterações",
"saving": "Salvando",
"scheduled": "agendado",
"search": "Buscar",
"security": "Segurança",
"segment": "segmento",
@@ -382,7 +380,6 @@
"survey_live": "Pesquisa ao vivo",
"survey_not_found": "Pesquisa não encontrada",
"survey_paused": "Pesquisa pausada.",
"survey_scheduled": "Pesquisa agendada.",
"survey_type": "Tipo de Pesquisa",
"surveys": "Pesquisas",
"switch_to": "Mudar para {environment}",
@@ -763,47 +760,21 @@
"unable_to_delete_api_key": "Não foi possível deletar a Chave API"
},
"app-connection": {
"api_host_description": "Essa é a URL do seu backend do Formbricks.",
"app_connection": "Conexão do App",
"app_connection_description": "Conecte seu app ao Formbricks.",
"cache_update_delay_description": "Quando você faz atualizações em pesquisas, contatos, ações ou outros dados, pode levar até 5 minutos para que essas mudanças apareçam no seu app local rodando o SDK do Formbricks. Esse atraso é devido a uma limitação no nosso sistema de cache atual. Estamos ativamente retrabalhando o cache e planejamos lançar uma correção no Formbricks 4.0.",
"cache_update_delay_title": "As mudanças serão refletidas após 5 minutos devido ao cache",
"check_out_the_docs": "Confere a documentação.",
"dive_into_the_docs": "Mergulha na documentação.",
"does_your_widget_work": "Seu widget funciona?",
"environment_id": "Seu Id do Ambiente",
"environment_id_description": "Este ID identifica exclusivamente este ambiente do Formbricks.",
"environment_id_description_with_environment_id": "Usado para identificar o ambiente correto: {environmentId} é o seu.",
"formbricks_sdk": "SDK do Formbricks",
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
"formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado.",
"formbricks_sdk_not_connected_description": "Conecte seu site ou app com o Formbricks",
"have_a_problem": "Tá com problema?",
"how_to_setup": "Como configurar",
"how_to_setup_description": "Siga esses passos para configurar o widget do Formbricks no seu app.",
"identifying_your_users": "identificando seus usuários",
"if_you_are_planning_to": "Se você está planejando",
"insert_this_code_into_the": "Insere esse código no",
"need_a_more_detailed_setup_guide_for": "Preciso de um guia de configuração mais detalhado para",
"not_working": "Não tá funcionando?",
"open_an_issue_on_github": "Abre uma issue no GitHub",
"open_the_browser_console_to_see_the_logs": "Abre o console do navegador pra ver os logs.",
"receiving_data": "Recebendo dados \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Verificar novamente",
"scroll_to_the_top": "Rola pra cima!",
"setup_alert_description": "Siga este tutorial passo a passo para conectar seu app ou site em menos de 5 minutos.",
"setup_alert_title": "Como conectar",
"step_1": "Passo 1: Instale com pnpm, npm ou yarn",
"step_2": "Passo 2: Iniciar widget",
"step_2_description": "Importe o Formbricks e inicialize o widget no seu Componente (por exemplo, App.tsx):",
"step_3": "Passo 3: Modo de depuração",
"switch_on_the_debug_mode_by_appending": "Ative o modo de depuração adicionando",
"tag_of_your_app": "etiqueta do seu app",
"to_the_url_where_you_load_the": "para a URL onde você carrega o",
"want_to_learn_how_to_add_user_attributes": "Quer aprender como adicionar atributos de usuário, eventos personalizados e mais?",
"you_are_done": "Você terminou \uD83C\uDF89",
"you_can_set_the_user_id_with": "você pode definir o id do usuário com",
"your_app_now_communicates_with_formbricks": "Seu app agora se comunica com o Formbricks - enviando eventos e carregando pesquisas automaticamente!"
"setup_alert_title": "Como conectar"
},
"general": {
"cannot_delete_only_project": "Esse é seu único projeto, não pode ser deletado. Crie um novo projeto primeiro.",
@@ -1256,9 +1227,7 @@
"automatically_close_survey_after": "Fechar pesquisa automaticamente após",
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente a pesquisa depois de um certo número de respostas.",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Feche automaticamente a pesquisa se o usuário não responder depois de alguns segundos.",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Fecha automaticamente a pesquisa no começo do dia (UTC).",
"automatically_mark_the_survey_as_complete_after": "Marcar automaticamente a pesquisa como concluída após",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Liberar automaticamente a pesquisa no começo do dia (UTC).",
"back_button_label": "Voltar",
"background_styling": "Estilo de Fundo",
"brand_color": "Cor da marca",
@@ -1306,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que disparam a pesquisa.",
"choose_where_to_run_the_survey": "Escolha onde realizar a pesquisa.",
"city": "cidade",
"close_survey_on_date": "Fechar pesquisa na data",
"close_survey_on_response_limit": "Fechar pesquisa ao atingir limite de respostas",
"color": "cor",
"column_used_in_logic_error": "Esta coluna é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
@@ -1514,7 +1482,6 @@
"redirect_thank_you_card": "Redirecionar cartão de agradecimento",
"redirect_to_url": "Redirecionar para URL",
"redirect_to_url_not_available_on_free_plan": "Redirecionar para URL não está disponível no plano gratuito",
"release_survey_on_date": "Lançar pesquisa na data",
"remove_description": "Remover descrição",
"remove_translations": "Remover traduções",
"require_answer": "Preciso de Resposta",
@@ -1858,7 +1825,6 @@
"survey_deleted_successfully": "Pesquisa deletada com sucesso!",
"survey_duplicated_successfully": "Pesquisa duplicada com sucesso.",
"survey_duplication_error": "Falha ao duplicar a pesquisa.",
"survey_status_tooltip": "Para atualizar o status da pesquisa, atualize o cronograma e feche a configuração nas opções de resposta da pesquisa.",
"templates": {
"all_channels": "Todos os canais",
"all_industries": "Todas as indústrias",

View File

@@ -264,7 +264,6 @@
"multiple_languages": "Várias línguas",
"name": "Nome",
"new": "Novo",
"new_survey": "Novo inquérito",
"new_version_available": "Formbricks {version} está aqui. Atualize agora!",
"next": "Seguinte",
"no_background_image_found": "Nenhuma imagem de fundo encontrada.",
@@ -342,7 +341,6 @@
"save": "Guardar",
"save_changes": "Guardar alterações",
"saving": "Guardando",
"scheduled": "Agendado",
"search": "Procurar",
"security": "Segurança",
"segment": "Segmento",
@@ -382,7 +380,6 @@
"survey_live": "Inquérito ao vivo",
"survey_not_found": "Inquérito não encontrado",
"survey_paused": "Inquérito pausado.",
"survey_scheduled": "Inquérito agendado.",
"survey_type": "Tipo de Inquérito",
"surveys": "Inquéritos",
"switch_to": "Mudar para {environment}",
@@ -763,47 +760,21 @@
"unable_to_delete_api_key": "Não é possível eliminar a chave API"
},
"app-connection": {
"api_host_description": "Este é o URL do seu backend Formbricks.",
"app_connection": "Ligação de Aplicação",
"app_connection_description": "Ligue a sua aplicação ao Formbricks",
"cache_update_delay_description": "Quando fizer atualizações para inquéritos, contactos, ações ou outros dados, pode demorar até 5 minutos para que essas alterações apareçam na sua aplicação local a correr o SDK do Formbricks. Este atraso deve-se a uma limitação no nosso atual sistema de cache. Estamos a trabalhar ativamente na reformulação da cache e lançaremos uma correção no Formbricks 4.0.",
"cache_update_delay_title": "As alterações serão refletidas após 5 minutos devido ao armazenamento em cache.",
"check_out_the_docs": "Consulte a documentação.",
"dive_into_the_docs": "Mergulhe na documentação.",
"does_your_widget_work": "O seu widget funciona?",
"environment_id": "O Seu ID de Ambiente",
"environment_id_description": "Este id identifica de forma única este ambiente Formbricks.",
"environment_id_description_with_environment_id": "Usado para identificar o ambiente correto: {environmentId} é o seu.",
"formbricks_sdk": "SDK Formbricks",
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
"formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado",
"formbricks_sdk_not_connected_description": "Ligue o seu website ou aplicação ao Formbricks",
"have_a_problem": "Tem um problema?",
"how_to_setup": "Como configurar",
"how_to_setup_description": "Siga estes passos para configurar o widget Formbricks na sua aplicação.",
"identifying_your_users": "identificar os seus utilizadores",
"if_you_are_planning_to": "Se está a planear",
"insert_this_code_into_the": "Insira este código no",
"need_a_more_detailed_setup_guide_for": "Precisa de um guia de configuração mais detalhado para",
"not_working": "Não está a funcionar?",
"open_an_issue_on_github": "Abrir um problema no GitHub",
"open_the_browser_console_to_see_the_logs": "Abra a consola do navegador para ver os registos.",
"receiving_data": "A receber dados \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Verificar novamente",
"scroll_to_the_top": "Rolar para o topo!",
"setup_alert_description": "Siga este tutorial passo-a-passo para ligar a sua aplicação ou website em menos de 5 minutos",
"setup_alert_title": "Como conectar",
"step_1": "Passo 1: Instalar com pnpm, npm ou yarn",
"step_2": "Passo 2: Inicializar widget",
"step_2_description": "Importar Formbricks e inicializar o widget no seu Componente (por exemplo, App.tsx):",
"step_3": "Passo 3: Modo de depuração",
"switch_on_the_debug_mode_by_appending": "Ativar o modo de depuração adicionando",
"tag_of_your_app": "tag da sua aplicação",
"to_the_url_where_you_load_the": "para o URL onde carrega o",
"want_to_learn_how_to_add_user_attributes": "Quer aprender a adicionar atributos de utilizador, eventos personalizados e mais?",
"you_are_done": "Está concluído \uD83C\uDF89",
"you_can_set_the_user_id_with": "pode definir o ID do utilizador com",
"your_app_now_communicates_with_formbricks": "A sua aplicação agora comunica com o Formbricks - enviando eventos e carregando inquéritos automaticamente!"
"setup_alert_title": "Como conectar"
},
"general": {
"cannot_delete_only_project": "Este é o seu único projeto, não pode ser eliminado. Crie um novo projeto primeiro.",
@@ -1256,9 +1227,7 @@
"automatically_close_survey_after": "Fechar automaticamente o inquérito após",
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente o inquérito após um certo número de respostas",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fechar automaticamente o inquérito se o utilizador não responder após um certo número de segundos.",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Encerrar automaticamente o inquérito no início do dia (UTC).",
"automatically_mark_the_survey_as_complete_after": "Marcar automaticamente o inquérito como concluído após",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Lançar automaticamente o inquérito no início do dia (UTC).",
"back_button_label": "Rótulo do botão \"Voltar\"",
"background_styling": "Estilo de Fundo",
"brand_color": "Cor da marca",
@@ -1306,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que desencadeiam o inquérito.",
"choose_where_to_run_the_survey": "Escolha onde realizar o inquérito.",
"city": "Cidade",
"close_survey_on_date": "Encerrar inquérito na data",
"close_survey_on_response_limit": "Fechar inquérito no limite de respostas",
"color": "Cor",
"column_used_in_logic_error": "Esta coluna é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
@@ -1514,7 +1482,6 @@
"redirect_thank_you_card": "Redirecionar cartão de agradecimento",
"redirect_to_url": "Redirecionar para Url",
"redirect_to_url_not_available_on_free_plan": "Redirecionar para URL não está disponível no plano gratuito",
"release_survey_on_date": "Lançar inquérito na data",
"remove_description": "Remover descrição",
"remove_translations": "Remover traduções",
"require_answer": "Exigir Resposta",
@@ -1858,7 +1825,6 @@
"survey_deleted_successfully": "Inquérito eliminado com sucesso!",
"survey_duplicated_successfully": "Inquérito duplicado com sucesso.",
"survey_duplication_error": "Falha ao duplicar o inquérito.",
"survey_status_tooltip": "Para atualizar o estado do inquérito, atualize o agendamento e feche a configuração nas opções de resposta do inquérito.",
"templates": {
"all_channels": "Todos os canais",
"all_industries": "Todas as indústrias",

View File

@@ -264,7 +264,6 @@
"multiple_languages": "Mai multe limbi",
"name": "Nume",
"new": "Nou",
"new_survey": "Chestionar Nou",
"new_version_available": "Formbricks {version} este disponibil. Actualizați acum!",
"next": "Următorul",
"no_background_image_found": "Nu a fost găsită nicio imagine de fundal.",
@@ -342,7 +341,6 @@
"save": "Salvează",
"save_changes": "Salvează modificările",
"saving": "Salvare",
"scheduled": "Programat",
"search": "Căutare",
"security": "Securitate",
"segment": "Segment",
@@ -382,7 +380,6 @@
"survey_live": "Chestionar activ",
"survey_not_found": "Sondajul nu a fost găsit",
"survey_paused": "Chestionar oprit.",
"survey_scheduled": "Chestionar programat.",
"survey_type": "Tip Chestionar",
"surveys": "Sondaje",
"switch_to": "Comută la {environment}",
@@ -763,47 +760,21 @@
"unable_to_delete_api_key": "Imposibil de șters cheia API"
},
"app-connection": {
"api_host_description": "Acesta este URL-ul backend-ului tău Formbricks.",
"app_connection": "Conectare aplicație",
"app_connection_description": "Conectează aplicația ta la Formbricks.",
"cache_update_delay_description": "Când faci actualizări la sondaje, contacte, acțiuni sau alte date, poate dura până la 5 minute pentru ca aceste modificări să apară în aplicația locală care rulează SDK Formbricks. Această întârziere se datorează unei limitări în sistemul nostru actual de caching. Revedem activ cache-ul și vom lansa o soluție în Formbricks 4.0.",
"cache_update_delay_title": "Modificările vor fi reflectate după 5 minute datorită memorării în cache",
"check_out_the_docs": "Consultați documentația.",
"dive_into_the_docs": "Accesați documentația.",
"does_your_widget_work": "Funcționează widgetul dvs.?",
"environment_id": "ID-ul mediului tău",
"environment_id_description": "Acest id identifică în mod unic acest mediu Formbricks.",
"environment_id_description_with_environment_id": "Folosit pentru a identifica mediul corect: {environmentId} este al tău.",
"formbricks_sdk": "SDK Formbricks",
"formbricks_sdk_connected": "SDK Formbricks este conectat",
"formbricks_sdk_not_connected": "Formbricks SDK nu este încă conectat.",
"formbricks_sdk_not_connected_description": "Conectează-ți site-ul sau aplicația cu Formbricks",
"have_a_problem": "Aveți o problemă?",
"how_to_setup": "Cum să configurezi",
"how_to_setup_description": "Urmează acești pași pentru a configura widget-ul Formbricks în aplicația ta.",
"identifying_your_users": "identificarea utilizatorilor tăi",
"if_you_are_planning_to": "Dacă planifici să",
"insert_this_code_into_the": "Insereză acest cod în",
"need_a_more_detailed_setup_guide_for": "Aveți nevoie de un ghid de configurare mai detaliat pentru",
"not_working": "Nu funcționează?",
"open_an_issue_on_github": "Deschideți o problemă pe GitHub",
"open_the_browser_console_to_see_the_logs": "Deschide consola browserului pentru a vedea jurnalele.",
"receiving_data": "Recepționare date \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Re-verifică",
"scroll_to_the_top": "Derulați în partea de sus!",
"setup_alert_description": "Urmează acest tutorial pas cu pas pentru a-ți conecta aplicația sau site-ul în mai puțin de 5 minute.",
"setup_alert_title": "Cum să conectezi",
"step_1": "Pasul 1: Instalează cu pnpm, npm sau yarn",
"step_2": "Pasul 2: Inițializează widget-ul",
"step_2_description": "Importați Formbricks și inițializați widgetul în componenta dumneavoastră (de exemplu, App.tsx):",
"step_3": "Pasul 3: Modul de depanare",
"switch_on_the_debug_mode_by_appending": "Activează modul de depanare prin adăugare",
"tag_of_your_app": "eticheta aplicației tale",
"to_the_url_where_you_load_the": "la adresa URL de unde încarci",
"want_to_learn_how_to_add_user_attributes": "Doriți să aflați cum să adăugați atribute ale utilizatorului, evenimente personalizate și altele?",
"you_are_done": "Ai terminat \uD83C\uDF89",
"you_can_set_the_user_id_with": "poți seta ID-ul utilizatorului cu",
"your_app_now_communicates_with_formbricks": "Aplicația ta comunică acum cu Formbricks - trimite evenimente și încarcă automat sondajele!"
"setup_alert_title": "Cum să conectezi"
},
"general": {
"cannot_delete_only_project": "Acesta este singurul tău proiect, nu poate fi șters. Creează mai întâi un proiect nou.",
@@ -1256,9 +1227,7 @@
"automatically_close_survey_after": "Închideți automat sondajul după",
"automatically_close_the_survey_after_a_certain_number_of_responses": "Închideți automat sondajul după un număr anumit de răspunsuri.",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Închideți automat sondajul dacă utilizatorul nu răspunde după un anumit număr de secunde.",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Închide automat sondajul la începutul zilei (UTC).",
"automatically_mark_the_survey_as_complete_after": "Marcați automat sondajul ca finalizat după",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Eliberați automat sondajul la începutul zilei (UTC).",
"back_button_label": "Etichetă buton \"Înapoi\"",
"background_styling": "Stilizare fundal",
"brand_color": "Culoarea brandului",
@@ -1306,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "Alegeți acțiunile care declanșează sondajul.",
"choose_where_to_run_the_survey": "Alegeți unde să rulați chestionarul.",
"city": "Oraș",
"close_survey_on_date": "Închide sondajul la dată",
"close_survey_on_response_limit": "Închideți sondajul la limită de răspunsuri",
"color": "Culoare",
"column_used_in_logic_error": "Această coloană este folosită în logica întrebării {questionIndex}. Vă rugăm să o eliminați din logică mai întâi.",
@@ -1514,7 +1482,6 @@
"redirect_thank_you_card": "Redirecționează cardul de mulțumire",
"redirect_to_url": "Redirecționează către URL",
"redirect_to_url_not_available_on_free_plan": "\"Redirecționarea către URL nu este disponibilă în planul gratuit\"",
"release_survey_on_date": "Eliberați sondajul la dată",
"remove_description": "Eliminați descrierea",
"remove_translations": "Eliminați traducerile",
"require_answer": "Cere răspuns",
@@ -1858,7 +1825,6 @@
"survey_deleted_successfully": "\"Sondaj șters cu succes!\"",
"survey_duplicated_successfully": "\"Sondaj duplicat cu succes!\"",
"survey_duplication_error": "Eșec la duplicarea sondajului.",
"survey_status_tooltip": "Pentru a actualiza starea sondajului, actualizați programarea și setările de închidere în opțiunile de răspuns la sondaj.",
"templates": {
"all_channels": "Toate canalele",
"all_industries": "Toate industriile",

View File

@@ -118,6 +118,7 @@
"account_settings": "帐户设置",
"action": "操作",
"actions": "操作",
"actions_description": "代码 和 无代码 操作 用于 触发 拦截 调查 在 应用程序 和 网站 中。",
"active_surveys": "活跃 调查",
"activity": "活动",
"add": "添加",
@@ -263,7 +264,6 @@
"multiple_languages": "多种 语言",
"name": "名称",
"new": "新建",
"new_survey": "新 调查",
"new_version_available": "Formbricks {version} 在 这里。立即 升级!",
"next": "下一步",
"no_background_image_found": "未找到 背景 图片。",
@@ -341,7 +341,6 @@
"save": "保存",
"save_changes": "保存 更改",
"saving": "保存",
"scheduled": "计划好的",
"search": "搜索",
"security": "安全",
"segment": "细分",
@@ -381,7 +380,6 @@
"survey_live": "调查 运行中",
"survey_not_found": "调查 未找到",
"survey_paused": "调查 暂停。",
"survey_scheduled": "调查 计划好的",
"survey_type": "调查 类型",
"surveys": "调查",
"switch_to": "切换到 {environment}",
@@ -762,45 +760,21 @@
"unable_to_delete_api_key": "无法删除 API Key"
},
"app-connection": {
"api_host_description": "这是你的 Formbricks 后台的 URL 。",
"app_connection": "应用程序 连接",
"app_connection_description": "连接 您 的 应用 与 Formbricks。",
"cache_update_delay_description": "当 你 对 调查 、 联系人 、 操作 或 其他 数据 进行 更新 时 可能 需要 最多 5 分钟 更改 才能 显示 在 你 本地 运行 Formbricks SDK 的 应用程序 中 。 这个 延迟 是 由于 我们 当前 缓存 系统 的 限制 。 我们 正在 积极 重新设计 缓存 并 将 在 Formbricks 4.0 中 发布 修复 。",
"cache_update_delay_title": "更改 将 在 5 分钟 后 由于 缓存 而 显示",
"check_out_the_docs": "查看 文档。",
"dive_into_the_docs": "深入 文档。",
"does_your_widget_work": "您的 widget 工作吗?",
"environment_id": "你的 环境 ID",
"environment_id_description": "这个 id 独特地 标识 这个 Formbricks 环境。",
"environment_id_description_with_environment_id": "用于识别正确环境: {environmentId} 是你的。",
"formbricks_sdk": "Formbricks SDK",
"formbricks_sdk_connected": "Formbricks SDK 已连接",
"formbricks_sdk_not_connected": "Formbricks SDK 尚未连接。",
"formbricks_sdk_not_connected_description": "连接 您 的 网站 或 应用 与 Formbricks",
"have_a_problem": "有问题?",
"how_to_setup": "如何设置",
"how_to_setup_description": "遵循这些步骤在你的应用中设置 Formbricks 小部件。",
"identifying_your_users": "识别 您 的 用户",
"if_you_are_planning_to": "如果你 正在计划",
"insert_this_code_into_the": "将此代码插入到",
"need_a_more_detailed_setup_guide_for": "需要 更详细 的 设置 指南 吗",
"not_working": "不好用?",
"open_an_issue_on_github": "在 GitHub 上 提交 问题。",
"open_the_browser_console_to_see_the_logs": "打开 浏览器 控制台 查看 日志。",
"receiving_data": "接收 数据 \uD83D\uDC83\uD83D\uDD7A",
"recheck": "重新检查",
"scroll_to_the_top": "滚动 到 顶部!",
"step_1": "步骤 1: 使用 pnpm 、npm 或 yarn 安装",
"step_2": "步骤 2: 初始化小组件",
"step_2_description": "在你的组件 (例如 App.tsx) 中 导入 Formbricks 并初始化 小组件 :",
"step_3": "步骤 3: 调试 模式",
"switch_on_the_debug_mode_by_appending": "通过附加 启用 调试 模式",
"tag_of_your_app": "您的 应用 标签",
"to_the_url_where_you_load_the": "到您加载的 URL ",
"want_to_learn_how_to_add_user_attributes": "想 学习 如何 添加 用户 属性、 自定义 事件 等等 吗?",
"you_are_done": "完成了 \uD83C\uDF89",
"you_can_set_the_user_id_with": "您 可以 设置 用户 ID 通过 ",
"your_app_now_communicates_with_formbricks": "您的 app 现在可与 Formbricks 通信 - 自动发送 events 和加载 surveys"
"setup_alert_description": "按照 此 步骤教程 在 5 分钟 以内 连接 你的 应用 或 网站。",
"setup_alert_title": "如何 连接"
},
"general": {
"cannot_delete_only_project": "这是 您 唯一的 项目,不可 删除。请 先 创建一个新的 项目。",
@@ -1253,9 +1227,7 @@
"automatically_close_survey_after": "自动 关闭 调查 后",
"automatically_close_the_survey_after_a_certain_number_of_responses": "自动 关闭 调查 在 达到 一定数量 的 回应 后",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "用户未在一定秒数内应答时 自动关闭 问卷",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "自动在 每天 开始时 UTC 关闭 调查",
"automatically_mark_the_survey_as_complete_after": "自动 标记 调查 为 完成 在",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "自动在 每天 开始时 UTC 发布 调查",
"back_button_label": "\"返回\" 按钮标签",
"background_styling": "背景 样式",
"brand_color": "品牌 颜色",
@@ -1303,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "选择 触发 调查 的 动作 。",
"choose_where_to_run_the_survey": "选择 调查 运行 的 位置 。",
"city": "城市",
"close_survey_on_date": "在日期关闭 调查",
"close_survey_on_response_limit": "在响应限制时关闭 调查",
"color": "颜色",
"column_used_in_logic_error": "\"这个 列 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
@@ -1511,7 +1482,6 @@
"redirect_thank_you_card": "重定向感谢卡",
"redirect_to_url": "重定向到 URL",
"redirect_to_url_not_available_on_free_plan": "重 定 向 到 URL 不 可 用 于 免 费 计 划",
"release_survey_on_date": "在日期发布 调查",
"remove_description": "移除 描述",
"remove_translations": "移除 翻译",
"require_answer": "需要回答",
@@ -1855,7 +1825,6 @@
"survey_deleted_successfully": "调查 删除 成功",
"survey_duplicated_successfully": "调查成功复制。",
"survey_duplication_error": "无法复制 调查。",
"survey_status_tooltip": "要更新 调查 状态,更新 调查回答 选项中的计划和关闭 设置。",
"templates": {
"all_channels": "所有 渠道",
"all_industries": "所有 行业",

View File

@@ -264,7 +264,6 @@
"multiple_languages": "多種語言",
"name": "名稱",
"new": "新增",
"new_survey": "新增問卷",
"new_version_available": "Formbricks '{'version'}' 已推出。立即升級!",
"next": "下一步",
"no_background_image_found": "找不到背景圖片。",
@@ -342,7 +341,6 @@
"save": "儲存",
"save_changes": "儲存變更",
"saving": "儲存",
"scheduled": "已排程",
"search": "搜尋",
"security": "安全性",
"segment": "區隔",
@@ -382,7 +380,6 @@
"survey_live": "問卷已上線",
"survey_not_found": "找不到問卷",
"survey_paused": "問卷已暫停。",
"survey_scheduled": "問卷已排程。",
"survey_type": "問卷類型",
"surveys": "問卷",
"switch_to": "切換至 '{'environment'}'",
@@ -763,47 +760,21 @@
"unable_to_delete_api_key": "無法刪除 API 金鑰"
},
"app-connection": {
"api_host_description": "這是您 Formbricks 後端的網址。",
"app_connection": "應用程式連線",
"app_connection_description": "將您的應用程式連線至 Formbricks。",
"cache_update_delay_description": "當您對調查、聯絡人、操作或其他資料進行更新時,可能需要長達 5 分鐘這些變更才能顯示在執行 Formbricks SDK 的本地應用程式中。此延遲是因我們目前快取系統的限制。我們正積極重新設計快取,並將在 Formbricks 4.0 中發佈修補程式。",
"cache_update_delay_title": "更改將於 5 分鐘後因快取而反映",
"check_out_the_docs": "查看文件。",
"dive_into_the_docs": "深入瞭解文件。",
"does_your_widget_work": "您的小工具運作嗎?",
"environment_id": "您的 EnvironmentId",
"environment_id_description": "此 ID 可唯一識別此 Formbricks 環境。",
"environment_id_description_with_environment_id": "用於識別正確的環境:'{'environmentId'}' 是您的。",
"formbricks_sdk": "Formbricks SDK",
"formbricks_sdk_connected": "Formbricks SDK 已連線",
"formbricks_sdk_not_connected": "Formbricks SDK 尚未連線。",
"formbricks_sdk_not_connected_description": "將您的網站或應用程式與 Formbricks 連線",
"have_a_problem": "有問題嗎?",
"how_to_setup": "如何設定",
"how_to_setup_description": "請按照這些步驟在您的應用程式中設定 Formbricks 小工具。",
"identifying_your_users": "識別您的使用者",
"if_you_are_planning_to": "如果您計劃",
"insert_this_code_into_the": "將此程式碼插入",
"need_a_more_detailed_setup_guide_for": "需要更詳細的設定指南,適用於",
"not_working": "無法運作?",
"open_an_issue_on_github": "在 GitHub 上開啟問題",
"open_the_browser_console_to_see_the_logs": "開啟瀏覽器主控台以查看記錄。",
"receiving_data": "正在接收資料 \uD83D\uDC83\uD83D\uDD7A",
"recheck": "重新檢查",
"scroll_to_the_top": "捲動至頂端!",
"setup_alert_description": "遵循 此 分步 教程 ,在 5 分鐘 內 將您的應用程式 或 網站 連線 。",
"setup_alert_title": "如何 連線",
"step_1": "步驟 1使用 pnpm、npm 或 yarn 安裝",
"step_2": "步驟 2初始化小工具",
"step_2_description": "匯入 Formbricks 並在您的元件中初始化小工具例如App.tsx",
"step_3": "步驟 3偵錯模式",
"switch_on_the_debug_mode_by_appending": "藉由附加以下項目開啟偵錯模式",
"tag_of_your_app": "您應用程式的標籤",
"to_the_url_where_you_load_the": "到您載入",
"want_to_learn_how_to_add_user_attributes": "想瞭解如何新增使用者屬性、自訂事件等嗎?",
"you_are_done": "您已完成 \uD83C\uDF89",
"you_can_set_the_user_id_with": "您可以使用 user id 設定",
"your_app_now_communicates_with_formbricks": "您的應用程式現在可與 Formbricks 通訊 - 自動傳送事件和載入問卷!"
"setup_alert_title": "如何 連線"
},
"general": {
"cannot_delete_only_project": "這是您唯一的專案,無法刪除。請先建立新專案。",
@@ -1256,9 +1227,7 @@
"automatically_close_survey_after": "在指定時間自動關閉問卷",
"automatically_close_the_survey_after_a_certain_number_of_responses": "在收到一定數量的回覆後自動關閉問卷。",
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "如果用戶在特定秒數後未回應,則自動關閉問卷。",
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "在指定日期UTC時間自動關閉問卷。",
"automatically_mark_the_survey_as_complete_after": "在指定時間後自動將問卷標記為完成",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "在指定日期UTC時間自動發佈問卷。",
"back_button_label": "「返回」按鈕標籤",
"background_styling": "背景樣式設定",
"brand_color": "品牌顏色",
@@ -1306,7 +1275,6 @@
"choose_the_actions_which_trigger_the_survey": "選擇觸發問卷的操作。",
"choose_where_to_run_the_survey": "選擇在哪裡執行問卷。",
"city": "城市",
"close_survey_on_date": "在指定日期關閉問卷",
"close_survey_on_response_limit": "在回應次數上限關閉問卷",
"color": "顏色",
"column_used_in_logic_error": "此 column 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
@@ -1514,7 +1482,6 @@
"redirect_thank_you_card": "重新導向感謝卡片",
"redirect_to_url": "重新導向至網址",
"redirect_to_url_not_available_on_free_plan": "重新導向至網址在免費方案中不可用",
"release_survey_on_date": "在指定日期發佈問卷",
"remove_description": "移除描述",
"remove_translations": "移除翻譯",
"require_answer": "要求回答",
@@ -1858,7 +1825,6 @@
"survey_deleted_successfully": "問卷已成功刪除!",
"survey_duplicated_successfully": "問卷已成功複製。",
"survey_duplication_error": "無法複製問卷。",
"survey_status_tooltip": "若要更新問卷狀態,請更新問卷回應選項中的排程和關閉設定。",
"templates": {
"all_channels": "所有管道",
"all_industries": "所有產業",

View File

@@ -1,10 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) 2025 Formbricks GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,53 +0,0 @@
# Setup
To run the project, open the `android` folder in **Android Studio**. The project contains two modules:
- **app**: A demo application to exercise the SDK.
- **formbricksSDK**: The SDK package.
Before launching the app, update the mandatory variables in `MainActivity`:
```kotlin
val config = FormbricksConfig.Builder("[API_HOST]", "[ENVIRONMENT_ID]")
.setLoggingEnabled(true)
.setFragmentManager(supportFragmentManager)
```
Once these values are properly set, the demo app can be launched.
The app consists of a single view, `FormbricksDemo`. It is a very simple Jetpack Compose view with a single button.
The button's action should be updated according to the survey actions:
```kotlin
Formbricks.track("click_demo_button")
```
Replace `"click_demo_button"` with the desired action.
---
# Documentation
You can generate developer documentation for the SDK using **Dokka**.
To do this, navigate to the `android` folder in a `Terminal` window (or open it in Android Studio) and run:
```sh
./gradlew dokkaHtml
```
This will generate the developer documentation in the `formbricksSDK/build/dokka/html` folder.
---
## Unit Tests
The SDK includes a unit test to verify the Manager's functionality. To run it:
1. Open the `FormbricksInstrumentedTest` file.
2. Since the SDK requires a `Context` for initialization, it uses an instrumented test.
3. Click the double arrow next to `class FormbricksInstrumentedTest` to execute the tests.
To generate a coverage report, navigate to the `android` folder in a `Terminal` window (or open it in Android Studio) and run:
```sh
./gradlew createDebugCoverageReport
```

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,54 +0,0 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
kotlin("kapt")
}
android {
namespace = "com.formbricks.demo"
compileSdk = 35
defaultConfig {
applicationId = "com.formbricks.demo"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
dataBinding = true
}
}
dependencies {
implementation(project(":formbricksSDK"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

View File

@@ -1,26 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn androidx.databinding.**
-keep class androidx.databinding.** { *; }
-keep class * extends androidx.databinding.DataBinderMapper { *; }
-keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; }

View File

@@ -1,24 +0,0 @@
package com.formbricks.demo
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.formbricks.demo", appContext.packageName)
}
}

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.Demo"
tools:targetApi="31" >
<activity
android:name=".MainActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,91 +0,0 @@
package com.formbricks.demo
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.FormbricksCallback
import com.formbricks.formbrickssdk.helper.FormbricksConfig
import com.formbricks.formbrickssdk.model.enums.SuccessType
import java.util.UUID
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
Formbricks.callback = object: FormbricksCallback {
override fun onSurveyStarted() {
Log.d("FormbricksCallback", "onSurveyStarted")
}
override fun onSurveyFinished() {
Log.d("FormbricksCallback", "onSurveyFinished")
}
override fun onSurveyClosed() {
Log.d("FormbricksCallback", "onSurveyClosed")
}
override fun onPageCommitVisible() {
Log.d("FormbricksCallback", "onPageCommitVisible")
}
override fun onError(error: Exception) {
Log.d("FormbricksCallback", "onError from the CB: ${error.localizedMessage}")
}
override fun onSuccess(successType: SuccessType) {
Log.d("FormbricksCallback", "onSuccess: ${successType.name}")
}
}
val config = FormbricksConfig.Builder("[appUrl]","[environmentId]")
.setLoggingEnabled(true)
.setFragmentManager(supportFragmentManager)
Formbricks.setup(this, config.build())
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
Formbricks.track("click_demo_button")
}
val setUserIdButton = findViewById<Button>(R.id.setUserId)
setUserIdButton.setOnClickListener {
Formbricks.setUserId(UUID.randomUUID().toString())
}
val setAttributeButton = findViewById<Button>(R.id.setAttribute)
setAttributeButton.setOnClickListener {
Formbricks.setAttribute("test@web.com", "email")
}
val setAttributesButton = findViewById<Button>(R.id.setAttributes)
setAttributesButton.setOnClickListener {
Formbricks.setAttributes(mapOf(Pair("attr1", "val1"), Pair("attr2", "val2")))
}
val setLanguageButton = findViewById<Button>(R.id.setLanguage)
setLanguageButton.setOnClickListener {
Formbricks.setLanguage("vi")
}
val logoutButton = findViewById<Button>(R.id.logout)
logoutButton.setOnClickListener {
Formbricks.logout()
}
}
}

View File

@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -1,93 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Track Action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.24" />
<Button
android:id="@+id/setUserId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="27dp"
android:layout_marginEnd="154dp"
android:layout_marginBottom="12dp"
android:text="setUserId"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@+id/setLanguage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
tools:visibility="visible" />
<Button
android:id="@+id/setLanguage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="128dp"
android:layout_marginBottom="11dp"
android:text="setLanguage"
app:layout_constraintBottom_toTopOf="@+id/setAttribute"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setUserId" />
<Button
android:id="@+id/setAttribute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="128dp"
android:layout_marginBottom="2dp"
android:text="setAttribute"
app:layout_constraintBottom_toTopOf="@+id/setAttributes"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setLanguage" />
<Button
android:id="@+id/setAttributes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="120dp"
android:layout_marginBottom="10dp"
android:text="setAttributes"
app:layout_constraintBottom_toTopOf="@+id/logout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setAttribute" />
<Button
android:id="@+id/logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="177dp"
android:layout_marginTop="9dp"
android:layout_marginEnd="146dp"
android:layout_marginBottom="199dp"
android:text="logout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setAttributes" />
</androidx.constraintlayout.widget.ConstraintLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
<string name="app_name">Demo</string>
</resources>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Demo" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- No specific include/exclude -->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">192.168.29.120</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>

View File

@@ -1,17 +0,0 @@
package com.formbricks.demo
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -1,6 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.android.library) apply false
}

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,97 +0,0 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
kotlin("plugin.serialization") version "2.1.0"
id("org.jetbrains.dokka") version "1.9.10"
id("jacoco")
}
android {
namespace = "com.formbricks.formbrickssdk"
compileSdk = 35
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
getByName("debug") {
enableAndroidTestCoverage = true
}
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
packaging {
resources {
excludes += "META-INF/library_release.kotlin_module"
excludes += "classes.dex"
excludes += "**.**"
pickFirsts += "**/DataBinderMapperImpl.java"
pickFirsts += "**/DataBinderMapperImpl.class"
pickFirsts += "**/formbrickssdk/DataBinderMapperImpl.java"
pickFirsts += "**/formbrickssdk/DataBinderMapperImpl.class"
}
}
viewBinding {
enable = true
}
dataBinding {
enable = true
}
buildFeatures {
dataBinding = true
viewBinding = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
tasks.withType<Test>().configureEach {
extensions.configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = true
excludes = listOf(
"jdk.internal.*",
)
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.annotation)
implementation(libs.androidx.appcompat)
implementation(libs.gson)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.retrofit.converter.scalars)
implementation(libs.okhttp3.logging.interceptor)
implementation(libs.material)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.legacy.support.v4)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.databinding.common)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(project(":formbricksSDK"))
}

View File

@@ -1,5 +0,0 @@
-keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; }
-keep class com.formbricks.formbrickssdk.Formbricks { *; }
-keep class com.formbricks.formbrickssdk.helper.FormbricksConfig { *; }
-keep class com.formbricks.formbrickssdk.model.error.SDKError { *; }
-keep interface com.formbricks.formbrickssdk.FormbricksCallback { *; }

View File

@@ -1,38 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-keeppackagenames com.formbricks.**
-keep class com.formbricks.** { *; }
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-keepattributes SourceFile,LineNumberTable,Exceptions,InnerClasses,Signature,Deprecated,*Annotation*,EnclosingMethod
# add all known-to-be-safely-shrinkable classes to the beginning of line below
-keep class !androidx.legacy.**,!com.google.android.**,!androidx.** { *; }
-keep class android.support.v4.app.** { *; }
# Retrofit
-dontwarn okio.**
-keep class com.squareup.okhttp.** { *; }
-keep interface com.squareup.okhttp.** { *; }
-keep class retrofit.** { *; }
-dontwarn com.squareup.okhttp.**
-keep class retrofit.** { *; }
-keepclasseswithmembers class * {
@retrofit.http.* <methods>;
}
-keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; }
-keep class com.formbricks.formbrickssdk.Formbricks { *; }
-keep class com.formbricks.formbrickssdk.helper.FormbricksConfig { *; }
-keep class com.formbricks.formbrickssdk.model.error.SDKError { *; }
-keep interface com.formbricks.formbrickssdk.FormbricksCallback { *; }

View File

@@ -1,167 +0,0 @@
package com.formbricks.formbrickssdk
import androidx.fragment.app.FragmentManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.formbricks.formbrickssdk.api.FormbricksApi
import com.formbricks.formbrickssdk.helper.FormbricksConfig
import com.formbricks.formbrickssdk.manager.SurveyManager
import com.formbricks.formbrickssdk.manager.UserManager
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class FormbricksInstrumentedTest {
private val environmentId = "environmentId"
private val appUrl = "http://appUrl"
private val userId = "6CCCE716-6783-4D0F-8344-9C7DFA43D8F7"
private val surveyID = "cm6ovw6j7000gsf0kduf4oo4i"
@Before
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.applicationContext = appContext
UserManager.logout()
SurveyManager.environmentDataHolder = null
FormbricksApi.service = MockFormbricksApiService()
}
@Test
fun testFormbricks() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.formbricks.formbrickssdk.test", appContext.packageName)
// Everything should be in the default state
assertFalse(Formbricks.isInitialized)
assertEquals(0, SurveyManager.filteredSurveys.size)
assertNull(SurveyManager.environmentDataHolder)
assertNull(UserManager.userId)
assertEquals("default", Formbricks.language)
// Use methods before init should have no effect
Formbricks.setUserId("userId")
Formbricks.setLanguage("de")
Formbricks.setAttributes(mapOf("testA" to "testB"))
Formbricks.setAttribute("test", "testKey")
assertNull(UserManager.userId)
assertEquals("default", Formbricks.language)
Formbricks.track("click_demo_button")
waitForSeconds(1)
assertFalse(SurveyManager.isShowingSurvey)
Formbricks.logout()
Formbricks.setFragmentManager(MockFragmentManager())
Formbricks.setLanguage("")
// Call the setup and initialize the SDK
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)
// Should be ignored, becuase we don't have user ID yet
Formbricks.setAttributes(mapOf("testA" to "testB"))
Formbricks.setAttribute("test", "testKey")
assertNull(UserManager.userId)
// Verify the base variables are set properly
assertTrue(Formbricks.isInitialized)
assertEquals(appUrl, Formbricks.appUrl)
assertEquals(environmentId, Formbricks.environmentId)
// User manager default state. There is no user yet.
assertEquals(UserManager.displays?.count(), 0)
assertEquals(UserManager.responses?.count(), 0)
assertEquals(UserManager.segments?.count(), 0)
// Check error state handling
(FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = true
assertFalse(SurveyManager.hasApiError)
SurveyManager.refreshEnvironmentIfNeeded(true)
waitForSeconds(1)
assertTrue(SurveyManager.hasApiError)
(FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = false
// Authenticate the user
Formbricks.setUserId(userId)
waitForSeconds(2)
assertEquals(userId, UserManager.userId)
assertNotNull(UserManager.syncTimer)
// The environment should be fetched already
assertNotNull(SurveyManager.environmentDataHolder)
// Check if the filter method works properly
assertEquals(1, SurveyManager.filteredSurveys.size)
assertFalse(SurveyManager.isShowingSurvey)
// Track an unknown event, shouldn't show the survey
Formbricks.track("unknown_event")
assertFalse(SurveyManager.isShowingSurvey)
// Track a known event, thus, the survey should be shown.
SurveyManager.isShowingSurvey = false
Formbricks.track("click_demo_button")
waitForSeconds(1)
assertTrue(SurveyManager.isShowingSurvey)
// Validate display and response
SurveyManager.onNewDisplay(surveyID)
SurveyManager.postResponse(surveyID)
assertEquals(1, UserManager.responses?.size)
assertEquals(1, UserManager.displays?.size)
// Track a valid event, but the survey should not shown, because we already gave a response.
SurveyManager.isShowingSurvey = false
Formbricks.track("click_demo_button")
waitForSeconds(1)
assertFalse(SurveyManager.isShowingSurvey)
// Validate logout
assertNotNull(UserManager.userId)
assertNotNull(UserManager.lastDisplayedAt)
assertNotEquals(UserManager.displays?.count(), 0)
assertNotEquals(UserManager.responses?.count(), 0)
assertNotEquals(UserManager.segments?.count(), 0)
assertNotNull(UserManager.expiresAt)
Formbricks.logout()
assertNull(UserManager.userId)
assertNull(UserManager.lastDisplayedAt)
assertNull(UserManager.displays)
assertEquals(UserManager.responses?.count(), 0)
assertEquals(UserManager.segments?.count(), 0)
assertNull(UserManager.expiresAt)
// Setting the language
assertEquals("default", Formbricks.language)
Formbricks.setLanguage("de")
assertEquals("de", Formbricks.language)
// Clear the responses
Formbricks.logout()
SurveyManager.filterSurveys()
Formbricks.track("click_demo_button")
waitForSeconds(1)
assertTrue(SurveyManager.isShowingSurvey)
}
private fun waitForSeconds(seconds: Long) {
val latch = CountDownLatch(1)
latch.await(seconds, TimeUnit.SECONDS)
}
}
class MockFragmentManager : FragmentManager()

View File

@@ -1,35 +0,0 @@
package com.formbricks.formbrickssdk
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.environment.EnvironmentResponse
import com.formbricks.formbrickssdk.model.user.PostUserBody
import com.formbricks.formbrickssdk.model.user.UserResponse
import com.formbricks.formbrickssdk.network.FormbricksApiService
import com.google.gson.Gson
class MockFormbricksApiService: FormbricksApiService() {
private val gson = Gson()
private val environmentJson = MockFormbricksApiService::class.java.getResource("/Environment.json")!!.readText()
private val userJson = MockFormbricksApiService::class.java.getResource("/User.json")!!.readText()
private val environment = gson.fromJson(environmentJson, EnvironmentResponse::class.java)
private val user = gson.fromJson(userJson, UserResponse::class.java)
var isErrorResponseNeeded = false
override fun getEnvironmentStateObject(environmentId: String): Result<EnvironmentDataHolder> {
return if (isErrorResponseNeeded) {
Result.failure(RuntimeException())
} else {
Result.success(EnvironmentDataHolder(environment.data, mapOf()))
}
}
override fun postUser(environmentId: String, body: PostUserBody): Result<UserResponse> {
return if (isErrorResponseNeeded) {
Result.failure(RuntimeException())
} else {
Result.success(user)
}
}
}

View File

@@ -1,372 +0,0 @@
{
"data": {
"data": {
"actionClasses": [
{
"id": "cm6ow6hht000isf0k39hbmi5f",
"key": "click_demo_button",
"name": "Clicked the demo button",
"noCodeConfig": null,
"type": "code"
}
],
"project": {
"clickOutsideClose": true,
"darkOverlay": false,
"id": "cm6ovvfnv0003sf0k7zi8r3ac",
"inAppSurveyBranding": true,
"placement": "bottomRight",
"recontactDays": 7,
"styling": {
"allowStyleOverwrite": true,
"brandColor": {
"light": "#126dec"
}
}
},
"surveys": [
{
"autoClose": null,
"delay": 0,
"displayLimit": 1,
"displayOption": "displayMultiple",
"displayPercentage": 100,
"endings": [
{
"buttonLabel": {
"default": "Create your own Survey\u200c\u200c\u200d\u200d\u200c\u200c\u200d\u200c\u200c\u200c\u200c\u200d\u200d\u200d\u200c\u200c\u200d\u200c\u200c\u200c\u200d\u200d\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200d\u200c\u200d\u200c\u200c"
},
"buttonLink": "https://formbricks.com",
"headline": {
"default": "Thank you!\u200c\u200c\u200d\u200d\u200c\u200c\u200d\u200c\u200c\u200c\u200c\u200d\u200d\u200d\u200c\u200c\u200c\u200c\u200c\u200c\u200d\u200d\u200d\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200d\u200c\u200d\u200c\u200c"
},
"id": "ik26bumalvsg2hs0sz7kd0dh",
"subheader": {
"default": "We appreciate your feedback.\u200c\u200c\u200d\u200d\u200c\u200c\u200d\u200c\u200c\u200c\u200c\u200d\u200d\u200d\u200c\u200c\u200c\u200c\u200c\u200c\u200d\u200d\u200d\u200c\u200c\u200d\u200c\u200c\u200c\u200c\u200c\u200d\u200c\u200d\u200c\u200c"
},
"type": "endScreen"
}
],
"hiddenFields": {
"enabled": true,
"fieldIds": []
},
"id": "cm6ovw6j7000gsf0kduf4oo4i",
"isBackButtonHidden": false,
"languages": [],
"name": "Start from scratch",
"projectOverwrites": null,
"questions": [
{
"allowMultipleFiles": true,
"allowedFileExtensions": ["jpeg", "jpg", "png"],
"backButtonLabel": {
"default": "Back"
},
"buttonLabel": {
"default": "Next"
},
"headline": {
"default": "Upload some file"
},
"id": "bak70kjkubxq66eup7i6l4lp",
"logic": [],
"required": false,
"subheader": {
"default": "Yeeeaaahh"
},
"type": "fileUpload"
},
{
"backButtonLabel": {
"default": "Back\u200c\u200c\u200d\u200d\u200c\u200c\u200c\u200d\u200c\u200c\u200c\u200d\u200d\u200c\u200c\u200d\u200c\u200c\u200c\u200c\u200d\u200d\u200c\u200d\u200c\u200c\u200c\u200c\u200c\u200c\u200c\u200d\u200c\u200d\u200c\u200c"
},
"buttonExternal": true,
"buttonLabel": {
"default": "Book interview\u200c\u200c\u200d\u200d\u200c\u200c\u200c\u200d\u200c\u200c\u200c\u200d\u200d\u200c\u200d\u200c\u200c\u200c\u200c\u200c\u200d\u200d\u200c\u200d\u200c\u200d\u200c\u200c\u200c\u200c\u200c\u200d\u200c\u200d\u200c\u200c"
},
"buttonUrl": "https://telex.hu",
"dismissButtonLabel": {
"default": "Skip\u200c\u200c\u200d\u200d\u200c\u200c\u200c\u200d\u200c\u200c\u200c\u200d\u200d\u200c\u200d\u200c\u200c\u200c\u200c\u200c\u200d\u200d\u200c\u200d\u200d\u200c\u200c\u200c\u200c\u200c\u200c\u200d\u200c\u200d\u200c\u200c"
},
"headline": {
"default": "Click something"
},
"html": {
"default": "<p class=\"fb-editor-paragraph\"><br></p><p class=\"fb-editor-paragraph\"><br></p><p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">yeah</span></p><p class=\"fb-editor-paragraph\"><br></p>"
},
"id": "zmjh52gfc92yo4ph2zru4nhn",
"required": true,
"type": "cta"
},
{
"buttonLabel": {
"default": "Next"
},
"charLimit": {
"enabled": false
},
"headline": {
"default": "How are things going?"
},
"id": "ve69a3mrkqixhtom8y2mtd9q",
"inputType": "text",
"logic": [],
"longAnswer": true,
"placeholder": {
"default": "This is a placeholder. Type your answer though.."
},
"required": false,
"type": "openText"
},
{
"backButtonLabel": {
"default": "Back"
},
"buttonLabel": {
"default": "Next"
},
"headline": {
"default": "Rate this question"
},
"id": "qo5khwchqa7g34qzdwb7p6kr",
"isColorCodingEnabled": false,
"lowerLabel": {
"default": "Not good"
},
"range": 5,
"required": true,
"scale": "smiley",
"type": "rating",
"upperLabel": {
"default": "Very good"
}
},
{
"backButtonLabel": {
"default": "Back"
},
"buttonLabel": {
"default": "Next"
},
"choices": [
{
"id": "wr01x35k5spyaz5x1d7vsa8g",
"label": {
"default": "One"
}
},
{
"id": "zvqn3rj26ph3krr1e4n8vn91",
"label": {
"default": "Kett\u0151"
}
},
{
"id": "f4e2xuspnajsz8kk7ctrc7sn",
"label": {
"default": "Tres"
}
}
],
"headline": {
"default": "Select something"
},
"id": "prdku49xb0gfpmipwc97scd4",
"required": true,
"shuffleOption": "none",
"type": "multipleChoiceMulti"
},
{
"backButtonLabel": {
"default": "Back"
},
"buttonLabel": {
"default": "Next"
},
"format": "M-d-y",
"headline": {
"default": "Select a date please"
},
"id": "yjape17lgfe7v74gbxwadj04",
"required": true,
"type": "date"
},
{
"addressLine1": {
"placeholder": {
"default": "Address Line 1"
},
"required": false,
"show": true
},
"addressLine2": {
"placeholder": {
"default": "Address Line 2"
},
"required": false,
"show": false
},
"backButtonLabel": {
"default": "Back"
},
"buttonLabel": {
"default": "Next"
},
"city": {
"placeholder": {
"default": "City"
},
"required": false,
"show": true
},
"country": {
"placeholder": {
"default": "Country"
},
"required": false,
"show": true
},
"headline": {
"default": "What is your address?"
},
"id": "z78kl00vherihw2oqv2loj2y",
"required": false,
"state": {
"placeholder": {
"default": "State"
},
"required": false,
"show": true
},
"type": "address",
"zip": {
"placeholder": {
"default": "Zip"
},
"required": false,
"show": true
}
},
{
"backButtonLabel": {
"default": "Back"
},
"buttonLabel": {
"default": "Next"
},
"headline": {
"default": "I dare you to check this!"
},
"html": {
"default": "<p class=\"fb-editor-paragraph\"><br></p><p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">I double dare you</span></p>"
},
"id": "qq30m24f14vhw356kqfzlrlr",
"label": {
"default": "Oookay"
},
"required": true,
"type": "consent"
},
{
"allowMulti": true,
"backButtonLabel": {
"default": "Back"
},
"buttonLabel": {
"default": "Next"
},
"choices": [
{
"id": "m8uxhdlq4yerpzfykb6mfpeb",
"imageUrl": "http://localhost:3000/storage/cm6ovvfoc000asf0k39wbzc8s/public/Screenshot%25202021-10-18%2520at%252011.35.15--fid--e7d90f81-56fb-4ed7-ad64-3ec869bc96d6.png"
},
{
"id": "ayv30brvesfxrl7dmj2kig4u",
"imageUrl": "http://localhost:3000/storage/cm6ovvfoc000asf0k39wbzc8s/public/Screenshot%25202022-11-22%2520at%252014.37.51--fid--5e4edd58-62b0-4df8-a096-e8e59b1bf080.png"
}
],
"headline": {
"default": "Select a picture"
},
"id": "krehs9wlntwtjf9hlh8pp82w",
"required": true,
"type": "pictureSelection"
}
],
"recontactDays": 0,
"segment": {
"createdAt": "2025-02-03T10:04:21.922Z",
"description": null,
"environmentId": "cm6ovvfoc000asf0k39wbzc8s",
"filters": [],
"id": "cm6ovw6jl000hsf0knn547w0y",
"isPrivate": true,
"surveys": ["cm6ovw6j7000gsf0kduf4oo4i"],
"title": "cm6ovw6j7000gsf0kduf4oo4i",
"updatedAt": "2025-02-03T10:04:21.922Z"
},
"showLanguageSwitch": null,
"status": "inProgress",
"styling": {
"background": {
"bg": "#fff",
"bgType": "color"
},
"brandColor": {
"light": "#60fa0d"
},
"cardArrangement": {
"appSurveys": "straight",
"linkSurveys": "straight"
},
"cardBackgroundColor": {
"light": "#ffffff"
},
"cardBorderColor": {
"light": "#f8fafc"
},
"inputBorderColor": {
"light": "#090e14"
},
"inputColor": {
"light": "#ffffff"
},
"isDarkModeEnabled": false,
"isLogoHidden": false,
"overwriteThemeStyling": true,
"questionColor": {
"light": "#3a09ff"
},
"roundness": 8
},
"triggers": [
{
"actionClass": {
"name": "Clicked the demo button"
}
}
],
"type": "app",
"variables": [],
"welcomeCard": {
"buttonLabel": {
"default": "Next"
},
"enabled": false,
"fileUrl": "",
"headline": {
"default": "Welcome!"
},
"html": {
"default": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Thanks for providing your feedback - let's go!</span></p>"
},
"showResponseCount": false,
"timeToFinish": false
}
}
]
},
"expiresAt": "2035-03-06T10:33:38.647Z"
}
}

View File

@@ -1,15 +0,0 @@
{
"data": {
"state": {
"data": {
"contactId": "cm6ovw6jl000hsf0knn547xyz",
"displays": [],
"lastDisplayAt": null,
"responses": [],
"segments": ["cm6ovw6jl000hsf0knn547w0y"],
"userId": "6CCCE716-6783-4D0F-8344-9C7DFA43D8F7"
},
"expiresAt": "2035-03-06T10:59:32.359Z"
}
}
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="35" />
</manifest>

View File

@@ -1,256 +0,0 @@
package com.formbricks.formbrickssdk
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.annotation.Keep
import androidx.fragment.app.FragmentManager
import com.formbricks.formbrickssdk.api.FormbricksApi
import com.formbricks.formbrickssdk.helper.FormbricksConfig
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.manager.SurveyManager
import com.formbricks.formbrickssdk.manager.UserManager
import com.formbricks.formbrickssdk.model.enums.SuccessType
import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.webview.FormbricksFragment
import java.lang.RuntimeException
@Keep
interface FormbricksCallback {
fun onSurveyStarted()
fun onSurveyFinished()
fun onSurveyClosed()
fun onPageCommitVisible()
fun onError(error: Exception)
fun onSuccess(successType: SuccessType)
}
@Keep
object Formbricks {
internal lateinit var applicationContext: Context
internal lateinit var environmentId: String
internal lateinit var appUrl: String
internal var language: String = "default"
internal var loggingEnabled: Boolean = true
private var fragmentManager: FragmentManager? = null
internal var isInitialized = false
var callback: FormbricksCallback? = null
/**
* Initializes the Formbricks SDK with the given [Context] config [FormbricksConfig].
* This method is mandatory to be called, and should be only once per application lifecycle.
* To show a survey, the SDK needs a [FragmentManager] instance.
*
* ```
* class MainActivity : FragmentActivity() {
*
* override fun onCreate() {
* super.onCreate()
* val config = FormbricksConfig.Builder("http://localhost:3000","my_environment_id")
* .setLoggingEnabled(true)
* .setFragmentManager(supportFragmentManager)
* .build())
* Formbricks.setup(this, config.build())
* }
* }
* ```
*
*/
fun setup(context: Context, config: FormbricksConfig, forceRefresh: Boolean = false) {
if (isInitialized && !forceRefresh) {
val error = SDKError.sdkIsAlreadyInitialized
callback?.onError(error)
Logger.e(error)
return
}
applicationContext = context
appUrl = config.appUrl
environmentId = config.environmentId
loggingEnabled = config.loggingEnabled
fragmentManager = config.fragmentManager
config.userId?.let { UserManager.set(it) }
config.attributes?.let { UserManager.setAttributes(it) }
config.attributes?.get("language")?.let { UserManager.setLanguage(it) }
FormbricksApi.initialize()
SurveyManager.refreshEnvironmentIfNeeded(force = forceRefresh)
UserManager.syncUserStateIfNeeded()
isInitialized = true
}
/**
* Sets the user id for the current user with the given [String].
* The SDK must be initialized before calling this method.
*
* ```
* Formbricks.setUserId("my_user_id")
* ```
*
*/
fun setUserId(userId: String) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
if(UserManager.userId != null) {
val error = RuntimeException("A userId is already set ${UserManager.userId} - please call logout first before setting a new one")
callback?.onError(error)
Logger.e(error)
return
}
UserManager.set(userId)
}
/**
* Adds an attribute for the current user with the given [String] value and [String] key.
* The SDK must be initialized before calling this method.
*
* ```
* Formbricks.setAttribute("my_attribute", "key")
* ```
*
*/
fun setAttribute(attribute: String, key: String) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
UserManager.addAttribute(attribute, key)
}
/**
* Sets the user attributes for the current user with the given [Map] of [String] values and [String] keys.
* The SDK must be initialized before calling this method.
*
* ```
* Formbricks.setAttributes(mapOf(Pair("key", "my_attribute")))
* ```
*
*/
fun setAttributes(attributes: Map<String, String>) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
UserManager.setAttributes(attributes)
}
/**
* Sets the language for the current user with the given [String].
* The SDK must be initialized before calling this method.
*
* ```
* Formbricks.setLanguage("de")
* ```
*
*/
fun setLanguage(language: String) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
Formbricks.language = language
UserManager.setLanguage(language)
}
/**
* Tracks an action with the given [String]. The SDK will process the action and it will present the survey if any of them can be triggered.
* The SDK must be initialized before calling this method.
*
* ```
* Formbricks.track("button_clicked")
* ```
*
*/
fun track(action: String) {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
if (!isInternetAvailable()) {
val error = SDKError.connectionIsNotAvailable
callback?.onError(error)
Logger.e(error)
return
}
SurveyManager.track(action)
}
/**
* Logs out the current user. This will clear the user attributes and the user id.
* The SDK must be initialized before calling this method.
*
* ```
* Formbricks.logout()
* ```
*
*/
fun logout() {
if (!isInitialized) {
val error = SDKError.sdkIsNotInitialized
callback?.onError(error)
Logger.e(error)
return
}
callback?.onSuccess(SuccessType.LOGOUT_SUCCESS)
UserManager.logout()
}
/**
* Sets the [FragmentManager] instance. The SDK always needs the actual [FragmentManager] to
* display surveys, so make sure you update it whenever it changes.
* The SDK must be initialized before calling this method.
*
* ```
* Formbricks.setFragmentManager(supportFragmentMananger)
* ```
*
*/
fun setFragmentManager(fragmentManager: FragmentManager) {
this.fragmentManager = fragmentManager
}
/// Assembles the survey fragment and presents it
internal fun showSurvey(id: String) {
if (fragmentManager == null) {
val error = SDKError.fragmentManagerIsNotSet
callback?.onError(error)
Logger.e(error)
return
}
fragmentManager?.let {
FormbricksFragment.show(it, surveyId = id)
}
}
/// Checks if the phone has active network connection
private fun isInternetAvailable(): Boolean {
val connectivityManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}

View File

@@ -1,58 +0,0 @@
package com.formbricks.formbrickssdk.api
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.user.PostUserBody
import com.formbricks.formbrickssdk.model.user.UserResponse
import com.formbricks.formbrickssdk.network.FormbricksApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
object FormbricksApi {
var service = FormbricksApiService()
private suspend fun <T> retryApiCall(
retries: Int = 2,
delayTime: Long = 1000,
block: suspend () -> Result<T>
): Result<T> {
repeat(retries) { attempt ->
val result = block()
if (result.isSuccess) return result
println("⚠️ Retry ${attempt + 1} due to error: ${result.exceptionOrNull()?.localizedMessage}")
delay(delayTime)
}
return block()
}
fun initialize() {
service.initialize(
appUrl = Formbricks.appUrl,
isLoggingEnabled = Formbricks.loggingEnabled
)
}
suspend fun getEnvironmentState(): Result<EnvironmentDataHolder> = withContext(Dispatchers.IO) {
retryApiCall {
try {
val response = service.getEnvironmentStateObject(Formbricks.environmentId)
val result = response.getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
}
suspend fun postUser(userId: String, attributes: Map<String, *>?): Result<UserResponse> = withContext(Dispatchers.IO) {
retryApiCall {
try {
val result = service.postUser(Formbricks.environmentId, PostUserBody.create(userId, attributes)).getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
}
}

View File

@@ -1,9 +0,0 @@
package com.formbricks.formbrickssdk.api.error
import com.google.gson.annotations.SerializedName
data class FormbricksAPIError(
@SerializedName("code") val code: String,
@SerializedName("message") val messageText: String,
@SerializedName("details") val details: Map<String, String>? = null
) : RuntimeException(messageText)

View File

@@ -1,59 +0,0 @@
package com.formbricks.formbrickssdk.extensions
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.user.UserState
import com.formbricks.formbrickssdk.model.user.UserStateData
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
internal const val dateFormatPattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
fun Date.dateString(): String {
val dateFormat = SimpleDateFormat(dateFormatPattern, Locale.getDefault())
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
return dateFormat.format(this)
}
fun UserStateData.lastDisplayAt(): Date? {
lastDisplayAt?.let {
try {
val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault())
formatter.timeZone = TimeZone.getTimeZone("UTC")
return formatter.parse(it)
} catch (e: Exception) {
return null
}
}
return null
}
fun UserState.expiresAt(): Date? {
expiresAt?.let {
try {
val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault())
formatter.timeZone = TimeZone.getTimeZone("UTC")
return formatter.parse(it)
} catch (e: Exception) {
return null
}
}
return null
}
fun EnvironmentDataHolder.expiresAt(): Date? {
data?.expiresAt?.let {
try {
val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault())
formatter.timeZone = TimeZone.getTimeZone("UTC")
return formatter.parse(it)
} catch (e: Exception) {
return null
}
}
return null
}

View File

@@ -1,33 +0,0 @@
package com.formbricks.formbrickssdk.extensions
/**
* Swift like guard statement.
* To achieve that, on null the statement must return an empty T object
*/
inline fun <reified T> T?.guard(block: T?.() -> Unit): T {
this?.let {
return it
} ?: run {
block()
}
return T::class.java.newInstance()
}
inline fun String?.guardEmpty(block: String?.() -> Unit): String {
if (isNullOrBlank()) {
block()
} else {
return this
}
return ""
}
inline fun <T: Any> guardLet(vararg elements: T?, closure: () -> Nothing): List<T> {
return if (elements.all { it != null }) {
elements.filterNotNull()
} else {
closure()
}
}

View File

@@ -1,62 +0,0 @@
package com.formbricks.formbrickssdk.helper
import androidx.annotation.Keep
import androidx.fragment.app.FragmentManager
/**
* Configuration options for the SDK
*
* Use the [Builder] to configure the options, then pass the result of [build] to the setup method.
*/
@Keep
class FormbricksConfig private constructor(
val appUrl: String,
val environmentId: String,
val userId: String?,
val attributes: Map<String,String>?,
val loggingEnabled: Boolean,
val fragmentManager: FragmentManager?
) {
class Builder(private val appUrl: String, private val environmentId: String) {
private var userId: String? = null
private var attributes: MutableMap<String,String> = mutableMapOf()
private var loggingEnabled = false
private var fragmentManager: FragmentManager? = null
fun setUserId(userId: String): Builder {
this.userId = userId
return this
}
fun setAttributes(attributes: MutableMap<String,String>): Builder {
this.attributes = attributes
return this
}
fun addAttribute(attribute: String, key: String): Builder {
this.attributes[key] = attribute
return this
}
fun setLoggingEnabled(loggingEnabled: Boolean): Builder {
this.loggingEnabled = loggingEnabled
return this
}
fun setFragmentManager(fragmentManager: FragmentManager): Builder {
this.fragmentManager = fragmentManager
return this
}
fun build(): FormbricksConfig {
return FormbricksConfig(
appUrl = appUrl,
environmentId = environmentId,
userId = userId,
attributes = attributes,
loggingEnabled = loggingEnabled,
fragmentManager = fragmentManager
)
}
}
}

View File

@@ -1,44 +0,0 @@
package com.formbricks.formbrickssdk.helper
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
fun mapToJsonElement(map: Map<String, Any?>): JsonElement {
return buildJsonObject {
map.forEach { (key, value) ->
when (value) {
is String -> put(key, value)
is Number -> put(key, value)
is Boolean -> put(key, value)
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
put(key, mapToJsonElement(value as Map<String, Any?>))
}
is List<*> -> {
put(key, JsonArray(value.map { elem -> mapToJsonElementItem(elem) }))
}
null -> put(key, JsonNull)
else -> throw IllegalArgumentException("Unsupported type: ${value::class}")
}
}
}
}
fun mapToJsonElementItem(value: Any?): JsonElement {
return when (value) {
is String -> JsonPrimitive(value)
is Number -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
mapToJsonElement(value as Map<String, Any?>)
}
is List<*> -> JsonArray(value.map { elem -> mapToJsonElementItem(elem) })
null -> JsonNull
else -> throw IllegalArgumentException("Unsupported type: ${value::class}")
}
}

View File

@@ -1,24 +0,0 @@
package com.formbricks.formbrickssdk.logger
import android.util.Log
import com.formbricks.formbrickssdk.Formbricks
object Logger {
fun d(message: String) {
if (Formbricks.loggingEnabled) {
Log.d("FormbricksSDK", message)
}
}
fun e(exception: RuntimeException) {
if (Formbricks.loggingEnabled) {
Log.e("FormbricksSDK", exception.localizedMessage, exception)
}
}
fun w(message: String? = "Warning", exception: RuntimeException? = null) {
if (Formbricks.loggingEnabled) {
Log.w("FormbricksSDK", message, exception)
}
}
}

View File

@@ -1,351 +0,0 @@
package com.formbricks.formbrickssdk.manager
import android.content.Context
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.api.FormbricksApi
import com.formbricks.formbrickssdk.extensions.expiresAt
import com.formbricks.formbrickssdk.extensions.guard
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.environment.Survey
import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.model.enums.SuccessType
import com.formbricks.formbrickssdk.model.user.Display
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.RuntimeException
import java.util.Date
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.TimeUnit
/**
* The SurveyManager is responsible for managing the surveys that are displayed to the user.
* Filtering surveys based on the user's segments, responses, and displays.
*/
object SurveyManager {
private const val REFRESH_STATE_ON_ERROR_TIMEOUT_IN_MINUTES = 10
private const val FORMBRICKS_PREFS = "formbricks_prefs"
private const val PREF_FORMBRICKS_DATA_HOLDER = "formbricksDataHolder"
internal val refreshTimer = Timer()
internal var displayTimer = Timer()
internal var hasApiError = false
internal var isShowingSurvey = false
private val prefManager by lazy { Formbricks.applicationContext.getSharedPreferences(FORMBRICKS_PREFS, Context.MODE_PRIVATE) }
internal var filteredSurveys: MutableList<Survey> = mutableListOf()
private var environmentDataHolderJson: String?
get() {
return prefManager.getString(PREF_FORMBRICKS_DATA_HOLDER, "")
}
set(value) {
if (null != value) {
prefManager.edit().putString(PREF_FORMBRICKS_DATA_HOLDER, value).apply()
} else {
prefManager.edit().remove(PREF_FORMBRICKS_DATA_HOLDER).apply()
}
}
private var backingEnvironmentDataHolder: EnvironmentDataHolder? = null
var environmentDataHolder: EnvironmentDataHolder?
get() {
if (null != backingEnvironmentDataHolder) {
return backingEnvironmentDataHolder
}
synchronized(this) {
backingEnvironmentDataHolder = environmentDataHolderJson?.let { json ->
try {
Gson().fromJson(json, EnvironmentDataHolder::class.java)
} catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Unable to retrieve environment data from the local storage."))
null
}
}
return backingEnvironmentDataHolder
}
}
set(value) {
synchronized(this) {
backingEnvironmentDataHolder = value
environmentDataHolderJson = Gson().toJson(value)
}
}
/**
* Fills up the [filteredSurveys] array
*/
fun filterSurveys() {
val surveys = environmentDataHolder?.data?.data?.surveys.guard { return }
val displays = UserManager.displays ?: listOf()
val responses = UserManager.responses ?: listOf()
val segments = UserManager.segments ?: listOf()
filteredSurveys = filterSurveysBasedOnDisplayType(surveys, displays, responses).toMutableList()
filteredSurveys = filterSurveysBasedOnRecontactDays(filteredSurveys, environmentDataHolder?.data?.data?.project?.recontactDays?.toInt()).toMutableList()
if (UserManager.userId != null) {
if (segments.isEmpty()) {
filteredSurveys = mutableListOf()
return
}
filteredSurveys = filterSurveysBasedOnSegments(filteredSurveys, segments).toMutableList()
}
}
/**
* Checks if the environment state needs to be refreshed based on its [expiresAt] property,
* and if so, refreshes it, starts the refresh timer, and filters the surveys.
*/
fun refreshEnvironmentIfNeeded(force: Boolean = false) {
if (!force) {
environmentDataHolder?.expiresAt()?.let {
if (it.after(Date())) {
Logger.d("Environment state is still valid until $it")
filterSurveys()
return
}
}
}
CoroutineScope(Dispatchers.IO).launch {
try {
environmentDataHolder = FormbricksApi.getEnvironmentState().getOrThrow()
startRefreshTimer(environmentDataHolder?.expiresAt())
filterSurveys()
hasApiError = false
Formbricks.callback?.onSuccess(SuccessType.GET_ENVIRONMENT_SUCCESS)
} catch (e: Exception) {
hasApiError = true
val error = SDKError.unableToRefreshEnvironment
Formbricks.callback?.onError(error)
Logger.e(error)
startErrorTimer()
}
}
}
/**
* Checks if there are any surveys to display, based in the track action, and if so, displays the first one.
* Handles the display percentage and the delay of the survey.
*/
fun track(action: String) {
val actionClasses = environmentDataHolder?.data?.data?.actionClasses ?: listOf()
val codeActionClasses = actionClasses.filter { it.type == "code" }
val actionClass = codeActionClasses.firstOrNull { it.key == action }
val firstSurveyWithActionClass = filteredSurveys.firstOrNull { survey ->
val triggers = survey.triggers ?: listOf()
triggers.firstOrNull { it.actionClass?.name.equals(actionClass?.name) } != null
}
if (firstSurveyWithActionClass == null) {
Formbricks.callback?.onError(SDKError.surveyNotFoundError)
return
}
val isMultiLangSurvey = (firstSurveyWithActionClass.languages?.size ?: 0) > 1
if(isMultiLangSurvey) {
val currentLanguage = Formbricks.language
val languageCode = getLanguageCode(firstSurveyWithActionClass, currentLanguage)
if (languageCode == null) {
val error = RuntimeException("Survey “${firstSurveyWithActionClass.name}” is not available in language “$currentLanguage”. Skipping.")
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
Formbricks.setLanguage(languageCode)
}
val shouldDisplay = shouldDisplayBasedOnPercentage(firstSurveyWithActionClass.displayPercentage)
if (shouldDisplay) {
firstSurveyWithActionClass.id.let {
isShowingSurvey = true
val timeout = firstSurveyWithActionClass.delay ?: 0.0
stopDisplayTimer()
displayTimer.schedule(object : TimerTask() {
override fun run() {
Formbricks.showSurvey(it)
}
}, Date(System.currentTimeMillis() + timeout.toLong() * 1000))
}
} else {
Formbricks.callback?.onError(SDKError.surveyNotDisplayedError)
}
}
private fun stopDisplayTimer() {
displayTimer.cancel()
displayTimer = Timer()
}
/**
* Posts a survey response to the Formbricks API.
*/
fun postResponse(surveyId: String?) {
val id = surveyId.guard {
val error = SDKError.missingSurveyId
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
UserManager.onResponse(id)
}
/**
* Creates a new display for the survey. It is called when the survey is displayed to the user.
*/
fun onNewDisplay(surveyId: String?) {
val id = surveyId.guard {
val error = SDKError.missingSurveyId
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
UserManager.onDisplay(id)
}
/**
* Starts a timer to refresh the environment state after the given timeout [expiresAt].
*/
private fun startRefreshTimer(expiresAt: Date?) {
val date = expiresAt.guard { return }
refreshTimer.schedule(object: TimerTask() {
override fun run() {
Logger.d("Refreshing environment state.")
refreshEnvironmentIfNeeded()
}
}, date)
}
/**
* When an error occurs, it starts a timer to refresh the environment state after the given timeout.
*/
private fun startErrorTimer() {
val targetDate = Date(System.currentTimeMillis() + 1000 * 60 * REFRESH_STATE_ON_ERROR_TIMEOUT_IN_MINUTES)
refreshTimer.schedule(object: TimerTask() {
override fun run() {
Logger.d("Refreshing environment state after an error")
refreshEnvironmentIfNeeded()
}
}, targetDate)
}
/**
* Filters the surveys based on the display type and limit.
*/
private fun filterSurveysBasedOnDisplayType(surveys: List<Survey>, displays: List<Display>, responses: List<String>): List<Survey> {
return surveys.filter { survey ->
when (survey.displayOption) {
"respondMultiple" -> true
"displayOnce" -> {
displays.none { it.surveyId == survey.id }
}
"displayMultiple" -> {
responses.none { it == survey.id }
}
"displaySome" -> {
survey.displayLimit?.let { limit ->
if (responses.any { it == survey.id }) {
return@filter false
}
displays.count { it.surveyId == survey.id } < limit
} ?: true
}
else -> {
val error = SDKError.invalidDisplayOption
Formbricks.callback?.onError(error)
Logger.e(error)
false
}
}
}
}
/**
* Filters the surveys based on the recontact days and the [UserManager.lastDisplayedAt] date.
*/
private fun filterSurveysBasedOnRecontactDays(surveys: List<Survey>, defaultRecontactDays: Int?): List<Survey> {
return surveys.filter { survey ->
val lastDisplayedAt = UserManager.lastDisplayedAt.guard { return@filter true }
val recontactDays = survey.recontactDays ?: defaultRecontactDays
if (recontactDays != null) {
val daysBetween = TimeUnit.MILLISECONDS.toDays(Date().time - lastDisplayedAt.time)
return@filter daysBetween >= recontactDays.toInt()
}
true
}
}
/**
* Filters the surveys based on the user's segments.
*/
private fun filterSurveysBasedOnSegments(surveys: List<Survey>, segments: List<String>): List<Survey> {
return surveys.filter { survey ->
val segmentId = survey.segment?.id?.guard { return@filter false }
segments.contains(segmentId)
}
}
/**
* Decides if the survey should be displayed based on the display percentage.
*/
private fun shouldDisplayBasedOnPercentage(displayPercentage: Double?): Boolean {
val percentage = displayPercentage.guard { return true }
val randomNum = (0 until 10000).random() / 100.0
return randomNum <= percentage
}
private fun getLanguageCode(survey: Survey, language: String?): String? {
// 1) Gather all valid codes
val availableLanguageCodes = survey.languages
?.map { it.language.code }
?: emptyList()
// 2) No input or explicit "default" → default
val raw = language
?.lowercase()
?.takeIf { it.isNotEmpty() }
?: return "default"
if (raw == "default") return "default"
// 3) Find matching entry by code or alias
val selected = survey.languages
?.firstOrNull { entry ->
entry.language.code.lowercase() == raw ||
entry.language.alias?.lowercase() == raw
}
// 4) If that entry is marked default → default
if (selected?.default == true) return "default"
// 5) If missing, disabled, or not in the available list → null
if (selected == null
|| !selected.enabled
|| !availableLanguageCodes.contains(selected.language.code)
) {
return null
}
// 6) Otherwise return its code
return selected.language.code
}
}

View File

@@ -1,265 +0,0 @@
package com.formbricks.formbrickssdk.manager
import android.content.Context
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.api.FormbricksApi
import com.formbricks.formbrickssdk.extensions.dateString
import com.formbricks.formbrickssdk.extensions.expiresAt
import com.formbricks.formbrickssdk.extensions.guard
import com.formbricks.formbrickssdk.extensions.lastDisplayAt
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.model.enums.SuccessType
import com.formbricks.formbrickssdk.model.user.Display
import com.formbricks.formbrickssdk.network.queue.UpdateQueue
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Date
import java.util.Timer
import java.util.TimerTask
/**
* Store and manage user state and sync with the server when needed.
*/
object UserManager {
private const val FORMBROCKS_PERFS = "formbricks_prefs"
private const val USER_ID_KEY = "userIdKey"
private const val CONTACT_ID_KEY = "contactIdKey"
private const val SEGMENTS_KEY = "segmentsKey"
private const val DISPLAYS_KEY = "displaysKey"
private const val RESPONSES_KEY = "responsesKey"
private const val LAST_DISPLAYED_AT_KEY = "lastDisplayedAtKey"
private const val EXPIRES_AT_KEY = "expiresAtKey"
private val prefManager by lazy { Formbricks.applicationContext.getSharedPreferences(FORMBROCKS_PERFS, Context.MODE_PRIVATE) }
private var backingUserId: String? = null
private var backingContactId: String? = null
private var backingSegments: List<String>? = null
private var backingDisplays: List<Display>? = null
private var backingResponses: List<String>? = null
private var backingLastDisplayedAt: Date? = null
private var backingExpiresAt: Date? = null
internal val syncTimer = Timer()
/**
* Starts an update queue with the given user id.
*
* @param userId
*/
fun set(userId: String) {
UpdateQueue.current.setUserId(userId)
}
/**
* Starts an update queue with the given attribute.
*
* @param attribute
* @param key
*/
fun addAttribute(attribute: String, key: String) {
UpdateQueue.current.addAttribute(key, attribute)
}
/**
* Starts an update queue with the given attributes.
*
* @param attributes
*/
fun setAttributes(attributes: Map<String, String>) {
UpdateQueue.current.setAttributes(attributes)
}
/**
* Starts an update queue with the given language..
*
* @param language
*/
fun setLanguage(language: String) {
UpdateQueue.current.setLanguage(language)
}
/**
* Saves [surveyId] to the [displays] property and the the current date to the [lastDisplayedAt] property.
*
* @param surveyId
*/
fun onDisplay(surveyId: String) {
val lastDisplayedAt = Date()
val newDisplays = displays?.toMutableList() ?: mutableListOf()
newDisplays.add(Display(surveyId, lastDisplayedAt.dateString()))
displays = newDisplays
this.lastDisplayedAt = lastDisplayedAt
SurveyManager.filterSurveys()
}
/**
* Saves [surveyId] to the [responses] property.
*
* @param surveyId
*/
fun onResponse(surveyId: String) {
val newResponses = responses?.toMutableList() ?: mutableListOf()
newResponses.add(surveyId)
responses = newResponses
SurveyManager.filterSurveys()
}
/**
* Syncs the user state with the server if the user id is set and the expiration date has passed.
*/
fun syncUserStateIfNeeded() {
val id = userId
val expiresAt = expiresAt
if (id != null && expiresAt != null && Date().before(expiresAt)) {
syncUser(id)
} else {
backingSegments = emptyList()
backingDisplays = emptyList()
backingResponses = emptyList()
}
}
/**
* Syncs the user state with the server, calls the [SurveyManager.filterSurveys] method and starts the sync timer.
*
* @param id
* @param attributes
*/
fun syncUser(id: String, attributes: Map<String, String>? = null) {
CoroutineScope(Dispatchers.IO).launch {
try {
val userResponse = FormbricksApi.postUser(id, attributes).getOrThrow()
userId = userResponse.data.state.data.userId
contactId = userResponse.data.state.data.contactId
segments = userResponse.data.state.data.segments
displays = userResponse.data.state.data.displays
responses = userResponse.data.state.data.responses
lastDisplayedAt = userResponse.data.state.data.lastDisplayAt()
expiresAt = userResponse.data.state.expiresAt()
val languageFromUserResponse = userResponse.data.state.data.language
if(languageFromUserResponse != null) {
Formbricks.language = languageFromUserResponse
}
UpdateQueue.current.reset()
SurveyManager.filterSurveys()
startSyncTimer()
Formbricks.callback?.onSuccess(SuccessType.SET_USER_SUCCESS)
} catch (e: Exception) {
val error = SDKError.unableToPostResponse
Formbricks.callback?.onError(error)
Logger.e(error)
}
}
}
/**
* Logs out the user and clears the user state.
*/
fun logout() {
val isUserIdDefined = userId != null
if (!isUserIdDefined) {
val error = SDKError.noUserIdSetError
Formbricks.callback?.onError(error)
Logger.e(error)
}
prefManager.edit().apply {
remove(CONTACT_ID_KEY)
remove(USER_ID_KEY)
remove(SEGMENTS_KEY)
remove(DISPLAYS_KEY)
remove(RESPONSES_KEY)
remove(LAST_DISPLAYED_AT_KEY)
remove(EXPIRES_AT_KEY)
apply()
}
backingUserId = null
backingContactId = null
backingSegments = null
backingDisplays = null
backingResponses = null
backingLastDisplayedAt = null
backingExpiresAt = null
Formbricks.language = "default"
UpdateQueue.current.reset()
if(isUserIdDefined) {
Logger.d("User logged out successfully!")
}
}
private fun startSyncTimer() {
val expiresAt = expiresAt.guard { return }
val userId = userId.guard { return }
syncTimer.schedule(object: TimerTask() {
override fun run() {
syncUser(userId)
}
}, expiresAt)
}
var userId: String?
get() = backingUserId ?: prefManager.getString(USER_ID_KEY, null).also { backingUserId = it }
private set(value) {
backingUserId = value
prefManager.edit().putString(USER_ID_KEY, value).apply()
}
var contactId: String?
get() = backingContactId ?: prefManager.getString(CONTACT_ID_KEY, null).also { backingContactId = it }
private set(value) {
backingContactId = value
prefManager.edit().putString(CONTACT_ID_KEY, value).apply()
}
var segments: List<String>?
get() = backingSegments ?: prefManager.getStringSet(SEGMENTS_KEY, emptySet())?.toList().also { backingSegments = it }
private set(value) {
backingSegments = value
prefManager.edit().putStringSet(SEGMENTS_KEY, value?.toSet()).apply()
}
var displays: List<Display>?
get() {
if (backingDisplays == null) {
val json = prefManager.getString(DISPLAYS_KEY, null)
if (json != null) {
backingDisplays = Gson().fromJson(json, Array<Display>::class.java).toList()
}
}
return backingDisplays
}
private set(value) {
backingDisplays = value
prefManager.edit().putString(DISPLAYS_KEY, Gson().toJson(value)).apply()
}
var responses: List<String>?
get() = backingResponses ?: prefManager.getStringSet(RESPONSES_KEY, emptySet())?.toList().also { backingResponses = it }
private set(value) {
backingResponses = value
prefManager.edit().putStringSet(RESPONSES_KEY, value?.toSet()).apply()
}
var lastDisplayedAt: Date?
get() = backingLastDisplayedAt ?: prefManager.getLong(LAST_DISPLAYED_AT_KEY, 0L).takeIf { it > 0 }?.let { Date(it) }.also { backingLastDisplayedAt = it }
private set(value) {
backingLastDisplayedAt = value
prefManager.edit().putLong(LAST_DISPLAYED_AT_KEY, value?.time ?: 0L).apply()
}
var expiresAt: Date?
get() = backingExpiresAt ?: prefManager.getLong(EXPIRES_AT_KEY, 0L).takeIf { it > 0 }?.let { Date(it) }.also { backingExpiresAt = it }
private set(value) {
backingExpiresAt = value
prefManager.edit().putLong(EXPIRES_AT_KEY, value?.time ?: 0L).apply()
}
}

View File

@@ -1,3 +0,0 @@
package com.formbricks.formbrickssdk.model
interface BaseFormbricksResponse

View File

@@ -1,7 +0,0 @@
package com.formbricks.formbrickssdk.model.enums
enum class SuccessType {
SET_USER_SUCCESS,
GET_ENVIRONMENT_SUCCESS,
LOGOUT_SUCCESS
}

View File

@@ -1,12 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class ActionClass(
@SerializedName("id") val id: String?,
@SerializedName("type") val type: String?,
@SerializedName("name") val name: String?,
@SerializedName("key") val key: String?,
)

View File

@@ -1,9 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class ActionClassReference(
@SerializedName("name") val name: String?
)

View File

@@ -1,9 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class BrandColor(
@SerializedName("light") val light: String?
)

View File

@@ -1,11 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class EnvironmentData(
@SerializedName("surveys") val surveys: List<Survey>?,
@SerializedName("actionClasses") val actionClasses: List<ActionClass>?,
@SerializedName("project") val project: Project
)

View File

@@ -1,48 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.Gson
import com.google.gson.JsonElement
data class EnvironmentDataHolder(
val data: EnvironmentResponseData?,
val originalResponseMap: Map<String, Any>
)
@Suppress("UNCHECKED_CAST")
fun EnvironmentDataHolder.getSurveyJson(surveyId: String): JsonElement? {
val responseMap = originalResponseMap["data"] as? Map<*, *>
val dataMap = responseMap?.get("data") as? Map<*, *>
val surveyArray = dataMap?.get("surveys") as? ArrayList<Map<String, Any?>>
val firstSurvey = surveyArray?.firstOrNull { it["id"] == surveyId }
firstSurvey?.let {
return Gson().toJsonTree(it)
}
return null
}
@Suppress("UNCHECKED_CAST")
fun EnvironmentDataHolder.getStyling(surveyId: String): JsonElement? {
val responseMap = originalResponseMap["data"] as? Map<*, *>
val dataMap = responseMap?.get("data") as? Map<*, *>
val surveyArray = dataMap?.get("surveys") as? ArrayList<Map<String, Any?>>
val firstSurvey = surveyArray?.firstOrNull { it["id"] == surveyId }
firstSurvey?.get("styling")?.let {
return Gson().toJsonTree(it)
}
return null
}
@Suppress("UNCHECKED_CAST")
fun EnvironmentDataHolder.getProjectStylingJson(): JsonElement? {
val responseMap = originalResponseMap["data"] as? Map<*, *>
val dataMap = responseMap?.get("data") as? Map<*, *>
val projectMap = dataMap?.get("project") as? Map<*, *>
val stylingMap = projectMap?.get("styling") as? Map<String, Any?>
stylingMap?.let {
return Gson().toJsonTree(it)
}
return null
}

View File

@@ -1,10 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.formbricks.formbrickssdk.model.BaseFormbricksResponse
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class EnvironmentResponse(
@SerializedName("data") val data: EnvironmentResponseData,
): BaseFormbricksResponse

View File

@@ -1,10 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class EnvironmentResponseData(
@SerializedName("data") val data: EnvironmentData,
@SerializedName("expiresAt") val expiresAt: String?
)

View File

@@ -1,15 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class Project(
@SerializedName("id") val id: String?,
@SerializedName("recontactDays") val recontactDays: Double?,
@SerializedName("clickOutsideClose") val clickOutsideClose: Boolean?,
@SerializedName("darkOverlay") val darkOverlay: Boolean?,
@SerializedName("placement") val placement: String?,
@SerializedName("inAppSurveyBranding") val inAppSurveyBranding: Boolean?,
@SerializedName("styling") val styling: Styling?
)

View File

@@ -1,17 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class Segment(
@SerializedName("id") val id: String? = null,
@SerializedName("createdAt") val createdAt: String? = null,
@SerializedName("updatedAt") val updatedAt: String? = null,
@SerializedName("title") val title: String? = null,
@SerializedName("description") val description: String? = null,
@SerializedName("isPrivate") val isPrivate: Boolean? = null,
@SerializedName("filters") val filters: List<String>? = null,
@SerializedName("environmentId") val environmentId: String? = null,
@SerializedName("surveys") val surveys: List<String>? = null
)

View File

@@ -1,10 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class Styling(
@SerializedName("roundness") val roundness: Double? = null,
@SerializedName("allowStyleOverwrite") val allowStyleOverwrite: Boolean? = null,
)

View File

@@ -1,34 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class SurveyLanguage(
@SerializedName("enabled") val enabled: Boolean,
@SerializedName("default") val default: Boolean,
@SerializedName("language") val language: LanguageDetail
)
@Serializable
data class LanguageDetail(
@SerializedName("id") val id: String,
@SerializedName("code") val code: String,
@SerializedName("alias") val alias: String?,
@SerializedName("projectId") val projectId: String
)
@Serializable
data class Survey(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("triggers") val triggers: List<Trigger>?,
@SerializedName("recontactDays") val recontactDays: Double?,
@SerializedName("displayLimit") val displayLimit: Double?,
@SerializedName("delay") val delay: Double?,
@SerializedName("displayPercentage") val displayPercentage: Double?,
@SerializedName("displayOption") val displayOption: String?,
@SerializedName("segment") val segment: Segment?,
@SerializedName("styling") val styling: Styling?,
@SerializedName("languages") val languages: List<SurveyLanguage>?
)

View File

@@ -1,9 +0,0 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class Trigger(
@SerializedName("actionClass") val actionClass: ActionClassReference?
)

View File

@@ -1,27 +0,0 @@
package com.formbricks.formbrickssdk.model.error
import androidx.annotation.Keep
@Keep
object SDKError {
// Errors related to SDK initialization and configuration
val sdkIsNotInitialized = RuntimeException("Formbricks SDK is not initialized")
val sdkIsAlreadyInitialized = RuntimeException("Formbricks SDK is already initialized")
val fragmentManagerIsNotSet = RuntimeException("The fragment manager is not set.")
// Errors related to network and connectivity
val connectionIsNotAvailable = RuntimeException("There is no connection.")
val unableToLoadFormbicksJs = RuntimeException("Unable to load Formbricks Javascript package.")
// Errors related to surveys
val surveyDisplayFetchError =
RuntimeException("Error: creating display: TypeError: Failure to fetch the survey data.")
val surveyNotDisplayedError = RuntimeException("Survey was not displayed due to display percentage restrictions.")
val unableToRefreshEnvironment = RuntimeException("Unable to refresh environment state.")
val missingSurveyId = RuntimeException("Survey id is mandatory to set.")
val invalidDisplayOption = RuntimeException("Invalid Display Option.")
val unableToPostResponse = RuntimeException("Unable to post survey response.")
val surveyNotFoundError = RuntimeException("No survey found matching the action class.")
val noUserIdSetError = RuntimeException("No userId is set, please set a userId first using the setUserId function")
}

View File

@@ -1,11 +0,0 @@
package com.formbricks.formbrickssdk.model.javascript
import com.google.gson.annotations.SerializedName
enum class EventType {
@SerializedName("onClose") ON_CLOSE,
@SerializedName("onDisplayCreated") ON_DISPLAY_CREATED,
@SerializedName("onResponseCreated") ON_RESPONSE_CREATED,
@SerializedName("onFilePick") ON_FILE_PICK,
@SerializedName("onSurveyLibraryLoadError") ON_SURVEY_LIBRARY_LOAD_ERROR
}

View File

@@ -1,25 +0,0 @@
package com.formbricks.formbrickssdk.model.javascript
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
data class FileUploadData(
@SerializedName("event") val event: EventType,
@SerializedName("fileUploadParams") val fileUploadParams: FileUploadParams,
) {
companion object {
fun from(string: String): FileUploadData {
return Gson().fromJson(string, FileUploadData::class.java)
}
}
}
data class FileUploadParams(
@SerializedName("allowedFileExtensions") val allowedFileExtensions: String?,
@SerializedName("allowMultipleFiles") val allowMultipleFiles: Boolean
) {
fun allowedExtensionsArray(): Array<String> {
return allowedFileExtensions?.split(",")?.map { it }?.toTypedArray() ?: arrayOf()
}
}

View File

@@ -1,18 +0,0 @@
package com.formbricks.formbrickssdk.model.javascript
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
data class JsMessageData(
@SerializedName("event") val event: EventType,
) {
companion object {
fun from(string: String): JsMessageData {
return try {
Gson().fromJson(string, JsMessageData::class.java)
} catch (e: Exception) {
throw IllegalArgumentException("Invalid JSON format: ${e.message}", e)
}
}
}
}

View File

@@ -1,17 +0,0 @@
package com.formbricks.formbrickssdk.model.upload
import com.google.gson.annotations.SerializedName
data class FetchStorageUrlRequestBody (
@SerializedName("fileName") val fileName: String,
@SerializedName("fileType") val fileType: String,
@SerializedName("allowedFileExtensions") val allowedFileExtensions: List<String>?,
@SerializedName("surveyId") val surveyId: String,
@SerializedName("accessType") val accessType: String,
) {
companion object {
fun create(fileName: String, fileType: String, allowedFileExtensions: List<String>?, surveyId: String, accessType: String = "public"): FetchStorageUrlRequestBody {
return FetchStorageUrlRequestBody(fileName, fileType, allowedFileExtensions, surveyId, accessType)
}
}
}

View File

@@ -1,7 +0,0 @@
package com.formbricks.formbrickssdk.model.upload
import com.google.gson.annotations.SerializedName
data class FetchStorageUrlResponse(
@SerializedName("data") val data: StorageData
)

View File

@@ -1,28 +0,0 @@
//package com.formbricks.formbrickssdk.model.upload
//
//import com.formbricks.formbrickssdk.model.javascript.FileData
//import com.google.gson.annotations.SerializedName
//
//data class FileUploadBody(
// @SerializedName("fileName") val fileName: String,
// @SerializedName("fileType") val fileType: String,
// @SerializedName("surveyId") val surveyId: String?,
// @SerializedName("signature") val signature: String,
// @SerializedName("timestamp") val timestamp: String,
// @SerializedName("uuid") val uuid: String,
// @SerializedName("fileBase64String") val fileBase64String: String,
//) {
// companion object {
// fun create(file: FileData, storageData: StorageData, surveyId: String?): FileUploadBody {
// return FileUploadBody(
// fileName = storageData.updatedFileName,
// fileType = file.type,
// surveyId = surveyId,
// signature = storageData.signingData.signature,
// uuid = storageData.signingData.uuid,
// timestamp = storageData.signingData.timestamp.toString(),
// fileBase64String = file.base64
// )
// }
// }
//}

View File

@@ -1,9 +0,0 @@
package com.formbricks.formbrickssdk.model.upload
import com.google.gson.annotations.SerializedName
data class SigningData(
@SerializedName("signature") val signature: String,
@SerializedName("timestamp") val timestamp: Long,
@SerializedName("uuid") val uuid: String
)

View File

@@ -1,10 +0,0 @@
package com.formbricks.formbrickssdk.model.upload
import com.google.gson.annotations.SerializedName
data class StorageData(
@SerializedName("signedUrl") val signedUrl: String,
@SerializedName("signingData") val signingData: SigningData,
@SerializedName("updatedFileName") val updatedFileName: String,
@SerializedName("fileUrl") val fileUrl: String
)

View File

@@ -1,8 +0,0 @@
package com.formbricks.formbrickssdk.model.user
import com.google.gson.annotations.SerializedName
data class Display(
@SerializedName("surveyId") val surveyId: String,
@SerializedName("createdAt") val createdAt: String
)

View File

@@ -1,14 +0,0 @@
package com.formbricks.formbrickssdk.model.user
import com.google.gson.annotations.SerializedName
data class PostUserBody(
@SerializedName("userId") val userId: String,
@SerializedName("attributes") val attributes: Map<String, *>?
) {
companion object {
fun create(userId: String, attributes: Map<String, *>?): PostUserBody {
return PostUserBody(userId, attributes)
}
}
}

View File

@@ -1,7 +0,0 @@
package com.formbricks.formbrickssdk.model.user
import com.google.gson.annotations.SerializedName
data class UserResponse(
@SerializedName("data") val data: UserResponseData
)

View File

@@ -1,7 +0,0 @@
package com.formbricks.formbrickssdk.model.user
import com.google.gson.annotations.SerializedName
data class UserResponseData(
@SerializedName("state") val state: UserState
)

View File

@@ -1,8 +0,0 @@
package com.formbricks.formbrickssdk.model.user
import com.google.gson.annotations.SerializedName
data class UserState(
@SerializedName("data") val data: UserStateData,
@SerializedName("expiresAt") val expiresAt: String?
)

View File

@@ -1,13 +0,0 @@
package com.formbricks.formbrickssdk.model.user
import com.google.gson.annotations.SerializedName
data class UserStateData(
@SerializedName("userId") val userId: String?,
@SerializedName("contactId") val contactId: String?,
@SerializedName("segments") val segments: List<String>?,
@SerializedName("displays") val displays: List<Display>?,
@SerializedName("responses") val responses: List<String>?,
@SerializedName("lastDisplayAt") val lastDisplayAt: String?,
@SerializedName("language") val language: String?
)

View File

@@ -1,66 +0,0 @@
package com.formbricks.formbrickssdk.network
import com.formbricks.formbrickssdk.api.error.FormbricksAPIError
import com.formbricks.formbrickssdk.helper.mapToJsonElement
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.environment.EnvironmentResponse
import com.formbricks.formbrickssdk.model.user.PostUserBody
import com.formbricks.formbrickssdk.model.user.UserResponse
import com.google.gson.Gson
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject
import retrofit2.Call
import retrofit2.Retrofit
open class FormbricksApiService {
private lateinit var retrofit: Retrofit
fun initialize(appUrl: String, isLoggingEnabled: Boolean) {
retrofit = FormbricksRetrofitBuilder(appUrl, isLoggingEnabled)
.getBuilder()
.build()
}
open fun getEnvironmentStateObject(environmentId: String): Result<EnvironmentDataHolder> {
val result = execute {
retrofit.create(FormbricksService::class.java)
.getEnvironmentState(environmentId)
}
val json = Json { ignoreUnknownKeys = true }
val resultMap = result.getOrThrow()
val resultJson = mapToJsonElement(resultMap).jsonObject
val environmentResponse = json.decodeFromJsonElement<EnvironmentResponse>(resultJson)
val data = EnvironmentDataHolder(environmentResponse.data, resultMap)
return Result.success(data)
}
open fun postUser(environmentId: String, body: PostUserBody): Result<UserResponse> {
return execute {
retrofit.create(FormbricksService::class.java)
.postUser(environmentId, body)
}
}
private inline fun <T> execute(apiCall: () -> Call<T>): Result<T> {
val call = apiCall().execute()
return if (call.isSuccessful) {
val body = call.body()
if (body == null) {
Result.failure(RuntimeException("Invalid response"))
} else {
Result.success(body)
}
} else {
return try {
val errorResponse =
Gson().fromJson(call.errorBody()?.string(), FormbricksAPIError::class.java)
Result.failure(errorResponse)
} catch (e: Exception) {
Result.failure(e)
}
}
}
}

View File

@@ -1,41 +0,0 @@
//package com.formbricks.formbrickssdk.network
//
//import com.formbricks.formbrickssdk.api.error.FormbricksAPIError
//import com.formbricks.formbrickssdk.model.upload.FileUploadBody
//import com.google.gson.Gson
//import retrofit2.Call
//import retrofit2.Retrofit
//
//class FormbricksFileUploadService(appUrl: String, isLoggingEnabled: Boolean) {
// private var retrofit: Retrofit = FormbricksRetrofitBuilder(appUrl, isLoggingEnabled)
// .getBuilder()
// .build()
//
//
// fun uploadFile(path: String, body: FileUploadBody): Result<Map<String, *>> {
// return execute {
// retrofit.create(FormbricksService::class.java)
// .uploadFile(path, body)
// }
// }
//
// private inline fun <T> execute(apiCall: () -> Call<T>): Result<T> {
// val call = apiCall().execute()
// return if (call.isSuccessful) {
// val body = call.body()
// if (body == null) {
// Result.failure(RuntimeException("Invalid response"))
// } else {
// Result.success(body)
// }
// } else {
// return try {
// val errorResponse =
// Gson().fromJson(call.errorBody()?.string(), FormbricksAPIError::class.java)
// Result.failure(errorResponse)
// } catch (e: Exception) {
// Result.failure(e)
// }
// }
// }
//}

View File

@@ -1,32 +0,0 @@
package com.formbricks.formbrickssdk.network
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
class FormbricksRetrofitBuilder(private val baseUrl: String, private val loggingEnabled: Boolean) {
fun getBuilder(): Retrofit.Builder {
val clientBuilder = OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
.readTimeout(READ_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
.followSslRedirects(true)
if (loggingEnabled) {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
clientBuilder.addInterceptor(logging)
}
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(clientBuilder.build())
}
companion object {
private const val CONNECT_TIMEOUT_MS = 30 * 1000 // 30 seconds
private const val READ_TIMEOUT_MS = 30 * 1000 // 30 seconds
}
}

View File

@@ -1,21 +0,0 @@
package com.formbricks.formbrickssdk.network
import com.formbricks.formbrickssdk.model.user.PostUserBody
import com.formbricks.formbrickssdk.model.user.UserResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
interface FormbricksService {
@GET("$API_PREFIX/client/{environmentId}/environment")
fun getEnvironmentState(@Path("environmentId") environmentId: String): Call<Map<String, Any>>
@POST("$API_PREFIX/client/{environmentId}/user")
fun postUser(@Path("environmentId") environmentId: String, @Body body: PostUserBody): Call<UserResponse>
companion object {
const val API_PREFIX = "/api/v2"
}
}

View File

@@ -1,84 +0,0 @@
package com.formbricks.formbrickssdk.network.queue
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.manager.UserManager
import com.formbricks.formbrickssdk.model.error.SDKError
import java.util.*
import kotlin.concurrent.timer
/**
* Update queue. This class is used to queue updates to the user.
* The given properties will be sent to the backend and updated in
* the user object when the debounce interval is reached.
*/
class UpdateQueue private constructor() {
private var userId: String? = null
private var attributes: MutableMap<String, String>? = null
private var language: String? = null
private var timer: Timer? = null
fun setUserId(userId: String) {
this.userId = userId
startDebounceTimer()
}
fun setAttributes(attributes: Map<String, String>) {
this.attributes = attributes.toMutableMap()
startDebounceTimer()
}
fun addAttribute(key: String, attribute: String) {
if (attributes == null) {
attributes = mutableMapOf()
}
attributes?.put(key, attribute)
startDebounceTimer()
}
fun setLanguage(language: String) {
val effectiveUserId = userId ?: UserManager.userId
if(effectiveUserId != null) {
addAttribute("language", language)
startDebounceTimer()
} else {
Logger.d("UpdateQueue - updating language locally: ${language}")
return
}
}
fun reset() {
userId = null
attributes = null
language = null
}
private fun startDebounceTimer() {
timer?.cancel()
timer = timer("debounceTimer", false, DEBOUNCE_INTERVAL, DEBOUNCE_INTERVAL) {
commit()
timer?.cancel()
}
}
private fun commit() {
val effectiveUserId = userId
?: UserManager.userId
if (effectiveUserId == null) {
val error = SDKError.noUserIdSetError
Formbricks.callback?.onError(error)
Logger.e(error)
return
}
Logger.d("UpdateQueue - commit() called on UpdateQueue with $effectiveUserId and $attributes")
UserManager.syncUser(effectiveUserId, attributes)
}
companion object {
private const val DEBOUNCE_INTERVAL: Long = 500 // 500 ms
val current: UpdateQueue = UpdateQueue()
}
}

View File

@@ -1,248 +0,0 @@
package com.formbricks.formbrickssdk.webview
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.app.Dialog
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.OpenableColumns
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.R
import com.formbricks.formbrickssdk.databinding.FragmentFormbricksBinding
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.manager.SurveyManager
import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.model.javascript.FileUploadData
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.gson.JsonObject
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.Timer
class FormbricksFragment : BottomSheetDialogFragment() {
private lateinit var binding: FragmentFormbricksBinding
private lateinit var surveyId: String
private val closeTimer = Timer()
private val viewModel: FormbricksViewModel by viewModels()
private var webAppInterface = WebAppInterface(object : WebAppInterface.WebAppCallback {
override fun onClose() {
Handler(Looper.getMainLooper()).post {
Formbricks.callback?.onSurveyClosed()
dismiss()
}
}
override fun onDisplayCreated() {
Formbricks.callback?.onSurveyStarted()
SurveyManager.onNewDisplay(surveyId)
}
override fun onResponseCreated() {
SurveyManager.postResponse(surveyId)
}
override fun onFilePick(data: FileUploadData) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.setType("*/*")
.putExtra(Intent.EXTRA_MIME_TYPES, data.fileUploadParams.allowedExtensionsArray())
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, data.fileUploadParams.allowMultipleFiles)
resultLauncher.launch(intent)
}
override fun onSurveyLibraryLoadError() {
Formbricks.callback?.onError(SDKError.unableToLoadFormbicksJs)
dismiss()
}
})
var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val intent: Intent? = result.data
var uriArray: MutableList<Uri> = mutableListOf()
val dataString = intent?.dataString
if (null != dataString) {
uriArray = arrayOf(Uri.parse(dataString)).toMutableList()
} else {
val clipData = intent?.clipData
if (null != clipData) {
for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri
uriArray.add(uri)
}
}
}
val jsonArray = com.google.gson.JsonArray()
uriArray.forEach { uri ->
val type = activity?.contentResolver?.getType(uri)
val fileName = getFileName(uri)
val base64 = "data:${type};base64,${uriToBase64(uri)}"
val json = JsonObject()
json.addProperty("name", fileName)
json.addProperty("type", type)
json.addProperty("base64", base64)
jsonArray.add(json)
}
binding.formbricksWebview.evaluateJavascript("""window.formbricksSurveys.onFilePick($jsonArray)""") { result ->
print(result)
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentFormbricksBinding.inflate(inflater).apply {
lifecycleOwner = viewLifecycleOwner
}
binding.viewModel = viewModel
return binding.root
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
setStyle(STYLE_NO_FRAME, R.style.BottomSheetDialog)
return super.onCreateDialog(savedInstanceState)
}
@Suppress("DEPRECATION")
override fun onStart() {
super.onStart()
val view: FrameLayout = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)!!
view.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
val behavior = BottomSheetBehavior.from(view)
behavior.peekHeight = resources.displayMetrics.heightPixels
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.isFitToContents = false
behavior.setState(BottomSheetBehavior.STATE_EXPANDED)
dialog?.setCancelable(false)
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
}
@Suppress("DEPRECATION")
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dialog?.window?.setDimAmount(0.0f)
binding.formbricksWebview.setBackgroundColor(Color.TRANSPARENT)
binding.formbricksWebview.let {
it.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
consoleMessage?.let { cm ->
if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
Formbricks.callback?.onError(SDKError.surveyDisplayFetchError)
dismiss()
}
val log = "[CONSOLE:${cm.messageLevel()}] \"${cm.message()}\", source: ${cm.sourceId()} (${cm.lineNumber()})"
Logger.d(log)
}
return super.onConsoleMessage(consoleMessage)
}
}
it.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
}
it.webViewClient = object : WebViewClient() {
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
Logger.d("WebView Error: ${error?.description}")
}
override fun onPageCommitVisible(view: WebView?, url: String?) {
dialog?.window?.setDimAmount(0.5f)
super.onPageCommitVisible(view, url)
}
}
it.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
}
}
it.setInitialScale(1)
it.addJavascriptInterface(webAppInterface, WebAppInterface.INTERFACE_NAME)
}
viewModel.loadHtml(surveyId)
}
private fun getFileName(uri: Uri): String? {
var fileName: String? = null
activity?.contentResolver?.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1 && cursor.moveToFirst()) {
fileName = cursor.getString(nameIndex)
}
}
return fileName
}
private fun uriToBase64(uri: Uri): String? {
return try {
val inputStream: InputStream? = activity?.contentResolver?.openInputStream(uri)
val outputStream = ByteArrayOutputStream()
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
inputStream?.close()
outputStream.close()
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
companion object {
private val TAG: String by lazy { FormbricksFragment::class.java.simpleName }
fun show(childFragmentManager: FragmentManager, surveyId: String) {
val fragment = FormbricksFragment()
fragment.surveyId = surveyId
fragment.show(childFragmentManager, TAG)
}
private const val CLOSING_TIMEOUT_IN_SECONDS = 5L
}
}

View File

@@ -1,159 +0,0 @@
package com.formbricks.formbrickssdk.webview
import android.webkit.WebView
import androidx.databinding.BindingAdapter
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.extensions.guard
import com.formbricks.formbrickssdk.manager.SurveyManager
import com.formbricks.formbrickssdk.manager.UserManager
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.environment.getProjectStylingJson
import com.formbricks.formbrickssdk.model.environment.getStyling
import com.formbricks.formbrickssdk.model.environment.getSurveyJson
import com.google.gson.JsonObject
/**
* A view model for the Formbricks WebView.
* It generates the HTML string with the necessary data to render the survey.
*/
class FormbricksViewModel : ViewModel() {
var html = MutableLiveData<String>()
/**
* The HTML template to render the Formbricks WebView.
*/
private val htmlTemplate = """
<!doctype html>
<html>
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0">
<head>
<title>Formbricks WebView Survey</title>
</head>
<body style="overflow: hidden; height: 100vh; display: flex; flex-direction: column; justify-content: flex-end;">
<div id="formbricks-react-native" style="width: 100%;"></div>
</body>
<script type="text/javascript">
var json = `{{WEBVIEW_DATA}}`
function onClose() {
FormbricksJavascript.message(JSON.stringify({ event: "onClose" }));
};
function onDisplayCreated() {
FormbricksJavascript.message(JSON.stringify({ event: "onDisplayCreated" }));
};
function onResponseCreated() {
FormbricksJavascript.message(JSON.stringify({ event: "onResponseCreated" }));
};
function loadSurvey() {
const options = JSON.parse(json);
const surveyProps = {
...options,
onDisplayCreated,
onResponseCreated,
onClose,
};
window.formbricksSurveys.renderSurvey(surveyProps);
}
// Function to attach click listener to file inputs
function attachFilePickerOverride() {
const inputs = document.querySelectorAll('input[type="file"]');
inputs.forEach(input => {
if (!input.getAttribute('data-file-picker-overridden')) {
input.setAttribute('data-file-picker-overridden', 'true');
const allowedFileExtensions = input.getAttribute('data-accept-extensions');
const allowMultipleFiles = input.getAttribute('data-accept-multiple');
input.addEventListener('click', function (e) {
e.preventDefault();
FormbricksJavascript.message(JSON.stringify({
event: "onFilePick",
fileUploadParams: {
allowedFileExtensions: allowedFileExtensions,
allowMultipleFiles: allowMultipleFiles === "true",
},
}));
});
}
});
}
// Initially attach the override
attachFilePickerOverride();
// Set up a MutationObserver to catch dynamically added file inputs
const observer = new MutationObserver(function (mutations) {
attachFilePickerOverride();
});
observer.observe(document.body, { childList: true, subtree: true });
const script = document.createElement("script");
script.src = "${Formbricks.appUrl}/js/surveys.umd.cjs";
script.async = true;
script.onload = () => loadSurvey();
script.onerror = (error) => {
FormbricksJavascript.message(JSON.stringify({ event: "onSurveyLibraryLoadError" }));
console.error("Failed to load Formbricks Surveys library:", error);
};
document.head.appendChild(script);
</script>
</html>
"""
fun loadHtml(surveyId: String) {
val environment = SurveyManager.environmentDataHolder.guard { return }
val json = getJson(environment, surveyId)
val htmlString = htmlTemplate.replace("{{WEBVIEW_DATA}}", json)
html.postValue(htmlString)
}
private fun getJson(environmentDataHolder: EnvironmentDataHolder, surveyId: String): String {
val jsonObject = JsonObject()
environmentDataHolder.getSurveyJson(surveyId).let { jsonObject.add("survey", it) }
jsonObject.addProperty("isBrandingEnabled", true)
jsonObject.addProperty("appUrl", Formbricks.appUrl)
jsonObject.addProperty("environmentId", Formbricks.environmentId)
jsonObject.addProperty("contactId", UserManager.contactId)
jsonObject.addProperty("isWebEnvironment", false)
val isMultiLangSurvey =
(environmentDataHolder.data?.data?.surveys?.first { it.id == surveyId }?.languages?.size
?: 0) > 1
if (isMultiLangSurvey) {
jsonObject.addProperty("languageCode", Formbricks.language)
} else {
jsonObject.addProperty("languageCode", "default")
}
val hasCustomStyling = environmentDataHolder.data?.data?.surveys?.first { it.id == surveyId }?.styling != null
val enabled = environmentDataHolder.data?.data?.project?.styling?.allowStyleOverwrite ?: false
if (hasCustomStyling && enabled) {
environmentDataHolder.getStyling(surveyId)?.let { jsonObject.add("styling", it) }
} else {
environmentDataHolder.getProjectStylingJson()?.let { jsonObject.add("styling", it) }
}
return jsonObject.toString()
.replace("#", "%23") // Hex color code's # breaks the JSON
.replace("\\\"","'") // " is replaced to ' in the html codes in the JSON
}
}
@BindingAdapter("htmlText")
fun WebView.setHtmlText(htmlString: String?) {
loadData(htmlString ?: "", "text/html", "UTF-8")
}

View File

@@ -1,55 +0,0 @@
package com.formbricks.formbrickssdk.webview
import android.webkit.JavascriptInterface
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.logger.Logger
import com.formbricks.formbrickssdk.model.javascript.JsMessageData
import com.formbricks.formbrickssdk.model.javascript.EventType
import com.formbricks.formbrickssdk.model.javascript.FileUploadData
import com.google.gson.JsonParseException
import java.lang.RuntimeException
class WebAppInterface(private val callback: WebAppCallback?) {
interface WebAppCallback {
fun onClose()
fun onDisplayCreated()
fun onResponseCreated()
fun onFilePick(data: FileUploadData)
fun onSurveyLibraryLoadError()
}
/**
* Javascript interface to get messages from the WebView's embedded JS
*/
@JavascriptInterface
fun message(data: String) {
Logger.d(data)
try {
val jsMessage = JsMessageData.from(data)
when (jsMessage.event) {
EventType.ON_CLOSE -> callback?.onClose()
EventType.ON_DISPLAY_CREATED -> callback?.onDisplayCreated()
EventType.ON_RESPONSE_CREATED -> callback?.onResponseCreated()
EventType.ON_FILE_PICK -> { callback?.onFilePick(FileUploadData.from(data)) }
EventType.ON_SURVEY_LIBRARY_LOAD_ERROR -> { callback?.onSurveyLibraryLoadError() }
}
} catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException(e.message))
} catch (e: JsonParseException) {
Logger.e(RuntimeException("Failed to parse JSON message: $data"))
} catch (e: IllegalArgumentException) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Invalid message format: $data"))
} catch (e: Exception) {
Formbricks.callback?.onError(e)
Logger.e(RuntimeException("Unexpected error processing message: $data"))
}
}
companion object {
const val INTERFACE_NAME = "FormbricksJavascript"
}
}

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.formbricks.formbrickssdk.webview.FormbricksViewModel" />
</data>
<WebView
android:id="@+id/formbricks_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
bind:htmlText="@{viewModel.html}"/>
</layout>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="BottomSheetDialog" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
<item name="bottomSheetStyle">@style/BottomSheetModal</item>
</style>
<style name="BottomSheetModal" parent="Widget.Design.BottomSheet.Modal">
<item name="android:background">@android:color/transparent</item>
</style>
</resources>

View File

@@ -1,23 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View File

@@ -1,59 +0,0 @@
[versions]
agp = "8.8.0"
kotlin = "2.0.0"
coreKtx = "1.10.1"
lifecycleRuntimeKtx = "2.6.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
androidx-annotation = "1.8.0"
kotlinx-serialization-json = "1.8.0"
retrofit = "2.9.0"
okhttp3 = "4.11.0"
gson = "2.10.1"
legacySupportV4 = "1.0.0"
lifecycleLivedataKtx = "2.8.7"
lifecycleViewmodelKtx = "2.8.7"
fragmentKtx = "1.8.5"
databindingCommon = "8.8.0"
activity = "1.10.1"
constraintlayout = "2.1.4"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
retrofit-converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retrofit" }
okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp3" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
androidx-databinding-common = { group = "androidx.databinding", name = "databinding-common", version.ref = "databindingCommon" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }

View File

@@ -1,6 +0,0 @@
#Mon Feb 10 09:17:42 CET 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1,24 +0,0 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Formbricks"
include(":app")
include(":formbricksSDK")

View File

@@ -1,2 +0,0 @@
# Xcode user-specific UI state
**/xcuserdata/

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