Compare commits
11 Commits
v3.3.0
...
fix-github
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e0d3780d3 | ||
|
|
38ff01aedc | ||
|
|
cdf687ad80 | ||
|
|
a399fc7f80 | ||
|
|
c54a48e70b | ||
|
|
884b6f12ae | ||
|
|
5cae0febc9 | ||
|
|
0e898db710 | ||
|
|
40d54d60d4 | ||
|
|
269e026381 | ||
|
|
8245f2f6af |
1
.github/workflows/sonarqube.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
merge_group:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@formbricks/web",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules .next",
|
||||
|
||||
44
docs/api-reference/generate-key.mdx
Normal 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:
|
||||

|
||||
</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.
|
||||

|
||||
</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.
|
||||
@@ -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 you’ve 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)!
|
||||
@@ -7725,7 +7725,7 @@
|
||||
}
|
||||
},
|
||||
"summary": "Health Check",
|
||||
"tags": ["default"]
|
||||
"tags": ["Health"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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”
|
||||
|
||||

|
||||
|
||||
- Create a key for the development or production environment.
|
||||
|
||||
- Copy the key immediately. 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>
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
50
docs/api-reference/test-key.mdx
Normal 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>
|
||||
BIN
docs/images/api-reference/config.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/images/api-reference/label.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
@@ -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": [
|
||||
|
||||
39
docs/xm-and-surveys/core-features/test-environment.mdx
Normal 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:
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Select Target Environment">
|
||||
Choose which environment you want to copy the survey to
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
7. **Configuring the Email Content**:
|
||||
|
||||
- Subject
|
||||
- Body: Supports basic HTML formatting (p, span, b, strong, i, em, a, br tags)
|
||||
|
||||

|
||||
|
||||
8. **Save and Activate**
|
||||
15
packages/android/.gitignore
vendored
Normal 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
@@ -0,0 +1 @@
|
||||
/build
|
||||
62
packages/android/app/build.gradle.kts
Normal 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
@@ -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 { *; }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
29
packages/android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
BIN
packages/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
BIN
packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
10
packages/android/app/src/main/res/values/colors.xml
Normal 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>
|
||||
3
packages/android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Demo</string>
|
||||
</resources>
|
||||
5
packages/android/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Demo" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
13
packages/android/app/src/main/res/xml/backup_rules.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
7
packages/android/build.gradle.kts
Normal 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
|
||||
}
|
||||
1
packages/android/formbricksSDK/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
67
packages/android/formbricksSDK/build.gradle.kts
Normal 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)
|
||||
}
|
||||
0
packages/android/formbricksSDK/consumer-rules.pro
Normal file
35
packages/android/formbricksSDK/proguard-rules.pro
vendored
Normal 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>;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.formbricks.formbrickssdk.model
|
||||
|
||||
interface BaseFormbricksResponse
|
||||
@@ -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?,
|
||||
)
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||