Compare commits

...

11 Commits

Author SHA1 Message Date
Dhruwang Jariwala
0e0d3780d3 fix: removed completed surveys from survey list in integrations (#4838)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-03-06 09:10:51 +00:00
Peter Pesti-Varga
38ff01aedc feat: Android & iOS SDK (#4871) 2025-03-06 09:43:49 +01:00
Dhruwang Jariwala
cdf687ad80 fix: delete webhook button visibility (#4862) 2025-03-06 07:40:00 +00:00
github-actions[bot]
a399fc7f80 chore: bump version to v3.3.1 (#4873)
Co-authored-by: GitHub Actions <github-actions@github.com>
2025-03-06 08:37:59 +01:00
Piyush Gupta
c54a48e70b docs: adds email followup docs (#4858)
Co-authored-by: Johannes <johannes@formbricks.com>
2025-03-04 16:02:54 +00:00
Johannes
884b6f12ae docs: update API intro and key management docs (#4841) 2025-03-04 15:36:23 +00:00
Piyush Gupta
5cae0febc9 fix: variables initialization in logic editor preview (#4819)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-03-04 14:58:13 +00:00
Dhruwang Jariwala
0e898db710 chore: Remove lib dependency from survey package (#4767)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-03-04 14:58:00 +00:00
Johannes
40d54d60d4 docs: Release test environment docs (#4842) 2025-03-04 14:57:41 +00:00
Matti Nannt
269e026381 fix: sonarqube action not running in merge queue (#4861) 2025-03-04 15:57:26 +01:00
Matti Nannt
8245f2f6af chore: update sonar-config to properly scan apps/web (#4844) 2025-03-03 18:38:16 +01:00
246 changed files with 8301 additions and 390 deletions

View File

@@ -6,6 +6,7 @@ on:
- main
pull_request:
types: [opened, synchronize, reopened]
merge_group:
permissions:
contents: read
jobs:

View File

@@ -1,4 +1,5 @@
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
@@ -15,7 +16,6 @@ import { getIntegrations } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";

View File

@@ -1,4 +1,5 @@
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
@@ -19,7 +20,6 @@ import { getIntegrations } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";

View File

@@ -0,0 +1,48 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { selectSurvey } from "@formbricks/lib/survey/service";
import { transformPrismaSurvey } from "@formbricks/lib/survey/utils";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TSurvey } from "@formbricks/types/surveys/types";
export const getSurveys = reactCache(
async (environmentId: string): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const surveysPrisma = await prisma.survey.findMany({
where: {
environmentId,
status: {
not: "completed",
},
},
select: selectSurvey,
orderBy: {
updatedAt: "desc",
},
});
return surveysPrisma.map((surveyPrisma) => transformPrismaSurvey<TSurvey>(surveyPrisma));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getSurveys-${environmentId}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
}
)()
);

View File

@@ -1,3 +1,4 @@
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
@@ -21,7 +22,6 @@ import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getNotionDatabases } from "@formbricks/lib/notion/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";

View File

@@ -1,3 +1,4 @@
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
@@ -14,7 +15,6 @@ import { getIntegrationByType } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationSlack } from "@formbricks/types/integration/slack";

View File

@@ -21,14 +21,14 @@ import { TSurvey } from "@formbricks/types/surveys/types";
import { deleteWebhookAction, testEndpointAction, updateWebhookAction } from "../actions";
import { TWebhookInput } from "../types/webhooks";
interface ActionSettingsTabProps {
interface WebhookSettingsTabProps {
webhook: Webhook;
surveys: TSurvey[];
setOpen: (v: boolean) => void;
isReadOnly: boolean;
}
export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: ActionSettingsTabProps) => {
export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: WebhookSettingsTabProps) => {
const { t } = useTranslate();
const router = useRouter();
const { register, handleSubmit } = useForm({
@@ -219,7 +219,7 @@ export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: Ac
<div className="flex justify-between border-t border-slate-200 py-6">
<div>
{webhook.source === "user" && !isReadOnly && (
{!isReadOnly && (
<Button
type="button"
variant="destructive"

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "3.3.0",
"version": "3.3.1",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",

View File

@@ -0,0 +1,44 @@
---
title: "Generate API Key"
icon: "key"
description: "Here is how you can generate an API key which gives you full access to the Formbricks Management API. Keep it safe!"
---
<Note>
As of now, API keys are located in the Project Configuration page. We are moving them to the Organization Settings page in the upcoming release. For you, nothing will change.
</Note>
## Generate API key
<Steps>
<Step title="Navigate to 'Configuration'">
Go to the Configuration page of your project:
![Configuration Page](/images/api-reference/config.webp)
</Step>
<Step title="Access 'API Keys' tab">
Click on the **API Keys** tab.
</Step>
<Step title="Generate key">
Decide if you want to generate a key for the development or production environment. If you want to switch environemnts you can do so in the top right corner. Click on the corresponding button.
</Step>
<Step title="Add a label to your key">
Add a label to your key to help you identify it.
![Label key](/images/api-reference/label.webp)
</Step>
<Step title="Copy your key">
Copy the API key and save it in a secure location. You won't be able to see it again.
<Note>
Store API key safely! Anyone who has your API key has full control over your
account. For security reasons, you cannot view the API key again.
</Note>
</Step>
</Steps>
## Delete API key
- On **Configuration** > **API Keys** page, find the key you wish to revoke and select “Delete”.
- Your API key will stop working immediately.

View File

@@ -1,14 +0,0 @@
---
title: "API v1.0.0"
icon: "code-compare"
---
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has *different* authentication requirements, and provides access to different data and settings.
### API Key Setup
Checkout the [API Key Setup](/api-reference/rest-api) to access the Management APIs with an API Key.
If youve forked the collection and are running it, update the `apiKey` and `environmentId` in the collection variables with your values. We also provide post-run scripts to help auto-assign variables when running scripts.
Need more help? Visit our [Website](https://formbricks.com/) or join our [Discord](https://formbricks.com/discord)!

View File

@@ -7725,7 +7725,7 @@
}
},
"summary": "Health Check",
"tags": ["default"]
"tags": ["Health"]
}
}
},

View File

@@ -5,9 +5,8 @@ description: "
Formbricks provides two APIs: the Public Client API for frontend survey interactions and the Management API for backend management tasks."
---
<Info>
View our [API Documentation](/api-reference) in more than 30 frameworks and languages.
</Info>
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has *different* authentication requirements, and provides access to different data and settings.
## Public Client API
@@ -17,7 +16,7 @@ We currently have the following Client API methods exposed and below is their do
- [Displays API](/api-reference/client-api->-display/create-display) - Mark a survey as displayed or link a display to a response for a person.
- [People API](/api-reference/client-api->-people/create-person) - Create & Update a Person (e.g., attributes, email, userId, etc.)
- [Contacts API](/api-reference/client-api->-contacts/update-contact-attributes) - Update contact attributes.
- [Responses API](/api-reference/client-api->-response/create-response) - Create & Update a Response for a Survey.
@@ -41,88 +40,7 @@ We currently have the following Management API methods exposed and below is thei
- [Webhook API](/api-reference/management-api->-webhook/get-all-webhooks) - List, Create, and Delete Webhooks
## How to Generate an API key
API requests require a personal API key for authorization. This API key gives you the same rights as if you were logged in at Formbricks UI - **keep it private!**
- Go to your settings on [Formbricks UI](https://app.formbricks.com).
- Go to page “API keys”
![API Keys](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738097810/image_jvhqsd.jpg)
- Create a key for the development or production environment.
- Copy the key immediately. You wont be able to see it again.
![API Key Label](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738098072/image_zjkvok.jpg)
<Note>
**Store API key safely! Anyone who has your API key has full control over your
account. For security reasons, you cannot view the API key again.**
</Note>
## Test your API Key
Hit the below request to verify that you are authenticated with your API Key and the server is responding.
### Get My Profile
Get the project details and environment type of your account.
### Mandatory Headers
| Name | x-Api-Key |
| --------------- | ------------------------ |
| **Type** | string |
| **Description** | Your Formbricks API key. |
### Request
```bash cURL
GET - /api/v1/me
curl --location \
'https://app.formbricks.com/api/v1/me' \
--header \
'x-api-key: <your-api-key>'
```
### Response
<CodeGroup>
```bash 200 (Success)
{
"id": "cll2m30r70004mx0huqkitgqv",
"createdAt": "2023-08-08T18:04:59.922Z",
"updatedAt": "2023-08-08T18:04:59.922Z",
"type": "production",
"project": {
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Project"
},
"appSetupCompleted": false,
"websiteSetupCompleted": false,
}
```
```bash 401 (Not Authenticated)
Not authenticated
```
</CodeGroup>
### Delete a personal API key
- Go to settings on [app.formbricks.com](https://app.formbricks.com/).
- Go to the page “API keys”.
- Find the key you wish to revoke and select “Delete”.
- Your API key will stop working immediately.
---

View File

@@ -0,0 +1,50 @@
---
title: "Test API Key"
icon: "message-check"
description: "Here is how you can test your API key to make sure it is working."
---
To test if your API key is working, you can use the following request:
### Mandatory Headers
| Name | x-Api-Key |
| --------------- | ------------------------ |
| **Type** | string |
| **Description** | Your Formbricks API key. |
### Request
```bash cURL
GET - /api/v1/me
curl --location \
'https://app.formbricks.com/api/v1/me' \
--header \
'x-api-key: <your-api-key>'
```
### Response
<CodeGroup>
```bash 200 (Success)
{
"id": "cll2m30r70004mx0huqkitgqv",
"createdAt": "2023-08-08T18:04:59.922Z",
"updatedAt": "2023-08-08T18:04:59.922Z",
"type": "production",
"project": {
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Project"
},
"appSetupCompleted": false,
"websiteSetupCompleted": false,
}
```
```bash 401 (Not Authenticated)
Not authenticated
```
</CodeGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -48,7 +48,8 @@
"xm-and-surveys/surveys/general-features/schedule-start-end-dates",
"xm-and-surveys/surveys/general-features/metadata",
"xm-and-surveys/surveys/general-features/variables",
"xm-and-surveys/surveys/general-features/hide-back-button"
"xm-and-surveys/surveys/general-features/hide-back-button",
"xm-and-surveys/surveys/general-features/email-followups"
]
},
{
@@ -90,33 +91,33 @@
]
}
]
},
{
"group": "Question Types",
"icon": "question",
"pages": [
"xm-and-surveys/surveys/question-type/address",
"xm-and-surveys/surveys/question-type/consent",
"xm-and-surveys/surveys/question-type/contact-info",
"xm-and-surveys/surveys/question-type/date",
"xm-and-surveys/surveys/question-type/file-upload",
"xm-and-surveys/surveys/question-type/free-text",
"xm-and-surveys/surveys/question-type/matrix",
"xm-and-surveys/surveys/question-type/net-promoter-score",
"xm-and-surveys/surveys/question-type/ranking",
"xm-and-surveys/surveys/question-type/rating",
"xm-and-surveys/surveys/question-type/schedule-a-meeting",
"xm-and-surveys/surveys/question-type/select-multiple",
"xm-and-surveys/surveys/question-type/select-picture",
"xm-and-surveys/surveys/question-type/select-single",
"xm-and-surveys/surveys/question-type/statement-cta"
]
}
]
},
{
"group": "Core Features",
"pages": [
{
"group": "Question Types",
"icon": "question",
"pages": [
"xm-and-surveys/core-features/question-type/address",
"xm-and-surveys/core-features/question-type/consent",
"xm-and-surveys/core-features/question-type/contact-info",
"xm-and-surveys/core-features/question-type/date",
"xm-and-surveys/core-features/question-type/file-upload",
"xm-and-surveys/core-features/question-type/free-text",
"xm-and-surveys/core-features/question-type/matrix",
"xm-and-surveys/core-features/question-type/net-promoter-score",
"xm-and-surveys/core-features/question-type/ranking",
"xm-and-surveys/core-features/question-type/rating",
"xm-and-surveys/core-features/question-type/schedule-a-meeting",
"xm-and-surveys/core-features/question-type/select-multiple",
"xm-and-surveys/core-features/question-type/select-picture",
"xm-and-surveys/core-features/question-type/select-single",
"xm-and-surveys/core-features/question-type/statement-cta"
]
},
{
"group": "Integrations",
"icon": "bridge",
@@ -136,7 +137,8 @@
},
"xm-and-surveys/core-features/user-management",
"xm-and-surveys/core-features/styling-theme",
"xm-and-surveys/core-features/email-customization"
"xm-and-surveys/core-features/email-customization",
"xm-and-surveys/core-features/test-environment"
]
},
{
@@ -288,7 +290,7 @@
},
{
"group": "API Documentation",
"pages": ["api-reference/introduction", "api-reference/rest-api"]
"pages": ["api-reference/rest-api", "api-reference/generate-key", "api-reference/test-key"]
}
],
"redirects": [

View File

@@ -0,0 +1,39 @@
---
title: "Test Environment"
icon: "code"
description: "Each project on Formbricks has two environments: a test environment and a production environment. The test environment is used to test your surveys through before they are sent out to your customers."
---
To clearly separate between test and production data, Formbricks provides two different environments for each project. These environments are completely independent of each other **incl. API keys, project configuration and environment Id's** used for connecting Formbricks to run in-product surveys.
## Toggle between environments
You can toggle between environments by clicking the environment name in the top right corner of the Formbricks app:
![Toggling between environments](/images/xm-and-surveys/core-features/test-environment/toggle.webp)
### Dev Environment (Testing)
The test environment is used to test your project before it goes live in the production environment. If you see an orange banner at the top, you are currently in the test environment:
![Test environment](/images/xm-and-surveys/core-features/test-environment/test-env.webp)
### Production Environment
The production environment is used to serve your surveys to your respondents.
## Copy surveys between environments
<Steps>
<Step title="Click More Actions">
Click on the "More actions" button of a survey in the survey list
![More actions](/images/xm-and-surveys/core-features/test-environment/more-actions.webp)
</Step>
<Step title="Select Target Environment">
Choose which environment you want to copy the survey to
![Copy survey](/images/xm-and-surveys/core-features/test-environment/modal.webp)
</Step>
</Steps>

View File

@@ -0,0 +1,74 @@
---
title: "Email Follow-ups"
description: "Follow-ups are a feature that allows you to send emails to your users on different survey events."
icon: "envelope"
---
## Overview
The email followup feature allows survey creators to automatically send customized emails to respondents based on their survey responses or when they reach specific survey endings. This feature is particularly useful for following up with respondents, sending thank you notes, or providing additional information.
<Note>
Email followups is a paid feature. It is only available for users on paid plans or have an enterprise license.
</Note>
## Key Components
### 1. Trigger Types
There are two types of triggers for email followups:
- **Response-based**: Triggered when a response is submitted
- **Ending-based**: Triggered when respondents reach specific survey endings
### 2. Email Configuration
Each followup email can be configured with:
- **Name**: A descriptive name for the followup
- **To**: Email recipient (sourced from):
- Open text questions with email input type
- Contact info questions
- Hidden fields
- **Reply-To**: One or more email addresses for replies
- **Subject**: Email subject line
- **Body**: HTML-formatted email content
## Setup Process
1. Navigate to the survey editor
2. Access the `follow-ups` section
![Followups tab](/images/xm-and-surveys/core-features/email-followups/followups-tab.webp)
3. Click the "New follow-up" button to add a new followup
4. Fill in the required information:
- Followup name
- Trigger type (response or endings)
![Followup form](/images/xm-and-surveys/core-features/email-followups/followup-form.webp)
5. **Configuring Recipients**:
The "To" field can be configured to use:
- Responses from email-type open text questions
- Responses from contact info questions
- Values from hidden fields
6. **Configure the Reply-To**:
- Add one or more valid email addresses
- Addresses can be added by typing and pressing space or comma
- Invalid email addresses are automatically rejected
![Followup recipient](/images/xm-and-surveys/core-features/email-followups/followup-recipient.webp)
7. **Configuring the Email Content**:
- Subject
- Body: Supports basic HTML formatting (p, span, b, strong, i, em, a, br tags)
![Followup content](/images/xm-and-surveys/core-features/email-followups/followup-content.webp)
8. **Save and Activate**

15
packages/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

1
packages/android/app/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,62 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
kotlin("kapt")
}
android {
namespace = "com.formbricks.demo"
compileSdk = 35
defaultConfig {
applicationId = "com.formbricks.demo"
minSdk = 26
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 {
compose = true
dataBinding = true
}
}
dependencies {
implementation(project(":formbricksSDK"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.fragment.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

26
packages/android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,26 @@
# 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

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,29 @@
<?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="true"
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:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustPan"
android:theme="@style/Theme.Demo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,71 @@
package com.formbricks.demo
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import com.formbricks.demo.ui.theme.DemoTheme
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.helper.FormbricksConfig
import java.util.UUID
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val config = FormbricksConfig.Builder("[API_HOST]","[ENVIRONMENT_ID]")
.setLoggingEnabled(true)
.setFragmentManager(supportFragmentManager)
Formbricks.setup(this, config.build())
Formbricks.logout()
Formbricks.setUserId(UUID.randomUUID().toString())
enableEdgeToEdge()
setContent {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
DemoTheme {
FormbricksDemo()
}
}
}
}
}
@Composable
fun FormbricksDemo() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
Formbricks.track("click_demo_button")
}) {
Text(
text = "Click me!",
modifier = Modifier.padding(16.dp)
)
}
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
DemoTheme {
FormbricksDemo()
}
}

View File

@@ -0,0 +1,11 @@
package com.formbricks.demo.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,51 @@
package com.formbricks.demo.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun DemoTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true, content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme, typography = Typography, content = content
)
}

