diff --git a/for-devs.md b/for-devs.md
new file mode 100644
index 000000000..212f99f6f
--- /dev/null
+++ b/for-devs.md
@@ -0,0 +1,78 @@
+## Developer Information
+
+This file is intended for developers. It provides details on the project's structure and instructions on how to work
+with it.
+
+### 1. Commands
+
+* Installation: `npm install`
+* Building packages: `npm run build:all`
+* Building the bundle: `npm run build:bundle`
+* Linting: `npm run lint-fix` (formatting & code validation)
+
+### 2. Environment Setup for Tests (Once)
+
+To set up the environment for testing, run the following commands:
+
+```
+cp demos/client-example-server/env.development demos/client-example-server/.env.test
+cp demos/client-example/env.development demos/client-example/.env.test
+cp demos/client-bundle-example/env.development demos/client-bundle-example/.env.test
+cp dev/scripts/env.test .env.test
+cp dev/scripts/env.test dev/scripts/.env.test
+cp dev/scripts/env.test packages/cli/.env.test
+cp dev/scripts/env.test packages/procaptcha-bundle/.env.test
+```
+
+### 3. Running E2E Client Tests Locally
+
+#### 3.1) Launching services
+
+The DB is docked, and to start the DB service, run the following:
+
+```
+docker compose --file ./docker/docker-compose.test.yml up -d --remove-orphans --force-recreate --always-recreate-deps
+NODE_ENV="test" npm run setup
+```
+
+> Note: the second command should be called once per the container lifetime, and adds the initial data, like domains,
+> siteKeys, etc.
+
+Then start the services:
+
+```
+npm run -w @prosopo/client-example-server build && NODE_ENV=test npm run start:server
+NODE_ENV=test npm run start:demo
+NODE_ENV=test npm run start:provider:admin
+```
+
+#### 3.2) Running the Tests
+
+```
+NODE_ENV=test npm run -w @prosopo/cypress-shared cypress:open:client-example
+```
+
+#### 3.3) Stopping Docker Services
+
+After the tests finish, stop the Docker services with:
+
+```
+docker compose --file ./docker/docker-compose.test.yml down
+```
+
+### 4. Running E2E Bundle Tests Locally
+
+#### 4.1) Launching Services
+
+For bundle tests, use the same services as for the E2E client tests, plus the following:
+
+```
+NODE_ENV="development" npm -w @prosopo/procaptcha-bundle run bundle
+NODE_ENV=test npm run start:bundle
+```
+
+#### 4.2) Running the Tests
+
+```
+NODE_ENV=test npm -w @prosopo/cypress-shared run cypress:open:client-bundle-example
+```
diff --git a/packages/procaptcha-bundle/src/util/defaultCallbacks.ts b/packages/procaptcha-bundle/src/util/defaultCallbacks.ts
index 01cd8ebed..8bc13474d 100644
--- a/packages/procaptcha-bundle/src/util/defaultCallbacks.ts
+++ b/packages/procaptcha-bundle/src/util/defaultCallbacks.ts
@@ -29,7 +29,18 @@ export const getWindowCallback = (callbackName: string) => {
return fn;
};
-export const getDefaultCallbacks = (element: Element) => ({
+export interface Callbacks {
+ onHuman: (token: ProcaptchaToken) => void;
+ onChallengeExpired: () => void;
+ onExpired: () => void;
+ onError: (error: Error) => void;
+ onClose: () => void;
+ onOpen: () => void;
+ onFailed: () => void;
+ onReset: () => void;
+}
+
+export const getDefaultCallbacks = (element: Element): Callbacks => ({
onHuman: (token: ProcaptchaToken) => handleOnHuman(element, token),
onChallengeExpired: () => {
removeProcaptchaResponse();
@@ -60,16 +71,7 @@ export const getDefaultCallbacks = (element: Element) => ({
export function setUserCallbacks(
renderOptions: ProcaptchaRenderOptions | undefined,
- callbacks: {
- onHuman: (token: ProcaptchaToken) => void;
- onChallengeExpired: () => void;
- onExpired: () => void;
- onError: (error: Error) => void;
- onClose: () => void;
- onOpen: () => void;
- onFailed: () => void;
- onReset: () => void;
- },
+ callbacks: Callbacks,
element: Element,
) {
if (typeof renderOptions?.callback === "function") {
diff --git a/packages/procaptcha-bundle/src/util/renderLogic.tsx b/packages/procaptcha-bundle/src/util/renderLogic.tsx
index 07feac6ab..88bbfd2fa 100644
--- a/packages/procaptcha-bundle/src/util/renderLogic.tsx
+++ b/packages/procaptcha-bundle/src/util/renderLogic.tsx
@@ -1,5 +1,3 @@
-import createCache from "@emotion/cache";
-import { CacheProvider } from "@emotion/react";
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,94 +11,34 @@ import { CacheProvider } from "@emotion/react";
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-import { ProcaptchaFrictionless } from "@prosopo/procaptcha-frictionless";
-import { ProcaptchaPow } from "@prosopo/procaptcha-pow";
-import { Procaptcha } from "@prosopo/procaptcha-react";
-import type {
- ProcaptchaClientConfigOutput,
- ProcaptchaRenderOptions,
+import {
+ FeaturesEnum,
+ type ProcaptchaClientConfigOutput,
+ type ProcaptchaRenderOptions,
} from "@prosopo/types";
-import { type Root, createRoot } from "react-dom/client";
-import { getDefaultCallbacks, setUserCallbacks } from "./defaultCallbacks.js";
-import { setLanguage } from "./language.js";
-import { setTheme } from "./theme.js";
-import { setValidChallengeLength } from "./timeout.js";
+import { CaptchaRenderer } from "./renderLogic/captcha/captchaRenderer.js";
+import { WebComponent } from "./renderLogic/webComponent.js";
+import { WidgetRenderer } from "./renderLogic/widgetRenderer.js";
-const identifierPrefix = "procaptcha-";
-
-function makeShadowRoot(
- element: Element,
- renderOptions?: ProcaptchaRenderOptions,
-): ShadowRoot {
- // todo maybe introduce customCSS in renderOptions.
- const customCss = "";
-
- const wrapperElement = document.createElement("prosopo-procaptcha");
-
- const wrapperShadow = wrapperElement.attachShadow({ mode: "open" });
- wrapperShadow.innerHTML +=
- '';
- wrapperShadow.innerHTML +=
- "" !== customCss ? `` : "";
-
- element.appendChild(wrapperElement);
-
- return wrapperShadow;
-}
+const widgetRenderer = new WidgetRenderer(
+ new WebComponent(),
+ new CaptchaRenderer(),
+);
export const renderLogic = (
elements: Element[],
config: ProcaptchaClientConfigOutput,
renderOptions?: ProcaptchaRenderOptions,
) => {
- const roots: Root[] = [];
-
- for (const element of elements) {
- const callbacks = getDefaultCallbacks(element);
- const shadowRoot = makeShadowRoot(element, renderOptions);
-
- setUserCallbacks(renderOptions, callbacks, element);
- setTheme(renderOptions, element, config);
- setValidChallengeLength(renderOptions, element, config);
- setLanguage(renderOptions, element, config);
-
- const emotionCache = createCache({
- key: "procaptcha",
- prepend: true,
- container: shadowRoot,
- });
-
- let root: Root | null = null;
- switch (renderOptions?.captchaType) {
- case "pow":
- console.log("rendering pow");
- root = createRoot(shadowRoot, { identifierPrefix });
- root.render(
-
-
- ,
- );
- break;
- case "image":
- console.log("rendering image");
- root = createRoot(shadowRoot, { identifierPrefix });
- root.render(
-
-
- ,
- );
- break;
- default:
- console.log("rendering frictionless");
- root = createRoot(shadowRoot, { identifierPrefix });
- root.render(
-
-
- ,
- );
- break;
- }
- roots.push(root);
- }
- return roots;
+ return widgetRenderer.renderElements(
+ {
+ identifierPrefix: "procaptcha-",
+ emotionCacheKey: "procaptcha",
+ webComponentTag: "prosopo-procaptcha",
+ defaultCaptchaType: FeaturesEnum.Frictionless,
+ },
+ elements,
+ config,
+ renderOptions,
+ );
};
diff --git a/packages/procaptcha-bundle/src/util/renderLogic/captcha/captcha.ts b/packages/procaptcha-bundle/src/util/renderLogic/captcha/captcha.ts
new file mode 100644
index 000000000..7ef95c7ed
--- /dev/null
+++ b/packages/procaptcha-bundle/src/util/renderLogic/captcha/captcha.ts
@@ -0,0 +1,25 @@
+// Copyright 2021-2024 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import type { ProcaptchaClientConfigOutput } from "@prosopo/types";
+import React from "react";
+import type { Callbacks } from "../../defaultCallbacks.js";
+
+interface CaptchaProps {
+ config: ProcaptchaClientConfigOutput;
+ callbacks: Callbacks;
+}
+
+abstract class CaptchaElement extends React.Component {}
+
+export { type CaptchaProps, CaptchaElement };
diff --git a/packages/procaptcha-bundle/src/util/renderLogic/captcha/captchaRenderer.tsx b/packages/procaptcha-bundle/src/util/renderLogic/captcha/captchaRenderer.tsx
new file mode 100644
index 000000000..9f4fc7d68
--- /dev/null
+++ b/packages/procaptcha-bundle/src/util/renderLogic/captcha/captchaRenderer.tsx
@@ -0,0 +1,37 @@
+// Copyright 2021-2024 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import type { FeaturesEnum } from "@prosopo/types";
+import type { ReactNode } from "react";
+import type { CaptchaProps } from "./captcha.js";
+import { componentsList } from "./componentsList.js";
+
+class CaptchaRenderer {
+ public render(
+ captchaType: FeaturesEnum,
+ captchaProps: CaptchaProps,
+ ): ReactNode {
+ const CaptchaComponent = componentsList[captchaType];
+
+ console.log(`rendering ${captchaType}`);
+
+ return (
+
+ );
+ }
+}
+
+export { CaptchaRenderer };
diff --git a/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/frictionlessCaptcha.tsx b/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/frictionlessCaptcha.tsx
new file mode 100644
index 000000000..bcac86d78
--- /dev/null
+++ b/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/frictionlessCaptcha.tsx
@@ -0,0 +1,26 @@
+// Copyright 2021-2024 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { ProcaptchaFrictionless } from "@prosopo/procaptcha-frictionless";
+import React from "react";
+import { CaptchaElement } from "../captcha.js";
+
+class FrictionlessCaptcha extends CaptchaElement {
+ public override render() {
+ const { config, callbacks } = this.props;
+
+ return ;
+ }
+}
+
+export { FrictionlessCaptcha };
diff --git a/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/imageCaptcha.tsx b/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/imageCaptcha.tsx
new file mode 100644
index 000000000..4acbdea04
--- /dev/null
+++ b/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/imageCaptcha.tsx
@@ -0,0 +1,26 @@
+// Copyright 2021-2024 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { Procaptcha } from "@prosopo/procaptcha-react";
+import React from "react";
+import { CaptchaElement } from "../captcha.js";
+
+class ImageCaptcha extends CaptchaElement {
+ public override render() {
+ const { config, callbacks } = this.props;
+
+ return ;
+ }
+}
+
+export { ImageCaptcha };
diff --git a/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/powCaptcha.tsx b/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/powCaptcha.tsx
new file mode 100644
index 000000000..4b626e72d
--- /dev/null
+++ b/packages/procaptcha-bundle/src/util/renderLogic/captcha/components/powCaptcha.tsx
@@ -0,0 +1,26 @@
+// Copyright 2021-2024 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { ProcaptchaPow } from "@prosopo/procaptcha-pow";
+import React from "react";
+import { CaptchaElement } from "../captcha.js";
+
+class PowCaptcha extends CaptchaElement {
+ public override render() {
+ const { config, callbacks } = this.props;
+
+ return ;
+ }
+}
+
+export { PowCaptcha };
diff --git a/packages/procaptcha-bundle/src/util/renderLogic/captcha/componentsList.ts b/packages/procaptcha-bundle/src/util/renderLogic/captcha/componentsList.ts
new file mode 100644
index 000000000..364cc3405
--- /dev/null
+++ b/packages/procaptcha-bundle/src/util/renderLogic/captcha/componentsList.ts
@@ -0,0 +1,30 @@
+// Copyright 2021-2024 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { FeaturesEnum } from "@prosopo/types";
+import type React from "react";
+import type { CaptchaProps } from "./captcha.js";
+import { FrictionlessCaptcha } from "./components/frictionlessCaptcha.js";
+import { ImageCaptcha } from "./components/imageCaptcha.js";
+import { PowCaptcha } from "./components/powCaptcha.js";
+
+const componentsList: Record<
+ FeaturesEnum,
+ React.ComponentType
+> = {
+ [FeaturesEnum.Image]: ImageCaptcha,
+ [FeaturesEnum.Pow]: PowCaptcha,
+ [FeaturesEnum.Frictionless]: FrictionlessCaptcha,
+};
+
+export { componentsList };
diff --git a/packages/procaptcha-bundle/src/util/renderLogic/webComponent.ts b/packages/procaptcha-bundle/src/util/renderLogic/webComponent.ts
new file mode 100644
index 000000000..61ef8d2cd
--- /dev/null
+++ b/packages/procaptcha-bundle/src/util/renderLogic/webComponent.ts
@@ -0,0 +1,48 @@
+// Copyright 2021-2024 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+class WebComponent {
+ public addToElement(componentTag: string, element: Element): ShadowRoot {
+ const webComponent = this.makeWebComponent(componentTag);
+ const shadowRoot = this.attachShadowDom(webComponent);
+
+ element.appendChild(webComponent);
+
+ return shadowRoot;
+ }
+
+ protected makeWebComponent(componentTag: string): HTMLElement {
+ return document.createElement(componentTag);
+ }
+
+ protected getBaseShadowStyles(): string {
+ // todo maybe introduce customCSS in renderOptions.
+ const customCss = "";
+
+ let baseStyles =
+ '';
+ baseStyles += "" !== customCss ? `` : "";
+
+ return baseStyles;
+ }
+
+ protected attachShadowDom(webComponent: HTMLElement): ShadowRoot {
+ const shadowRoot = webComponent.attachShadow({ mode: "open" });
+
+ shadowRoot.innerHTML += this.getBaseShadowStyles();
+
+ return shadowRoot;
+ }
+}
+
+export { WebComponent };
diff --git a/packages/procaptcha-bundle/src/util/renderLogic/widgetRenderer.tsx b/packages/procaptcha-bundle/src/util/renderLogic/widgetRenderer.tsx
new file mode 100644
index 000000000..248eca594
--- /dev/null
+++ b/packages/procaptcha-bundle/src/util/renderLogic/widgetRenderer.tsx
@@ -0,0 +1,123 @@
+// Copyright 2021-2024 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import createCache, { type EmotionCache } from "@emotion/cache";
+import { CacheProvider } from "@emotion/react";
+import type {
+ ProcaptchaClientConfigOutput,
+ ProcaptchaRenderOptions,
+} from "@prosopo/types";
+import type { FeaturesEnum } from "@prosopo/types";
+import { type Root, createRoot } from "react-dom/client";
+import {
+ type Callbacks,
+ getDefaultCallbacks,
+ setUserCallbacks,
+} from "../defaultCallbacks.js";
+import { setLanguage } from "../language.js";
+import { setTheme } from "../theme.js";
+import { setValidChallengeLength } from "../timeout.js";
+import type { CaptchaRenderer } from "./captcha/captchaRenderer.js";
+import type { WebComponent } from "./webComponent.js";
+
+interface RenderSettings {
+ identifierPrefix: string;
+ emotionCacheKey: string;
+ webComponentTag: string;
+ defaultCaptchaType: FeaturesEnum;
+}
+
+class WidgetRenderer {
+ private readonly webComponent: WebComponent;
+ private readonly captchaRenderer: CaptchaRenderer;
+
+ constructor(webComponent: WebComponent, captchaRenderer: CaptchaRenderer) {
+ this.webComponent = webComponent;
+ this.captchaRenderer = captchaRenderer;
+ }
+
+ public renderElements(
+ settings: RenderSettings,
+ elements: Element[],
+ config: ProcaptchaClientConfigOutput,
+ renderOptions?: ProcaptchaRenderOptions,
+ ): Root[] {
+ return elements.map((element) => {
+ return this.renderElement(settings, element, config, renderOptions);
+ });
+ }
+
+ protected renderElement(
+ settings: RenderSettings,
+ element: Element,
+ config: ProcaptchaClientConfigOutput,
+ renderOptions?: ProcaptchaRenderOptions,
+ ): Root {
+ const captchaType =
+ (renderOptions?.captchaType as FeaturesEnum) ||
+ settings.defaultCaptchaType;
+ const callbacks = getDefaultCallbacks(element);
+
+ this.readAndValidateSettings(element, callbacks, config, renderOptions);
+
+ // Clear all the children inside, if there are any.
+ // If the renderElement() is called several times on the same element, it should recreate the captcha from scratch.
+ element.innerHTML = "";
+
+ const shadowRoot = this.webComponent.addToElement(
+ settings.webComponentTag,
+ element,
+ );
+ const emotionCache = this.makeEmotionCache(
+ settings.emotionCacheKey,
+ shadowRoot,
+ );
+ const root = createRoot(shadowRoot, {
+ identifierPrefix: settings.identifierPrefix,
+ });
+
+ const captcha = this.captchaRenderer.render(captchaType, {
+ config: config,
+ callbacks: callbacks,
+ });
+
+ root.render({captcha});
+
+ return root;
+ }
+
+ protected readAndValidateSettings(
+ element: Element,
+ callbacks: Callbacks,
+ config: ProcaptchaClientConfigOutput,
+ renderOptions?: ProcaptchaRenderOptions,
+ ): void {
+ setUserCallbacks(renderOptions, callbacks, element);
+ setTheme(renderOptions, element, config);
+ setValidChallengeLength(renderOptions, element, config);
+ setLanguage(renderOptions, element, config);
+ }
+
+ protected makeEmotionCache(
+ cacheKey: string,
+ shadowRoot: ShadowRoot,
+ ): EmotionCache {
+ return createCache({
+ key: cacheKey,
+ prepend: true,
+ container: shadowRoot,
+ });
+ }
+}
+
+export { WidgetRenderer };