Compare commits

..

5 Commits

Author SHA1 Message Date
StepSecurity Bot
93f60db26c [StepSecurity] Apply security best practices
Signed-off-by: StepSecurity Bot <bot@stepsecurity.io>
2025-03-06 10:15:24 +00:00
Dhruwang Jariwala
0e0d3780d3 fix: removed completed surveys from survey list in integrations (#4838)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-03-06 09:10:51 +00:00
Peter Pesti-Varga
38ff01aedc feat: Android & iOS SDK (#4871) 2025-03-06 09:43:49 +01:00
Dhruwang Jariwala
cdf687ad80 fix: delete webhook button visibility (#4862) 2025-03-06 07:40:00 +00:00
github-actions[bot]
a399fc7f80 chore: bump version to v3.3.1 (#4873)
Co-authored-by: GitHub Actions <github-actions@github.com>
2025-03-06 08:37:59 +01:00
190 changed files with 7528 additions and 135 deletions

96
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
- package-ecosystem: npm
directory: /apps/demo-react-native
schedule:
interval: daily
- package-ecosystem: npm
directory: /apps/demo
schedule:
interval: daily
- package-ecosystem: npm
directory: /apps/storybook
schedule:
interval: daily
- package-ecosystem: docker
directory: /apps/web
schedule:
interval: daily
- package-ecosystem: npm
directory: /apps/web
schedule:
interval: daily
- package-ecosystem: npm
directory: /
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/api
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/config-eslint
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/config-prettier
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/config-typescript
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/database
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/js-core
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/js
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/lib
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/react-native
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/surveys
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/types
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/vite-plugins
schedule:
interval: daily

View File

@@ -5,6 +5,9 @@ on:
types:
- opened
permissions:
contents: read
jobs:
label_on_pr:
runs-on: ubuntu-latest
@@ -15,8 +18,13 @@ jobs:
pull-requests: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Apply labels from linked issue to PR
uses: actions/github-script@v5
uses: actions/github-script@211cb3fefb35a799baa5156f9321bb774fe56294 # v5.2.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -12,7 +12,12 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Build & Cache Web Binaries

View File

@@ -11,19 +11,24 @@ jobs:
name: Run Chromatic
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: actions/setup-node@v4
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Run Chromatic
uses: chromaui/action@latest
uses: chromaui/action@c93e0bc3a63aa176e14a75b61a31847cbfdd341c # latest
with:
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

78
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: ["main"]
pull_request:
# The branches below must be a subset of the branches above
branches: ["main"]
schedule:
- cron: "0 0 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["javascript", "typescript"]
# CodeQL supports [ $supported-codeql-languages ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
with:
category: "/language:${{matrix.language}}"

View File

@@ -18,6 +18,11 @@ jobs:
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@v2
with:
egress-policy: audit
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |

View File

@@ -7,6 +7,9 @@ on:
schedule:
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
- cron: "0 8 * * 1"
permissions:
contents: read
jobs:
cron-weeklySummary:
permissions:
@@ -16,6 +19,11 @@ jobs:
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@v2
with:
egress-policy: audit
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |

27
.github/workflows/dependency-review.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0

View File

@@ -43,16 +43,21 @@ jobs:
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v3
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x
uses: actions/setup-node@v3
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
@@ -112,7 +117,7 @@ jobs:
- name: Azure login
if: env.AZURE_ENABLED == 'true'
uses: azure/login@v2
uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -130,7 +135,7 @@ jobs:
run: |
pnpm test:e2e
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
if: always()
with:
name: playwright-report

View File

@@ -4,6 +4,9 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
labeler:
name: Pull Request Labeler
@@ -12,7 +15,12 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481

View File

@@ -12,6 +12,11 @@ jobs:
timeout-minutes: 15
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: ./.github/actions/dangerous-git-checkout

View File

@@ -50,6 +50,11 @@ jobs:
checks: write
statuses: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@v2
with:
egress-policy: audit
- name: fail if conditional jobs failed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
run: exit 1

View File

@@ -18,6 +18,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: ./.github/actions/dangerous-git-checkout

View File

@@ -26,23 +26,28 @@ jobs:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout Repo
uses: actions/checkout@v2
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: Setup Node.js 18.x
uses: actions/setup-node@v2
uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2
with:
node-version: 18.x
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4
- name: Install Dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9
with:
# This expects you to have a script called release which does a build for your packages and calls changeset publish
publish: pnpm release

View File

@@ -17,6 +17,9 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -28,23 +31,28 @@ jobs:
id-token: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Set up Depot CLI
uses: depot/setup-action@v1
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3 # v3.0.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -54,7 +62,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5 # v5.0.0
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -62,7 +70,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: depot/build-push-action@v1
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
with:
project: tw0fqmsx3c
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}

View File

@@ -20,6 +20,9 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -31,23 +34,28 @@ jobs:
id-token: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Set up Depot CLI
uses: depot/setup-action@v1
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3 # v3.0.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -57,7 +65,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5 # v5.0.0
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -65,7 +73,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: depot/build-push-action@v1
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
with:
project: tw0fqmsx3c
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}

View File

@@ -5,6 +5,9 @@ on:
tags:
- "v*"
permissions:
contents: read
jobs:
release-image-on-dockerhub:
name: Release on Dockerhub
@@ -16,17 +19,22 @@ jobs:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout Repo
uses: actions/checkout@v2
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: Log in to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Get Release Tag
id: extract_release_tag
@@ -36,7 +44,7 @@ jobs:
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1
with:
context: .
file: ./apps/web/Dockerfile

View File

@@ -34,6 +34,11 @@ jobs:
# actions: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
@@ -71,6 +76,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
with:
sarif_file: results.sarif

View File

@@ -16,7 +16,12 @@ jobs:
name: PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -35,7 +40,7 @@ jobs:
revert
ossgg
- uses: marocchino/sticky-pull-request-comment@v2
- uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
@@ -54,7 +59,7 @@ jobs:
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
with:
header: pr-title-lint-error
message: |

View File

@@ -14,7 +14,12 @@ jobs:
name: SonarQube
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

View File

@@ -1,6 +1,9 @@
name: Tests
on:
workflow_call:
permissions:
contents: read
jobs:
build:
name: Unit Tests
@@ -10,16 +13,21 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x
uses: actions/setup-node@v3
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64

View File

@@ -12,11 +12,16 @@ jobs:
check-missing-translations:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: 18

View File

@@ -15,8 +15,13 @@ jobs:
if: github.event.pull_request.merged == true
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0 # This ensures we get the full git history
@@ -38,7 +43,7 @@ jobs:
echo "Detected source branch: $SOURCE_BRANCH"
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: 18 # Ensure compatibility with your project
@@ -77,7 +82,7 @@ jobs:
--yes
- name: Upload backup as artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: tolgee-backup-${{ github.sha }}
path: ./tolgee-backup

View File

@@ -17,7 +17,12 @@ jobs:
timeout-minutes: 10
if: github.event.action == 'opened'
steps:
- uses: actions/first-interaction@v1
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/first-interaction@3c71ce730280171fd1cfb57c00c774f8998586f7 # v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |-

18
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,18 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.16.3
hooks:
- id: gitleaks
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shellcheck
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.38.0
hooks:
- id: eslint
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace

View File

@@ -1,4 +1,4 @@
FROM node:22-alpine3.20 AS base
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
#
## step 1: Prune monorepo

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,67 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
kotlin("plugin.serialization") version "2.1.0"
id("org.jetbrains.dokka") version "1.9.10"
}
android {
namespace = "com.formbricks.formbrickssdk"
compileSdk = 35
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
dataBinding = true
viewBinding = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.annotation)
implementation(libs.androidx.appcompat)
implementation(libs.gson)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.retrofit.converter.scalars)
implementation(libs.okhttp3.logging.interceptor)
implementation(libs.material)
implementation(libs.timber)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.legacy.support.v4)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.databinding.common)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
package com.formbricks.formbrickssdk.api
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.user.PostUserBody
import com.formbricks.formbrickssdk.model.user.UserResponse
import com.formbricks.formbrickssdk.network.FormbricksApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object FormbricksApi {
private val service = FormbricksApiService()
fun initialize() {
service.initialize(
appUrl = Formbricks.appUrl,
isLoggingEnabled = Formbricks.loggingEnabled
)
}
suspend fun getEnvironmentState(): Result<EnvironmentDataHolder> = withContext(Dispatchers.IO) {
try {
val response = service.getEnvironmentStateObject(Formbricks.environmentId)
val result = response.getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun postUser(userId: String, attributes: Map<String, *>?): Result<UserResponse> = withContext(Dispatchers.IO) {
try {
val result = service.postUser(Formbricks.environmentId, PostUserBody.create(userId, attributes)).getOrThrow()
Result.success(result)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,284 @@
package com.formbricks.formbrickssdk.manager
import android.content.Context
import com.formbricks.formbrickssdk.Formbricks
import com.formbricks.formbrickssdk.api.FormbricksApi
import com.formbricks.formbrickssdk.extensions.expiresAt
import com.formbricks.formbrickssdk.extensions.guard
import com.formbricks.formbrickssdk.model.environment.DisplayOptionType
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
import com.formbricks.formbrickssdk.model.environment.Survey
import com.formbricks.formbrickssdk.model.user.Display
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Timer
import java.util.TimerTask
interface FileUploadListener {
fun fileUploaded(url: String, uploadId: String)
}
/**
* The SurveyManager is responsible for managing the surveys that are displayed to the user.
* Filtering surveys based on the user's segments, responses, and displays.
*/
object SurveyManager {
private const val REFRESH_STATE_ON_ERROR_TIMEOUT_IN_MINUTES = 10
private const val FORMBRICKS_PREFS = "formbricks_prefs"
private const val PREF_FORMBRICKS_DATA_HOLDER = "formbricksDataHolder"
private val refreshTimer = Timer()
private var displayTimer = Timer()
private val prefManager by lazy { Formbricks.applicationContext.getSharedPreferences(FORMBRICKS_PREFS, Context.MODE_PRIVATE) }
private var filteredSurveys: MutableList<Survey> = mutableListOf()
private var environmentDataHolderJson: String?
get() {
return prefManager.getString(PREF_FORMBRICKS_DATA_HOLDER, "")
}
set(value) {
if (null != value) {
prefManager.edit().putString(PREF_FORMBRICKS_DATA_HOLDER, value).apply()
} else {
prefManager.edit().remove(PREF_FORMBRICKS_DATA_HOLDER).apply()
}
}
private var backingEnvironmentDataHolder: EnvironmentDataHolder? = null
var environmentDataHolder: EnvironmentDataHolder?
get() {
if (null != backingEnvironmentDataHolder) {
return backingEnvironmentDataHolder
}
synchronized(this) {
backingEnvironmentDataHolder = environmentDataHolderJson?.let { json ->
try {
Gson().fromJson(json, EnvironmentDataHolder::class.java)
} catch (e: Exception) {
Timber.tag("SurveyManager").e("Unable to retrieve environment data from the local storage.")
null
}
}
return backingEnvironmentDataHolder
}
}
set(value) {
synchronized(this) {
backingEnvironmentDataHolder = value
environmentDataHolderJson = Gson().toJson(value)
}
}
/**
* Fills up the [filteredSurveys] array
*/
fun filterSurveys() {
val surveys = environmentDataHolder?.data?.data?.surveys.guard { return }
val displays = UserManager.displays ?: listOf()
val responses = UserManager.responses ?: listOf()
val segments = UserManager.segments ?: listOf()
filteredSurveys = filterSurveysBasedOnDisplayType(surveys, displays, responses).toMutableList()
filteredSurveys = filterSurveysBasedOnRecontactDays(filteredSurveys, environmentDataHolder?.data?.data?.project?.recontactDays?.toInt()).toMutableList()
if (UserManager.userId != null) {
if (segments.isEmpty()) {
filteredSurveys = mutableListOf()
return
}
filteredSurveys = filterSurveysBasedOnSegments(filteredSurveys, segments).toMutableList()
}
}
/**
* Checks if the environment state needs to be refreshed based on its [expiresAt] property,
* and if so, refreshes it, starts the refresh timer, and filters the surveys.
*/
fun refreshEnvironmentIfNeeded() {
environmentDataHolder?.expiresAt()?.let {
if (it.after(Date())) {
Timber.tag("SurveyManager").d("Environment state is still valid until $it")
filterSurveys()
return
}
}
CoroutineScope(Dispatchers.IO).launch {
try {
environmentDataHolder = FormbricksApi.getEnvironmentState().getOrThrow()
startRefreshTimer(environmentDataHolder?.expiresAt())
filterSurveys()
} catch (e: Exception) {
Timber.tag("SurveyManager").e(e, "Unable to refresh environment state.")
startErrorTimer()
}
}
}
/**
* Checks if there are any surveys to display, based in the track action, and if so, displays the first one.
* Handles the display percentage and the delay of the survey.
*/
fun track(action: String) {
val actionClasses = environmentDataHolder?.data?.data?.actionClasses ?: listOf()
val codeActionClasses = actionClasses.filter { it.type == "code" }
val actionClass = codeActionClasses.firstOrNull { it.key == action }
val firstSurveyWithActionClass = filteredSurveys.firstOrNull { survey ->
val triggers = survey.triggers ?: listOf()
triggers.firstOrNull { it.actionClass?.name.equals(actionClass?.name) } != null
}
val shouldDisplay = shouldDisplayBasedOnPercentage(firstSurveyWithActionClass?.displayPercentage)
if (shouldDisplay) {
firstSurveyWithActionClass?.id?.let {
val timeout = firstSurveyWithActionClass.delay ?: 0.0
stopDisplayTimer()
displayTimer.schedule(object : TimerTask() {
override fun run() {
Formbricks.showSurvey(it)
}
}, Date.from(Instant.now().plusSeconds(timeout.toLong())))
}
}
}
private fun stopDisplayTimer() {
try {
displayTimer.cancel()
displayTimer = Timer()
} catch (_: Exception) {
}
}
/**
* Posts a survey response to the Formbricks API.
*/
fun postResponse(surveyId: String?) {
val id = surveyId.guard {
Timber.tag("SurveyManager").e("Survey id is mandatory to set.")
return
}
UserManager.onResponse(id)
}
/**
* Creates a new display for the survey. It is called when the survey is displayed to the user.
*/
fun onNewDisplay(surveyId: String?) {
val id = surveyId.guard {
Timber.tag("SurveyManager").e("Survey id is mandatory to set.")
return
}
UserManager.onDisplay(id)
}
/**
* Starts a timer to refresh the environment state after the given timeout [expiresAt].
*/
private fun startRefreshTimer(expiresAt: Date?) {
val date = expiresAt.guard { return }
refreshTimer.schedule(object: TimerTask() {
override fun run() {
Timber.tag("SurveyManager").d("Refreshing environment state.")
refreshEnvironmentIfNeeded()
}
}, date)
}
/**
* When an error occurs, it starts a timer to refresh the environment state after the given timeout.
*/
private fun startErrorTimer() {
val targetDate = Date(System.currentTimeMillis() + 1000 * 60 * REFRESH_STATE_ON_ERROR_TIMEOUT_IN_MINUTES)
refreshTimer.schedule(object: TimerTask() {
override fun run() {
Timber.tag("SurveyManager").d("Refreshing environment state after an error")
refreshEnvironmentIfNeeded()
}
}, targetDate)
}
/**
* Filters the surveys based on the display type and limit.
*/
private fun filterSurveysBasedOnDisplayType(surveys: List<Survey>, displays: List<Display>, responses: List<String>): List<Survey> {
return surveys.filter { survey ->
when (survey.displayOption) {
DisplayOptionType.RESPOND_MULTIPLE -> true
DisplayOptionType.DISPLAY_ONCE -> {
displays.none { it.surveyId == survey.id }
}
DisplayOptionType.DISPLAY_MULTIPLE -> {
responses.none { it == survey.id }
}
DisplayOptionType.DISPLAY_SOME -> {
survey.displayLimit?.let { limit ->
if (responses.any { it == survey.id }) {
return@filter false
}
displays.count { it.surveyId == survey.id } < limit
} ?: true
}
else -> {
Timber.tag("SurveyManager").e("Invalid Display Option")
false
}
}
}
}
/**
* Filters the surveys based on the recontact days and the [UserManager.lastDisplayedAt] date.
*/
private fun filterSurveysBasedOnRecontactDays(surveys: List<Survey>, defaultRecontactDays: Int?): List<Survey> {
return surveys.filter { survey ->
val lastDisplayedAt = UserManager.lastDisplayedAt.guard { return@filter true }
val recontactDays = survey.recontactDays ?: defaultRecontactDays
if (recontactDays != null) {
val daysBetween = ChronoUnit.DAYS.between(lastDisplayedAt.toInstant(), Instant.now())
return@filter daysBetween >= recontactDays.toInt()
}
true
}
}
/**
* Filters the surveys based on the user's segments.
*/
private fun filterSurveysBasedOnSegments(surveys: List<Survey>, segments: List<String>): List<Survey> {
return surveys.filter { survey ->
val segmentId = survey.segment?.id?.guard { return@filter false }
segments.contains(segmentId)
}
}
/**
* Decides if the survey should be displayed based on the display percentage.
*/
private fun shouldDisplayBasedOnPercentage(displayPercentage: Double?): Boolean {
val percentage = displayPercentage.guard { return true }
val randomNum = (0 until 10000).random() / 100.0
return randomNum <= percentage
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
package com.formbricks.formbrickssdk.model.environment
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
@Serializable
enum class DisplayOptionType {
@SerialName("respondMultiple") RESPOND_MULTIPLE,
@SerialName("displayOnce") DISPLAY_ONCE,
@SerialName("displayMultiple") DISPLAY_MULTIPLE,
@SerialName("displaySome") DISPLAY_SOME,
}
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonIgnoreUnknownKeys
data class Survey(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("triggers") val triggers: List<Trigger>?,
@SerializedName("recontactDays") val recontactDays: Double?,
@SerializedName("displayLimit") val displayLimit: Double?,
@SerializedName("delay") val delay: Double?,
@SerializedName("displayPercentage") val displayPercentage: Double?,
@SerializedName("displayOption") val displayOption: DisplayOptionType?,
@SerializedName("segment") val segment: Segment?,
@SerializedName("styling") val styling: Styling?,
)

View File

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

View File

@@ -0,0 +1,7 @@
package com.formbricks.formbrickssdk.model.error
object SDKError {
val sdkIsNotInitialized = RuntimeException("Formbricks SDK is not initialized")
val fragmentManagerIsNotSet = RuntimeException("The fragment manager is not set.")
val connectionIsNotAvailable = RuntimeException("There is no connection.")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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