diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
new file mode 100644
index 000000000..f8963a41f
--- /dev/null
+++ b/.github/workflows/integration.yml
@@ -0,0 +1,33 @@
+name: Integration Tests
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ run-integration-tests:
+ strategy:
+ matrix:
+ react-version: [18, 19]
+ runs-on: ubuntu-latest
+ name: React ${{ matrix.react-version }}
+ env:
+ INTEGRATION_KNOCK_PUBLIC_KEY: ${{ secrets.INTEGRATION_KNOCK_PUBLIC_KEY }}
+ INTEGRATION_KNOCK_USER_ID: ${{ secrets.INTEGRATION_KNOCK_USER_ID }}
+ INTEGRATION_KNOCK_FEED_ID: ${{ secrets.INTEGRATION_KNOCK_FEED_ID }}
+ steps:
+ - name: Checkout Latest
+ uses: actions/checkout@v4
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: "package.json"
+ cache: "yarn"
+ registry-url: "https://registry.npmjs.org"
+ scope: "@knocklabs"
+ - name: Install Dependencies
+ run: yarn install
+ - name: Build Packages
+ run: yarn build:packages
+ - name: Run Integration Tests
+ run: yarn test:integration:react-${{ matrix.react-version }}
diff --git a/integration/.env.sample b/integration/.env.sample
new file mode 100644
index 000000000..0c938acbd
--- /dev/null
+++ b/integration/.env.sample
@@ -0,0 +1,3 @@
+INTEGRATION_KNOCK_PUBLIC_KEY=
+INTEGRATION_KNOCK_FEED_ID=
+INTEGRATION_KNOCK_USER_ID=
\ No newline at end of file
diff --git a/integration/.eslintrc.js b/integration/.eslintrc.js
new file mode 100644
index 000000000..4c1e8a3fa
--- /dev/null
+++ b/integration/.eslintrc.js
@@ -0,0 +1,17 @@
+/** @type {import("eslint").Linter.Config} */
+module.exports = {
+ root: true,
+ extends: [
+ "@knocklabs/eslint-config/library.js",
+ "plugin:react-hooks/recommended",
+ "plugin:jsx-a11y/strict",
+ ],
+ parserOptions: {
+ projects: ["tsconfig.json", "tsconfig.node.json"],
+ },
+ settings: {
+ "jsx-a11y": {
+ polymorphicPropName: "as",
+ },
+ },
+};
diff --git a/integration/README.md b/integration/README.md
new file mode 100644
index 000000000..7a11410cb
--- /dev/null
+++ b/integration/README.md
@@ -0,0 +1,96 @@
+# Knock Javascript SDK Integration Testing
+
+Because the Knock SDK can be utilized in different versions of React, it's vital that we have visibility into this surface area. This package aims to give maintainers an easy way to verify that their package
+works across the versions of react that we support.
+
+## Getting started
+
+First you will need to setup your environment variables so that the test runner can properly run any of the functions and components that we are testing. You can find an example of what this file should like in `.env.sample`. For Knock employees, you can find the file contents in the 1Password vault titled "JS SDK Integration Testing Env File".
+
+## Running the integration tests locally
+
+There are 3 different ways that you can run the test suite depending on the result you're looking for.
+
+Run for all versions of react that we support:
+
+```bash
+yarn test:integration
+```
+
+Run for individual version of react that we support:
+
+```bash
+yarn test:integration:react-18
+yarn test:integration:react-19
+```
+
+Run for a specific version of react not already defined in `package.json`
+
+```bash
+./integration/run-integration.sh x.x.x
+```
+
+It's recommended that you build your packages using `yarn build:packages` before running this command, to ensure that the test suite references the correctly built packages when running the test suite.
+
+> [!CAUTION]
+> When running the integration tests, the runner will perform a `yarn install` so that the correct version of react is present. In doing so, it will add the `resolutions` key to the root `package.json` temporarily while the test suite runs. Please make sure that the `resolutions` key is **NEVER** committed to version control. See more details about how this process works below.
+
+## Adding new tests
+
+To add new tests to our integration test suite, navigate to `./integration/tests`. We try to categorize tests into top level grouping so that they're easier to find later. For this test suite, at this point in time, we're only _really_ looking to see if the current package can work in multiple versions of react. So, all that you need to do is make sure the component or function is called in the test and ran.
+
+Here's an example of adding a component to our test suite.
+
+```tsx
+import { NotificationFeed } from "@knocklabs/react";
+import { render } from "@testing-library/react";
+import { describe, it } from "vitest";
+
+describe("NotificationFeed", () => {
+ it("should render", () => {
+ render();
+ });
+});
+```
+
+## Explanation of the architecture
+
+In order to reproduce the most realistic integration test, we need to:
+
+1. Build our packages with the react version currently present in the repo.
+2. Override that react version when testing to see how our code responds in those scenarios, similar to how `peerDependencies` work.
+
+### The issue
+
+Unfortunately, this isn't super straightforward. In our `yarn` monorepo there is a single version of `react` present. We do this so that there are not multiple versions running at the same time to avoid this error:
+
+```
+A React Element from an older version of React was rendered. This is not supported. It can happen if:
+- Multiple copies of "react" are used
+- A library pre-bundled an old copy of "react" or "react/jsx-runtime"
+- A compiler tries to "inline" JSX instead of using the runtime.
+```
+
+This means that if we want to test specific versions of react in our integration tests, the entire repo will need to resolve to that version. The initial solve would be to add this specific version as the one that is referenced in `@knocklabs/integration`, this won't work. The resolved version of `react` will end up being the version hoisted in the root `node_modules`. If you try to override that by pointing directly to specific version of `react` in the `node_modules` folder via `vitest` alias (or other solution), you will get the above error because the built version and the resolved version will be running at the same time. We could build the packages utilizing the version we want to test against, but that means in some cases the build would not succeed even though the package would work with a lower version of react.
+
+There is no "easy" way around this.
+
+### The solve
+
+Luckily, `yarn` v4 gives us one singular escape hatch, the `resolutions` key in `package.json`. This config will override **EVERY** version of the specified package throughout the repo, yippee. But the caveat is this value is only configurable in the monorepo's root `package.json` file. So, if we want to test specific `react` versions we'll need to add the `resolutions` key defining those versions. Here's how our script works.
+
+1. Take in the `react` version that the maintainer wants to test. Any version should be easily testable without any extra configuration. So we take this in as a parameter when running the script, example: `./integration.run-integration.sh 18.2.0`.
+2. Create a copy of the monorepo's root `package.json` file so that we can restore it back to it's original state after the run. This helps to prevent the maintainer from committing configuration changes to version control every time they need to test a different version of `react`.
+3. Set the `resolutions` key to the specified `react` version passed as a parameter and write it to the `package.json` file. This sets the version for `react` and `react-dom`.
+4. Run `yarn` so that each instance of `react` points to the specified version.
+5. Run the test suite from `@knocklabs/integration` via `yarn test:integration:runner`
+6. Restore the `package.json` file back to it's original state, removing the `resolutions` key entirely.
+7. Run `yarn` again to restore the dependencies back to their original state.
+
+Full script exists here:
+
+```
+./integration/run-integration.sh
+```
+
+Running the test suite in this way gives the maintainer the ability to locally run the test suite while also allowing us to run the same script in CI. This setup is the best we've found without introducing a TON of overhead to our repo. The crux of this issue is how dependency hoisting is managed in a monorepo. Yarn has a [great article](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/) describing this pattern in more detail, note that this is referencing `yarn` v1 under the "How to use it?" section, which is not applicable to us since we use `yarn` v4.
diff --git a/integration/package.json b/integration/package.json
new file mode 100644
index 000000000..f08089e91
--- /dev/null
+++ b/integration/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@knocklabs/integration",
+ "private": true,
+ "prettier": "@knocklabs/prettier-config",
+ "engines": {
+ "node": "20.9.0"
+ },
+ "scripts": {
+ "test:integration": "vitest run"
+ }
+}
diff --git a/integration/run-integration.sh b/integration/run-integration.sh
new file mode 100755
index 000000000..2a564b54e
--- /dev/null
+++ b/integration/run-integration.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -e # exit on any error
+
+# Get React version from first argument
+REACT_VERSION=$1
+
+if [ -z "$REACT_VERSION" ]; then
+ echo "Error: You must provide a React version."
+ echo "Usage: $0 "
+ exit 1
+fi
+
+# Set the root package.json resolutions to the correct
+# react version so that we can run tests against it.
+JQ_CMD=".resolutions += {}
+| .resolutions[\"react\"] = \"$REACT_VERSION\"
+| .resolutions[\"react-dom\"] = \"$REACT_VERSION\""
+
+# Back up original package.json
+cp ./package.json ./original.json
+
+# Ensure that we always restore the original package.json AND run yarn install on exit
+trap 'echo "Restoring original package.json..."; mv ./original.json ./package.json; echo "Running yarn install to restore lockfile and node_modules..."; yarn install --no-immutable' EXIT
+
+# Apply modifications
+jq "$JQ_CMD" ./package.json > tmp.json
+mv tmp.json ./package.json
+
+echo "Starting run for react version $REACT_VERSION"
+
+# Run commands
+yarn install --no-immutable
+yarn test:integration:runner
+
+
diff --git a/integration/tests/feed/NotificationFeed.test.tsx b/integration/tests/feed/NotificationFeed.test.tsx
new file mode 100644
index 000000000..166dae462
--- /dev/null
+++ b/integration/tests/feed/NotificationFeed.test.tsx
@@ -0,0 +1,26 @@
+import {
+ KnockFeedProvider,
+ KnockProvider,
+ NotificationFeed,
+} from "@knocklabs/react";
+import { render } from "@testing-library/react";
+import { describe, it } from "vitest";
+
+const Feed = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+describe("NotificationFeed", () => {
+ it("should render", () => {
+ render();
+ });
+});
diff --git a/integration/tsconfig.json b/integration/tsconfig.json
new file mode 100644
index 000000000..a25cadbab
--- /dev/null
+++ b/integration/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "@knocklabs/typescript-config/node.json",
+ "include": ["vitest.config.ts", "tests"],
+ "exclude": ["node_modules", "dist"],
+ "compilerOptions": {
+ "types": ["node", "vite"],
+ "incremental": true,
+ "jsx": "react-jsx"
+ }
+}
diff --git a/integration/vitest.config.ts b/integration/vitest.config.ts
new file mode 100644
index 000000000..3b5620bdf
--- /dev/null
+++ b/integration/vitest.config.ts
@@ -0,0 +1,15 @@
+import path from "path";
+import { loadEnv } from "vite";
+import { defineConfig } from "vitest/config";
+
+export default defineConfig(({ mode }) => {
+ return {
+ test: {
+ globals: true,
+ environment: "jsdom",
+ setupFiles: ["./vitest.setup.ts"],
+ env: loadEnv(mode, "./", ""),
+ include: ["./tests/**/*.test.tsx"],
+ },
+ };
+});
diff --git a/integration/vitest.setup.ts b/integration/vitest.setup.ts
new file mode 100644
index 000000000..e26219327
--- /dev/null
+++ b/integration/vitest.setup.ts
@@ -0,0 +1,7 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup } from "@testing-library/react";
+import { afterEach } from "vitest";
+
+afterEach(() => {
+ cleanup();
+});
diff --git a/package.json b/package.json
index 11daed262..c70da3f57 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,10 @@
"test:watch": "vitest run --config=./vitest/config.ts --workspace=./vitest/workspaces.ts --watch",
"test:coverage": "vitest run --config=./vitest/config.ts --workspace=./vitest/workspaces.ts --coverage",
"test:ci": "vitest run --config=./vitest/config.ts --workspace=./vitest/workspaces.ts --silent --coverage --reporter=junit --outputFile=test-report.junit.xml",
+ "test:integration": "yarn test:integration:react-18 && yarn test:integration:react-19",
+ "test:integration:react-18": "./integration/run-integration.sh 18.2.0",
+ "test:integration:react-19": "./integration/run-integration.sh 19.1.0",
+ "test:integration:runner": "cd ./integration && yarn test:integration",
"type:check": "turbo type:check",
"release": "yarn build:packages && yarn release:publish && yarn changeset tag",
"release:publish": "yarn workspaces foreach -Rpt --no-private --from '@knocklabs/*' npm publish --access public --tolerate-republish",
@@ -29,7 +33,8 @@
},
"workspaces": [
"examples/*",
- "packages/*"
+ "packages/*",
+ "integration"
],
"manypkg": {
"defaultBranch": "main",
diff --git a/turbo.json b/turbo.json
index 8e9d199c3..22c73b7a6 100644
--- a/turbo.json
+++ b/turbo.json
@@ -23,6 +23,14 @@
"dev": {
"cache": false,
"persistent": true
+ },
+ "test:integration:runner": {
+ "cache": false,
+ "env": [
+ "INTEGRATION_KNOCK_PUBLIC_KEY",
+ "INTEGRATION_KNOCK_USER_ID",
+ "INTEGRATION_KNOCK_FEED_ID"
+ ]
}
}
}
diff --git a/yarn.lock b/yarn.lock
index 056b33a57..2f87979b7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4143,6 +4143,12 @@ __metadata:
languageName: unknown
linkType: soft
+"@knocklabs/integration@workspace:integration":
+ version: 0.0.0-use.local
+ resolution: "@knocklabs/integration@workspace:integration"
+ languageName: unknown
+ linkType: soft
+
"@knocklabs/javascript@workspace:.":
version: 0.0.0-use.local
resolution: "@knocklabs/javascript@workspace:."