diff --git a/.gitignore b/.gitignore
index 82eac680c..9d18cd92c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,6 +57,9 @@ web_modules/
# TypeScript cache
*.tsbuildinfo
+# parcel cache
+.parcel-cache
+
# Optional npm cache directory
.npm
diff --git a/kleros-app/README.md b/kleros-app/README.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/kleros-app/eslint.config.mjs b/kleros-app/eslint.config.mjs
new file mode 100644
index 000000000..d00b2c977
--- /dev/null
+++ b/kleros-app/eslint.config.mjs
@@ -0,0 +1,120 @@
+import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
+import react from "eslint-plugin-react";
+import reactHooks from "eslint-plugin-react-hooks";
+import security from "eslint-plugin-security";
+import _import from "eslint-plugin-import";
+import globals from "globals";
+import tsParser from "@typescript-eslint/parser";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import js from "@eslint/js";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all,
+});
+
+export default [
+ {
+ ignores: ["src/assets"],
+ },
+ ...fixupConfigRules(
+ compat.extends(
+ "plugin:react/recommended",
+ "plugin:react-hooks/recommended",
+ "plugin:import/recommended",
+ "plugin:import/react",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:prettier/recommended",
+ "prettier"
+ )
+ ),
+ {
+ plugins: {
+ react: fixupPluginRules(react),
+ "react-hooks": fixupPluginRules(reactHooks),
+ security: fixupPluginRules(security),
+ import: fixupPluginRules(_import),
+ },
+
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ ...globals.node,
+ Atomics: "readonly",
+ SharedArrayBuffer: "readonly",
+ },
+
+ parser: tsParser,
+ ecmaVersion: 2020,
+ sourceType: "module",
+
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+
+ settings: {
+ react: {
+ version: "^18.3.1",
+ },
+
+ "import/resolver": {
+ typescript: {
+ project: "./tsconfig.json",
+ },
+ },
+ },
+
+ rules: {
+ "max-len": [
+ "warn",
+ {
+ code: 120,
+ },
+ ],
+
+ "react/prop-types": 0,
+ "no-unused-vars": "off",
+
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ varsIgnorePattern: "(^_+[0-9]*$)|([iI]gnored$)|(^ignored)",
+ argsIgnorePattern: "(^_+[0-9]*$)|([iI]gnored$)|(^ignored)",
+ },
+ ],
+
+ "no-console": [
+ "error",
+ {
+ allow: ["warn", "error", "info", "debug"],
+ },
+ ],
+
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "security/detect-object-injection": "off",
+ "security/detect-non-literal-fs-filename": "off",
+
+ "import/extensions": [
+ "error",
+ "ignorePackages",
+ {
+ js: "never",
+ jsx: "never",
+ ts: "never",
+ tsx: "never",
+ },
+ ],
+
+ "import/no-unresolved": "off",
+ },
+ },
+];
diff --git a/kleros-app/package.json b/kleros-app/package.json
new file mode 100644
index 000000000..56f29fa37
--- /dev/null
+++ b/kleros-app/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@kleros/kleros-app",
+ "version": "1.0.0",
+ "source": "src/lib/index.ts",
+ "main": "dist/main.js",
+ "module": "dist/module.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "clear": "rm -r ../.parcel-cache",
+ "clean": "rm -rf dist",
+ "start": "parcel src/index.html",
+ "build": "yarn clear & yarn clean & yarn parcel build",
+ "check-style": "eslint 'src/**/*.{ts,tsx}' -fix",
+ "check-types": "tsc --noEmit"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/kleros/kleros-v2.git"
+ },
+ "keywords": [
+ "kleros",
+ "dapp",
+ "atlas"
+ ],
+ "author": "Kleros",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/kleros/kleros-v2/issues"
+ },
+ "homepage": "https://github.com/kleros/kleros-v2#readme",
+ "description": "",
+ "files": [
+ "dist"
+ ],
+ "prettier": "@kleros/kleros-v2-prettier-config",
+ "devDependencies": {
+ "@eslint/compat": "^1.2.2",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "^9.14.0",
+ "@kleros/kleros-v2-eslint-config": "workspace:^",
+ "@kleros/kleros-v2-prettier-config": "workspace:^",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "eslint": "^9.14.0",
+ "eslint-config-prettier": "^9.1.0",
+ "globals": "^15.12.0",
+ "parcel": "^2.12.0",
+ "typescript": "^5.6.3"
+ },
+ "dependencies": {
+ "@kleros/ui-components-library": "^2.15.0",
+ "jose": "^5.9.6"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.59.20",
+ "graphql": "^16.9.0",
+ "graphql-request": "^7.1.2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "viem": "^2.21.42",
+ "wagmi": "^2.12.27"
+ }
+}
diff --git a/kleros-app/src/App.tsx b/kleros-app/src/App.tsx
new file mode 100644
index 000000000..51426bc3c
--- /dev/null
+++ b/kleros-app/src/App.tsx
@@ -0,0 +1,18 @@
+import { createRoot } from "react-dom/client";
+import React from "react";
+
+const App = () => {
+ return (
+
+
+
Kleros
+
+
+ );
+};
+
+const app = document.getElementById("app");
+if (app) {
+ const root = createRoot(app);
+ root.render();
+}
diff --git a/kleros-app/src/index.html b/kleros-app/src/index.html
new file mode 100644
index 000000000..b3ab534ee
--- /dev/null
+++ b/kleros-app/src/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Kleros App
+
+
+
+
+
+
diff --git a/kleros-app/src/lib/atlas/hooks/useSessionStorage.ts b/kleros-app/src/lib/atlas/hooks/useSessionStorage.ts
new file mode 100644
index 000000000..5cac8fb6e
--- /dev/null
+++ b/kleros-app/src/lib/atlas/hooks/useSessionStorage.ts
@@ -0,0 +1,23 @@
+import { useState } from "react";
+
+export function useSessionStorage(keyName: string, defaultValue: T) {
+ const [storedValue, setStoredValue] = useState(() => {
+ try {
+ const value = window.sessionStorage.getItem(keyName);
+
+ return value ? JSON.parse(value) : defaultValue;
+ } catch (err) {
+ return defaultValue;
+ }
+ });
+
+ const setValue = (newValue: T) => {
+ try {
+ window.sessionStorage.setItem(keyName, JSON.stringify(newValue));
+ } finally {
+ setStoredValue(newValue);
+ }
+ };
+
+ return [storedValue, setValue] as [T, (newValue: T) => void];
+}
diff --git a/kleros-app/src/lib/atlas/index.ts b/kleros-app/src/lib/atlas/index.ts
new file mode 100644
index 000000000..21968d708
--- /dev/null
+++ b/kleros-app/src/lib/atlas/index.ts
@@ -0,0 +1,2 @@
+export * from "./providers";
+export * from "./utils";
diff --git a/kleros-app/src/lib/atlas/providers/AtlasProvider.tsx b/kleros-app/src/lib/atlas/providers/AtlasProvider.tsx
new file mode 100644
index 000000000..826407d0f
--- /dev/null
+++ b/kleros-app/src/lib/atlas/providers/AtlasProvider.tsx
@@ -0,0 +1,343 @@
+import React, { useMemo, createContext, useContext, useState, useCallback, useEffect } from "react";
+import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
+import { GraphQLClient } from "graphql-request";
+import { decodeJwt } from "jose";
+import { useAccount, useChainId, useSignMessage } from "wagmi";
+import {
+ createMessage,
+ getNonce,
+ loginUser,
+ addUser as addUserToAtlas,
+ fetchUser,
+ updateEmail as updateEmailInAtlas,
+ confirmEmail as confirmEmailInAtlas,
+ uploadToIpfs,
+ type User,
+ type AddUserData,
+ type UpdateEmailData,
+ type ConfirmEmailData,
+ type ConfirmEmailResponse,
+ Roles,
+ Products,
+ AuthorizationError,
+} from "../utils";
+
+import { GraphQLError } from "graphql";
+import { useSessionStorage } from "../hooks/useSessionStorage";
+import { isUndefined } from "../../../utils";
+
+interface IAtlasProvider {
+ isVerified: boolean;
+ isSigningIn: boolean;
+ isAddingUser: boolean;
+ isFetchingUser: boolean;
+ isUpdatingUser: boolean;
+ isUploadingFile: boolean;
+ user: User | undefined;
+ userExists: boolean;
+ authoriseUser: () => Promise;
+ addUser: (userSettings: AddUserData) => Promise;
+ updateEmail: (userSettings: UpdateEmailData) => Promise;
+ uploadFile: (file: File, role: Roles) => Promise;
+ confirmEmail: (userSettings: ConfirmEmailData) => Promise<
+ ConfirmEmailResponse & {
+ isError: boolean;
+ }
+ >;
+}
+
+const Context = createContext(undefined);
+
+interface AtlasConfig {
+ uri: string;
+ product: Products;
+ queryClient: QueryClient;
+}
+
+export const AtlasProvider: React.FC<{ config: AtlasConfig; children?: React.ReactNode }> = ({ children, config }) => {
+ const { address } = useAccount();
+ const chainId = useChainId();
+ const queryClient = useQueryClient(config.queryClient);
+
+ const [authToken, setAuthToken] = useSessionStorage("authToken", undefined);
+ const [isSigningIn, setIsSigningIn] = useState(false);
+ const [isAddingUser, setIsAddingUser] = useState(false);
+ const [isUpdatingUser, setIsUpdatingUser] = useState(false);
+ const [isVerified, setIsVerified] = useState(false);
+ const [isUploadingFile, setIsUploadingFile] = useState(false);
+ const { signMessageAsync } = useSignMessage();
+
+ const atlasGqlClient = useMemo(() => {
+ const headers = authToken
+ ? {
+ authorization: `Bearer ${authToken}`,
+ }
+ : undefined;
+ return new GraphQLClient(`${config.uri}/graphql`, { headers });
+ }, [authToken]);
+
+ /**
+ * @description verifies user authorisation
+ * @returns boolean - true if user is authorized
+ */
+ const verifySession = useCallback(() => {
+ try {
+ if (!authToken || !address) return false;
+
+ const payload = decodeJwt(authToken);
+
+ if ((payload?.sub as string)?.toLowerCase() !== address.toLowerCase()) return false;
+ if (payload.exp && payload.exp < Date.now() / 1000) return false;
+
+ return true;
+ } catch {
+ return false;
+ }
+ }, [authToken, address]);
+
+ useEffect(() => {
+ let timeoutId: ReturnType;
+
+ const verifyAndSchedule = () => {
+ // initial verify check
+ const isValid = verifySession();
+ setIsVerified(isValid);
+
+ if (isValid && authToken) {
+ try {
+ const payload = decodeJwt(authToken);
+ const expiresIn = (payload.exp as number) * 1000 - Date.now();
+
+ timeoutId = setTimeout(verifyAndSchedule, Math.max(0, expiresIn));
+ } catch (err) {
+ console.error("Error decoding JWT:", err);
+ setIsVerified(false);
+ }
+ }
+ };
+
+ verifyAndSchedule();
+
+ return () => {
+ clearTimeout(timeoutId);
+ };
+ }, [authToken, verifySession, address]);
+
+ const {
+ data: user,
+ isLoading: isFetchingUser,
+ refetch: refetchUser,
+ } = useQuery(
+ {
+ queryKey: [`UserSettings`],
+ enabled: isVerified && !isUndefined(address),
+ queryFn: async () => {
+ try {
+ if (!isVerified || isUndefined(address)) return undefined;
+ return await fetchUser(atlasGqlClient);
+ } catch {
+ return undefined;
+ }
+ },
+ },
+ queryClient
+ );
+
+ useEffect(() => {
+ if (!isVerified) return;
+ refetchUser();
+ }, [isVerified, refetchUser]);
+
+ // remove old user's data on address change
+ useEffect(() => {
+ queryClient.removeQueries({ queryKey: ["UserSettings"] });
+ }, [address, queryClient]);
+
+ // this would change based on the fields we have and what defines a user to be existing
+ const userExists = useMemo(() => {
+ if (!user) return false;
+ return !isUndefined(user.email);
+ }, [user]);
+
+ async function fetchWithAuthErrorHandling(request: () => Promise): Promise {
+ try {
+ return await request();
+ } catch (error) {
+ if (
+ error instanceof AuthorizationError ||
+ (error instanceof GraphQLError && error?.extensions?.["code"] === "UNAUTHENTICATED")
+ ) {
+ setIsVerified(false);
+ }
+ throw error;
+ }
+ }
+
+ /**
+ * @description authorise user and enable authorised calls
+ */
+ const authoriseUser = useCallback(
+ async (statement?: string) => {
+ try {
+ if (!address || !chainId) return;
+ setIsSigningIn(true);
+ const nonce = await getNonce(atlasGqlClient, address);
+
+ const message = createMessage(address, nonce, chainId, statement);
+ const signature = await signMessageAsync({ message });
+
+ const token = await loginUser(atlasGqlClient, { message, signature });
+ setAuthToken(token);
+ } catch (err: any) {
+ throw new Error(err);
+ } finally {
+ setIsSigningIn(false);
+ }
+ },
+ [address, chainId, setAuthToken, signMessageAsync, atlasGqlClient]
+ );
+
+ /**
+ * @description adds a new user to atlas
+ * @param {AddUserData} userSettings - object containing data to be added
+ * @returns {Promise} A promise that resolves to true if the user was added successfully
+ */
+ const addUser = useCallback(
+ async (userSettings: AddUserData) => {
+ try {
+ if (!address || !isVerified) return false;
+ setIsAddingUser(true);
+
+ const userAdded = await fetchWithAuthErrorHandling(() => addUserToAtlas(atlasGqlClient, userSettings));
+ refetchUser();
+
+ return userAdded;
+ } catch (err: any) {
+ throw new Error(err);
+ } finally {
+ setIsAddingUser(false);
+ }
+ },
+ [address, isVerified, setIsAddingUser, atlasGqlClient, refetchUser]
+ );
+
+ /**
+ * @description updates user email in atlas
+ * @param {UpdateEmailData} userSettings - object containing data to be updated
+ * @returns {Promise} A promise that resolves to true if email was updated successfully
+ */
+ const updateEmail = useCallback(
+ async (userSettings: UpdateEmailData) => {
+ try {
+ if (!address || !isVerified) return false;
+ setIsUpdatingUser(true);
+
+ const emailUpdated = await fetchWithAuthErrorHandling(() => updateEmailInAtlas(atlasGqlClient, userSettings));
+ refetchUser();
+
+ return emailUpdated;
+ } catch (err: any) {
+ throw new Error(err);
+ } finally {
+ setIsUpdatingUser(false);
+ }
+ },
+ [address, isVerified, setIsUpdatingUser, atlasGqlClient, refetchUser]
+ );
+
+ /**
+ * @description upload file to ipfs
+ * @param {File} file - file to be uploaded
+ * @param {Roles} role - role for which file is being uploaded
+ * @returns {Promise} A promise that resolves to the ipfs cid if file was uploaded successfully else
+ * null
+ */
+ const uploadFile = useCallback(
+ async (file: File, role: Roles) => {
+ try {
+ if (!address || !isVerified || !config.uri || !authToken) return null;
+ setIsUploadingFile(true);
+
+ const hash = await fetchWithAuthErrorHandling(() =>
+ uploadToIpfs({ baseUrl: config.uri, authToken }, { file, name: file.name, role, product: config.product })
+ );
+ return hash ? `/ipfs/${hash}` : null;
+ } catch (err: any) {
+ throw new Error(err);
+ } finally {
+ setIsUploadingFile(false);
+ }
+ },
+ [address, isVerified, setIsUploadingFile, authToken]
+ );
+
+ /**
+ * @description confirms user email in atlas
+ * @param {ConfirmEmailData} userSettings - object containing data to be sent
+ * @returns {Promise} A promise that resolves to true if email was confirmed successfully
+ */
+ const confirmEmail = useCallback(
+ async (userSettings: ConfirmEmailData): Promise => {
+ try {
+ setIsUpdatingUser(true);
+
+ const emailConfirmed = await confirmEmailInAtlas(atlasGqlClient, userSettings);
+
+ return { ...emailConfirmed, isError: false };
+ } catch (err: any) {
+ // eslint-disable-next-line
+ console.log("Confirm Email Error : ", err?.message);
+ return { isConfirmed: false, isTokenExpired: false, isTokenInvalid: false, isError: true };
+ }
+ },
+ [atlasGqlClient]
+ );
+
+ return (
+ ({
+ isVerified,
+ isSigningIn,
+ isAddingUser,
+ authoriseUser,
+ addUser,
+ user,
+ isFetchingUser,
+ updateEmail,
+ isUpdatingUser,
+ userExists,
+ isUploadingFile,
+ uploadFile,
+ confirmEmail,
+ }),
+ [
+ isVerified,
+ isSigningIn,
+ isAddingUser,
+ authoriseUser,
+ addUser,
+ user,
+ isFetchingUser,
+ updateEmail,
+ isUpdatingUser,
+ userExists,
+ isUploadingFile,
+ uploadFile,
+ ]
+ )}
+ >
+ {children}
+
+ );
+};
+
+export const useAtlasProvider = () => {
+ const context = useContext(Context);
+ if (!context) {
+ throw new Error("Context Provider not found.");
+ }
+ return context;
+};
+
+export default AtlasProvider;
diff --git a/kleros-app/src/lib/atlas/providers/index.ts b/kleros-app/src/lib/atlas/providers/index.ts
new file mode 100644
index 000000000..fbe2073a3
--- /dev/null
+++ b/kleros-app/src/lib/atlas/providers/index.ts
@@ -0,0 +1 @@
+export * from "./AtlasProvider";
diff --git a/kleros-app/src/lib/atlas/utils/addUser.ts b/kleros-app/src/lib/atlas/utils/addUser.ts
new file mode 100644
index 000000000..0c4da84a8
--- /dev/null
+++ b/kleros-app/src/lib/atlas/utils/addUser.ts
@@ -0,0 +1,37 @@
+import { GraphQLError } from "graphql";
+import { gql, type GraphQLClient } from "graphql-request";
+
+const query = gql`
+ mutation AddUser($settings: AddUserSettingsDto!) {
+ addUser(addUserSettings: $settings)
+ }
+`;
+
+export type AddUserData = {
+ email: string;
+};
+
+type AddUserResponse = {
+ addUser: boolean;
+};
+
+export function addUser(client: GraphQLClient, userData: AddUserData): Promise {
+ const variables = {
+ settings: userData,
+ };
+
+ return client
+ .request(query, variables)
+ .then(async (response) => response.addUser)
+ .catch((errors) => {
+ // eslint-disable-next-line no-console
+ console.log("Add User error:", { errors });
+
+ const error = errors?.response?.errors?.[0];
+
+ if (error) {
+ throw new GraphQLError(error?.message, { ...error });
+ }
+ throw new Error("Unknown Error");
+ });
+}
diff --git a/web/src/utils/atlas/confirmEmail.ts b/kleros-app/src/lib/atlas/utils/confirmEmail.ts
similarity index 100%
rename from web/src/utils/atlas/confirmEmail.ts
rename to kleros-app/src/lib/atlas/utils/confirmEmail.ts
diff --git a/web/src/utils/atlas/createMessage.ts b/kleros-app/src/lib/atlas/utils/createMessage.ts
similarity index 76%
rename from web/src/utils/atlas/createMessage.ts
rename to kleros-app/src/lib/atlas/utils/createMessage.ts
index 9eb24ade9..ebb5531bb 100644
--- a/web/src/utils/atlas/createMessage.ts
+++ b/kleros-app/src/lib/atlas/utils/createMessage.ts
@@ -1,8 +1,6 @@
import { createSiweMessage } from "viem/siwe";
-import { DEFAULT_CHAIN } from "consts/chains";
-
-export const createMessage = (address: `0x${string}`, nonce: string, chainId: number = DEFAULT_CHAIN) => {
+export const createMessage = (address: `0x${string}`, nonce: string, chainId: number, statement?: string) => {
const domain = window.location.host;
const origin = window.location.origin;
@@ -12,7 +10,7 @@ export const createMessage = (address: `0x${string}`, nonce: string, chainId: nu
const message = createSiweMessage({
domain,
address,
- statement: "Sign In to Kleros with Ethereum.",
+ statement: statement ?? "Sign In to Kleros with Ethereum.",
uri: origin,
version: "1",
chainId,
diff --git a/kleros-app/src/lib/atlas/utils/fetchUser.ts b/kleros-app/src/lib/atlas/utils/fetchUser.ts
new file mode 100644
index 000000000..5fe38d294
--- /dev/null
+++ b/kleros-app/src/lib/atlas/utils/fetchUser.ts
@@ -0,0 +1,34 @@
+import { gql, type GraphQLClient } from "graphql-request";
+
+export type User = {
+ email: string;
+ isEmailVerified: string;
+ emailUpdateableAt: string | null;
+};
+
+type GetUserResponse = {
+ user: User;
+};
+const query = gql`
+ query GetUser {
+ user {
+ email
+ isEmailVerified
+ emailUpdateableAt
+ }
+ }
+`;
+
+export async function fetchUser(client: GraphQLClient): Promise {
+ return client
+ .request(query)
+ .then((response) => response.user)
+ .catch((errors) => {
+ // eslint-disable-next-line no-console
+ console.log("Error fetching user :", { errors });
+ const errorMessage = Array.isArray(errors?.response?.errors)
+ ? errors.response.errors[0]?.message
+ : "Error fetching user";
+ throw Error(errorMessage);
+ });
+}
diff --git a/kleros-app/src/lib/atlas/utils/getNonce.ts b/kleros-app/src/lib/atlas/utils/getNonce.ts
new file mode 100644
index 000000000..951503a48
--- /dev/null
+++ b/kleros-app/src/lib/atlas/utils/getNonce.ts
@@ -0,0 +1,29 @@
+import { gql, type GraphQLClient } from "graphql-request";
+
+type GetNonce = {
+ nonce: string;
+};
+
+const query = gql`
+ mutation GetNonce($address: Address!) {
+ nonce(address: $address)
+ }
+`;
+
+export async function getNonce(client: GraphQLClient, address: string): Promise {
+ const variables = {
+ address,
+ };
+
+ return client
+ .request(query, variables)
+ .then((response) => response.nonce)
+ .catch((errors) => {
+ // eslint-disable-next-line no-console
+ console.log("Error fetching nonce for address:", address, { errors });
+ const errorMessage = Array.isArray(errors?.response?.errors)
+ ? errors.response.errors[0]?.message
+ : "Error fetching nonce";
+ throw Error(errorMessage);
+ });
+}
diff --git a/kleros-app/src/lib/atlas/utils/index.ts b/kleros-app/src/lib/atlas/utils/index.ts
new file mode 100644
index 000000000..46e105e64
--- /dev/null
+++ b/kleros-app/src/lib/atlas/utils/index.ts
@@ -0,0 +1,32 @@
+export enum Products {
+ CourtV1 = "CourtV1",
+ CourtV2 = "CourtV2",
+ Curate = "Curate",
+ Escrow = "Escrow",
+ Governor = "Governor",
+ ProofOfHumanity = "ProofOfHumanity",
+ Reality = "Reality",
+ Test = "Test",
+}
+
+export enum Roles {
+ Evidence = "evidence",
+ Generic = "generic",
+ IdentificationVideo = "identification-video",
+ CurateItemImage = "curate-item-image",
+ CurateItemFile = "curate-item-file",
+ Logo = "logo",
+ MetaEvidence = "meta-evidence",
+ Photo = "photo",
+ Policy = "policy",
+ Test = "test",
+}
+
+export * from "./loginUser";
+export * from "./getNonce";
+export * from "./createMessage";
+export * from "./addUser";
+export * from "./fetchUser";
+export * from "./updateEmail";
+export * from "./confirmEmail";
+export * from "./uploadToIpfs";
diff --git a/kleros-app/src/lib/atlas/utils/loginUser.ts b/kleros-app/src/lib/atlas/utils/loginUser.ts
new file mode 100644
index 000000000..968d6652f
--- /dev/null
+++ b/kleros-app/src/lib/atlas/utils/loginUser.ts
@@ -0,0 +1,38 @@
+import { gql, type GraphQLClient } from "graphql-request";
+
+const query = gql`
+ mutation Login($message: String!, $signature: String!) {
+ login(message: $message, signature: $signature)
+ }
+`;
+
+type AuthoriseUserData = {
+ signature: `0x${string}`;
+ message: string;
+};
+
+type Login = {
+ login: {
+ accessToken: string;
+ };
+};
+
+export async function loginUser(client: GraphQLClient, authData: AuthoriseUserData): Promise {
+ const variables = {
+ message: authData.message,
+ signature: authData.signature,
+ };
+
+ return client
+ .request(query, variables)
+ .then(async (response) => response.login.accessToken)
+ .catch((errors) => {
+ // eslint-disable-next-line no-console
+ console.log("Authorization error:", { errors });
+
+ const errorMessage = Array.isArray(errors?.response?.errors)
+ ? errors.response.errors[0]?.message
+ : "Unknown error";
+ throw new Error(errorMessage);
+ });
+}
diff --git a/kleros-app/src/lib/atlas/utils/updateEmail.ts b/kleros-app/src/lib/atlas/utils/updateEmail.ts
new file mode 100644
index 000000000..c217f5c5a
--- /dev/null
+++ b/kleros-app/src/lib/atlas/utils/updateEmail.ts
@@ -0,0 +1,35 @@
+import { GraphQLError } from "graphql";
+import { gql, type GraphQLClient } from "graphql-request";
+
+const query = gql`
+ mutation UpdateEmail($newEmail: String!) {
+ updateEmail(newEmail: $newEmail)
+ }
+`;
+
+export type UpdateEmailData = {
+ newEmail: string;
+};
+
+type UpdateEmailResponse = {
+ updateEmail: boolean;
+};
+
+export async function updateEmail(client: GraphQLClient, userData: UpdateEmailData): Promise {
+ const variables = userData;
+
+ return client
+ .request(query, variables)
+ .then(async (response) => response.updateEmail)
+ .catch((errors) => {
+ // eslint-disable-next-line no-console
+ console.log("Update Email error:", { errors });
+
+ const error = errors?.response?.errors?.[0];
+
+ if (error) {
+ throw new GraphQLError(error?.message, { ...error });
+ }
+ throw new Error("Unknown Error");
+ });
+}
diff --git a/kleros-app/src/lib/atlas/utils/uploadToIpfs.ts b/kleros-app/src/lib/atlas/utils/uploadToIpfs.ts
new file mode 100644
index 000000000..b3283890a
--- /dev/null
+++ b/kleros-app/src/lib/atlas/utils/uploadToIpfs.ts
@@ -0,0 +1,49 @@
+import { Products, Roles } from ".";
+
+export type IpfsUploadPayload = {
+ file: File;
+ name: string;
+ product: Products;
+ role: Roles;
+};
+
+type Config = {
+ baseUrl: string;
+ authToken: string;
+};
+
+export async function uploadToIpfs(config: Config, payload: IpfsUploadPayload): Promise {
+ const formData = new FormData();
+ formData.append("file", payload.file, payload.name);
+ formData.append("name", payload.name);
+ formData.append("product", payload.product);
+ formData.append("role", payload.role);
+
+ return fetch(`${config.baseUrl}/ipfs/file`, {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${config.authToken}`,
+ },
+ body: formData,
+ }).then(async (response) => {
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ message: "Error uploading to IPFS" }));
+
+ if (response.status === 401) throw new AuthorizationError(error.message);
+ throw new Error(error.message);
+ }
+
+ return await response.text();
+ });
+}
+
+export class AuthorizationError extends Error {
+ readonly name = "AuthorizationError" as const;
+ constructor(message: string) {
+ super(message);
+
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, this.constructor);
+ }
+ }
+}
diff --git a/kleros-app/src/lib/index.ts b/kleros-app/src/lib/index.ts
new file mode 100644
index 000000000..d057e151a
--- /dev/null
+++ b/kleros-app/src/lib/index.ts
@@ -0,0 +1 @@
+export * from "./atlas";
diff --git a/kleros-app/src/utils/index.ts b/kleros-app/src/utils/index.ts
new file mode 100644
index 000000000..b0d6c5b33
--- /dev/null
+++ b/kleros-app/src/utils/index.ts
@@ -0,0 +1,2 @@
+export const isUndefined = (maybeObject: any): maybeObject is undefined | null =>
+ typeof maybeObject === "undefined" || maybeObject === null;
diff --git a/kleros-app/tsconfig.json b/kleros-app/tsconfig.json
new file mode 100644
index 000000000..a657db74d
--- /dev/null
+++ b/kleros-app/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "extends": "@kleros/kleros-v2-tsconfig/react-library.json",
+ "compilerOptions": {
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "rootDir": "src",
+ "outDir": "dist",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "strictNullChecks": true,
+ "noUnusedLocals": true,
+ "skipLibCheck": true,
+ "allowSyntheticDefaultImports": true,
+ "removeComments": true,
+ "isolatedModules": true,
+ "moduleResolution": "node",
+ "strict": true,
+ "esModuleInterop": true,
+ "declaration": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": false,
+ "resolveJsonModule": true
+ },
+ "include": [
+ "src/lib/**/*"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist"
+ ]
+}
diff --git a/package.json b/package.json
index dadf545c3..b4c84a67b 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,8 @@
"web-devtools",
"eslint-config",
"prettier-config",
- "tsconfig"
+ "tsconfig",
+ "kleros-app"
],
"packageManager": "yarn@4.5.1",
"volta": {
@@ -35,6 +36,8 @@
"devDependencies": {
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
+ "@parcel/packager-ts": "2.12.0",
+ "@parcel/transformer-typescript-types": "2.12.0",
"assert": "^2.0.0",
"buffer": "^5.7.1",
"conventional-changelog-cli": "^2.2.2",
@@ -44,7 +47,8 @@
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.0",
"process": "^0.11.10",
- "string_decoder": "^1.3.0"
+ "string_decoder": "^1.3.0",
+ "typescript": ">=3.0.0"
},
"resolutions": {
"async@npm^2.4.0": "^2.6.4",
diff --git a/prettier-config/package.json b/prettier-config/package.json
index b237e8474..0f76e95a1 100644
--- a/prettier-config/package.json
+++ b/prettier-config/package.json
@@ -5,7 +5,7 @@
"license": "MIT",
"dependencies": {
"eslint": "^8.57.1",
- "prettier": "^2.8.8",
+ "prettier": "^3.3.3",
"prettier-plugin-solidity": "^1.3.1"
},
"scripts": {
diff --git a/web/package.json b/web/package.json
index cd93ab570..837a544d6 100644
--- a/web/package.json
+++ b/web/package.json
@@ -74,6 +74,7 @@
},
"dependencies": {
"@cyntler/react-doc-viewer": "^1.16.3",
+ "@kleros/kleros-app": "workspace:^",
"@kleros/kleros-sdk": "workspace:^",
"@kleros/kleros-v2-contracts": "workspace:^",
"@kleros/ui-components-library": "^2.15.0",
diff --git a/web/src/components/EnsureAuth.tsx b/web/src/components/EnsureAuth.tsx
index 3414bdf72..7852aade5 100644
--- a/web/src/components/EnsureAuth.tsx
+++ b/web/src/components/EnsureAuth.tsx
@@ -1,10 +1,11 @@
-import React from "react";
+import React, { useCallback } from "react";
import { useAccount } from "wagmi";
import { Button } from "@kleros/ui-components-library";
-
-import { useAtlasProvider } from "context/AtlasProvider";
+import { useAtlasProvider } from "@kleros/kleros-app";
+import { toast } from "react-toastify";
+import { OPTIONS as toastOptions } from "utils/wrapWithToast";
interface IEnsureAuth {
children: React.ReactElement;
@@ -14,12 +15,23 @@ interface IEnsureAuth {
const EnsureAuth: React.FC = ({ children, className }) => {
const { address } = useAccount();
const { isVerified, isSigningIn, authoriseUser } = useAtlasProvider();
+
+ const handleClick = useCallback(() => {
+ toast.info(`Signing in User...`, toastOptions);
+
+ authoriseUser()
+ .then(() => toast.success("Signed In successfully!", toastOptions))
+ .catch((err) => {
+ console.log(err);
+ toast.error(`Sign-In failed: ${err?.message}`, toastOptions);
+ });
+ }, [authoriseUser]);
return isVerified ? (
children
) : (