View File

@@ -0,0 +1,33 @@
package com.formbricks.demo.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@@ -0,0 +1,170 @@
<?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

@@ -0,0 +1,30 @@
<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

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,10 @@
<?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

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

View File

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

View File

@@ -0,0 +1,13 @@
<?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

@@ -0,0 +1,19 @@
<?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>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

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

View File

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,7 @@
// 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.kotlin.compose) apply false
alias(libs.plugins.android.library) apply false
}

View File

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

View File

@@ -0,0 +1,67 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
kotlin("plugin.serialization") version "2.1.0"
id("org.jetbrains.dokka") version "1.9.10"
}
android {
namespace = "com.formbricks.formbrickssdk"
compileSdk = 35
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
dataBinding = true
viewBinding = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
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.timber)
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)
}

View File

@@ -0,0 +1,35 @@
# 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>;
}

View File

@@ -0,0 +1,24 @@
package com.formbricks.formbrickssdk
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.formbrickssdk.test", appContext.packageName)
}
}

View File

@@ -0,0 +1,7 @@
<?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" />
</manifest>

View File

@@ -0,0 +1,214 @@
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.manager.SurveyManager
import com.formbricks.formbrickssdk.manager.UserManager
import com.formbricks.formbrickssdk.model.error.SDKError
import com.formbricks.formbrickssdk.webview.FormbricksFragment
import timber.log.Timber
@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
private var isInitialized = false
/**
* 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) {
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()
UserManager.syncUserStateIfNeeded()
if (loggingEnabled) {
Timber.plant(Timber.DebugTree())
}
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) {
Timber.e(SDKError.sdkIsNotInitialized)
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) {
Timber.e(SDKError.sdkIsNotInitialized)
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) {
Timber.e(SDKError.sdkIsNotInitialized)
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) {
Timber.e(SDKError.sdkIsNotInitialized)
return
}
Formbricks.language = language
UserManager.addAttribute(language, "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) {
Timber.e(SDKError.sdkIsNotInitialized)
return
}
if (!isInternetAvailable()) {
Timber.w(SDKError.connectionIsNotAvailable)
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) {
Timber.e(SDKError.sdkIsNotInitialized)
return
}
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) {
Timber.e(SDKError.fragmentManagerIsNotSet)
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

@@ -0,0 +1,39 @@
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.withContext
object FormbricksApi {
private val service = FormbricksApiService()
fun initialize() {
service.initialize(
appUrl = Formbricks.appUrl,
isLoggingEnabled = Formbricks.loggingEnabled
)
}
suspend fun getEnvironmentState(): Result<EnvironmentDataHolder> = withContext(Dispatchers.IO) {
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) {
try {
val result = service.postUser(Formbricks.environmentId, PostUserBody.create(userId, attributes)).getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,62 @@
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.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
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 = DateTimeFormatter.ofPattern(dateFormatPattern)
val dateTime = LocalDateTime.parse(it, formatter)
return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant())
} catch (e: Exception) {
return null
}
}
return null
}
fun UserState.expiresAt(): Date? {
expiresAt?.let {
try {
val formatter = DateTimeFormatter.ofPattern(dateFormatPattern)
val dateTime = LocalDateTime.parse(it, formatter)
return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant())
} catch (e: Exception) {
return null
}
}
return null
}
fun EnvironmentDataHolder.expiresAt(): Date? {
data?.expiresAt?.let {
try {
val formatter = DateTimeFormatter.ofPattern(dateFormatPattern)
val dateTime = LocalDateTime.parse(it, formatter)
return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant())
} catch (e: Exception) {
return null
}
}
return null
}

View File

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,62 @@
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

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,284 @@
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.model.environment.DisplayOptionType
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.environment.Survey
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 timber.log.Timber
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Timer
import java.util.TimerTask
interface FileUploadListener {
fun fileUploaded(url: String, uploadId: String)
}
/**
* 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"
private val refreshTimer = Timer()
private var displayTimer = Timer()
private val prefManager by lazy { Formbricks.applicationContext.getSharedPreferences(FORMBRICKS_PREFS, Context.MODE_PRIVATE) }
private 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) {
Timber.tag("SurveyManager").e("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() {
environmentDataHolder?.expiresAt()?.let {
if (it.after(Date())) {
Timber.tag("SurveyManager").d("Environment state is still valid until $it")
filterSurveys()
return
}
}
CoroutineScope(Dispatchers.IO).launch {
try {
environmentDataHolder = FormbricksApi.getEnvironmentState().getOrThrow()
startRefreshTimer(environmentDataHolder?.expiresAt())
filterSurveys()
} catch (e: Exception) {
Timber.tag("SurveyManager").e(e, "Unable to refresh environment state.")
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
}
val shouldDisplay = shouldDisplayBasedOnPercentage(firstSurveyWithActionClass?.displayPercentage)
if (shouldDisplay) {
firstSurveyWithActionClass?.id?.let {
val timeout = firstSurveyWithActionClass.delay ?: 0.0
stopDisplayTimer()
displayTimer.schedule(object : TimerTask() {
override fun run() {
Formbricks.showSurvey(it)
}
}, Date.from(Instant.now().plusSeconds(timeout.toLong())))
}
}
}
private fun stopDisplayTimer() {
try {
displayTimer.cancel()
displayTimer = Timer()
} catch (_: Exception) {
}
}
/**
* Posts a survey response to the Formbricks API.
*/
fun postResponse(surveyId: String?) {
val id = surveyId.guard {
Timber.tag("SurveyManager").e("Survey id is mandatory to set.")
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 {
Timber.tag("SurveyManager").e("Survey id is mandatory to set.")
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() {
Timber.tag("SurveyManager").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() {
Timber.tag("SurveyManager").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) {
DisplayOptionType.RESPOND_MULTIPLE -> true
DisplayOptionType.DISPLAY_ONCE -> {
displays.none { it.surveyId == survey.id }
}
DisplayOptionType.DISPLAY_MULTIPLE -> {
responses.none { it == survey.id }
}
DisplayOptionType.DISPLAY_SOME -> {
survey.displayLimit?.let { limit ->
if (responses.any { it == survey.id }) {
return@filter false
}
displays.count { it.surveyId == survey.id } < limit
} ?: true
}
else -> {
Timber.tag("SurveyManager").e("Invalid Display Option")
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 = ChronoUnit.DAYS.between(lastDisplayedAt.toInstant(), Instant.now())
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
}
}

View File

@@ -0,0 +1,226 @@
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.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 timber.log.Timber
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 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 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
private 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
}
/**
* Saves [surveyId] to the [responses] property.
*
* @param surveyId
*/
fun onResponse(surveyId: String) {
val newResponses = responses?.toMutableList() ?: mutableListOf()
newResponses.add(surveyId)
responses = newResponses
}
/**
* 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
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()
UpdateQueue.current.reset()
SurveyManager.filterSurveys()
startSyncTimer()
} catch (e: Exception) {
Timber.tag("SurveyManager").e(e, "Unable to post survey response.")
}
}
}
/**
* Logs out the user and clears the user state.
*/
fun logout() {
prefManager.edit().apply {
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
backingSegments = null
backingDisplays = null
backingResponses = null
backingLastDisplayedAt = null
backingExpiresAt = null
UpdateQueue.current.reset()
}
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 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

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

View File

@@ -0,0 +1,16 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonIgnoreUnknownKeys
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

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

View File

@@ -0,0 +1,9 @@
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

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

View File

@@ -0,0 +1,48 @@
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

@@ -0,0 +1,10 @@
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

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

View File

@@ -0,0 +1,19 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonIgnoreUnknownKeys
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

@@ -0,0 +1,21 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonIgnoreUnknownKeys
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

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

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