From 23af806dbea14535ab2e0e6391312a44463b64ac Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:11:27 +0200 Subject: [PATCH 1/3] more --- .eslintrc | 16 +- package-lock.json | 349 +++++++--------- packages/connect-ui/package.json | 14 +- packages/connect-ui/src/App.tsx | 12 +- .../connect-ui/src/components/ui/button.tsx | 11 +- .../connect-ui/src/components/ui/form.tsx | 117 ++++++ .../connect-ui/src/components/ui/input.tsx | 22 + .../connect-ui/src/components/ui/label.tsx | 17 + packages/connect-ui/src/lib/nango.ts | 6 + packages/connect-ui/src/lib/routes.ts | 21 + packages/connect-ui/src/lib/store.ts | 21 + packages/connect-ui/src/views/Go.tsx | 391 ++++++++++++++++++ .../connect-ui/src/views/IntegrationsList.tsx | 31 +- packages/connect-ui/tailwind.config.js | 3 + packages/connect-ui/tsconfig.json | 3 + packages/frontend/lib/index.ts | 137 ++++-- .../lib/controllers/config.controller.ts | 6 +- .../images/template-logos/unauthenticated.svg | 6 + .../webapp/src/pages/Connection/Create.tsx | 4 +- vite.config.ts | 5 + 20 files changed, 911 insertions(+), 281 deletions(-) create mode 100644 packages/connect-ui/src/components/ui/form.tsx create mode 100644 packages/connect-ui/src/components/ui/input.tsx create mode 100644 packages/connect-ui/src/components/ui/label.tsx create mode 100644 packages/connect-ui/src/lib/nango.ts create mode 100644 packages/connect-ui/src/lib/routes.ts create mode 100644 packages/connect-ui/src/lib/store.ts create mode 100644 packages/connect-ui/src/views/Go.tsx create mode 100644 packages/webapp/public/images/template-logos/unauthenticated.svg diff --git a/.eslintrc b/.eslintrc index 64f4011316e..385929667b3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -220,7 +220,7 @@ "impliedStrict": true, "jsx": true }, - "project": "tsconfig.json" + "project": "packages/connect-ui/tsconfig.json" }, "rules": { // unnecessary when bundling @@ -230,6 +230,14 @@ "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/only-throw-error": "error", "react/prop-types": "off", + "react/jsx-sort-props": [ + "error", + { + "callbacksLast": true, + "shorthandFirst": true, + "reservedFirst": true + } + ], "import/order": [ "error", { @@ -246,16 +254,16 @@ ], "newlines-between": "always", "alphabetize": { - "order": "asc", + "order": "asc" }, "warnOnUnassignedImports": true, "pathGroups": [ { "pattern": "@/**", - "group": "parent", + "group": "parent" }, { - "pattern": "@nangohq/**", + "pattern": "@nangohq/*", "group": "internal", "position": "after" } diff --git a/package-lock.json b/package-lock.json index 3de4b6f8303..e635764bdd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4767,6 +4767,15 @@ "react": ">= 16" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "dev": true, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "dev": true, @@ -6743,6 +6752,85 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", + "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", + "dev": true, + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "dev": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dev": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dev": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz", @@ -8350,8 +8438,9 @@ }, "node_modules/@radix-ui/react-toast": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.1.tgz", + "integrity": "sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==", "dev": true, - "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -23409,15 +23498,6 @@ "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==" }, - "node_modules/lucide-react": { - "version": "0.441.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.441.0.tgz", - "integrity": "sha512-0vfExYtvSDhkC2lqg0zYVW1Uu9GsI4knuV9GP9by5z0Xhc4Zi5RejTxfz9LsjRmCyWVzHCJvxGKZWcRyvQCWVg==", - "dev": true, - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" - } - }, "node_modules/magic-string": { "version": "0.30.10", "dev": true, @@ -26697,6 +26777,22 @@ "react": ">=16.3.0" } }, + "node_modules/react-hook-form": { + "version": "7.53.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz", + "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "18.2.0", "dev": true, @@ -32937,8 +33033,11 @@ "name": "@nangohq/connect-ui", "version": "0.0.0", "devDependencies": { + "@hookform/resolvers": "^3.9.0", + "@nangohq/frontend": "file:../frontend", "@nangohq/types": "file:../types", - "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-slot": "^1.1.0", "@tabler/icons-react": "3.17.0", "@tanstack/react-query": "5.56.2", "@tanstack/react-router": "1.58.3", @@ -32949,16 +33048,18 @@ "class-variance-authority": "0.7.0", "clsx": "2.1.1", "globals": "15.9.0", - "lucide-react": "0.441.0", "postcss": "8.4.45", "react": "18.3.1", "react-dom": "18.3.1", "react-error-boundary": "4.0.13", + "react-hook-form": "^7.53.0", "tailwind-merge": "2.5.2", "tailwindcss": "3.4.11", "tailwindcss-animate": "1.0.7", "typescript": "5.5.3", - "vite": "5.4.6" + "vite": "5.4.6", + "zod": "^3.23.8", + "zustand": "5.0.0-rc.2" } }, "packages/connect-ui/node_modules/@radix-ui/react-compose-refs": { @@ -33135,6 +33236,35 @@ "node": ">=14.17" } }, + "packages/connect-ui/node_modules/zustand": { + "version": "5.0.0-rc.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0-rc.2.tgz", + "integrity": "sha512-o2Nwuvnk8vQBX7CcHL8WfFkZNJdxB/VKeWw0tNglw8p4cypsZ3tRT7rTRTDNeUPFS0qaMBRSKe+fVwL5xpcE3A==", + "dev": true, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "packages/data-ingestion": { "name": "@nangohq/data-ingestion", "version": "1.0.0", @@ -35360,37 +35490,11 @@ "acorn": "^8" } }, - "packages/shared/node_modules/@nangohq/utils/node_modules/agent-base": { - "version": "7.1.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "packages/shared/node_modules/@nangohq/utils/node_modules/async": { "version": "3.2.6", "inBundle": true, "license": "MIT" }, - "packages/shared/node_modules/@nangohq/utils/node_modules/asynckit": { - "version": "0.4.0", - "inBundle": true, - "license": "MIT" - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/axios": { - "version": "1.7.7", - "inBundle": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "packages/shared/node_modules/@nangohq/utils/node_modules/cjs-module-lexer": { "version": "1.4.1", "inBundle": true, @@ -35436,17 +35540,6 @@ "text-hex": "1.0.x" } }, - "packages/shared/node_modules/@nangohq/utils/node_modules/combined-stream": { - "version": "1.0.8", - "inBundle": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "packages/shared/node_modules/@nangohq/utils/node_modules/crypto-randomuuid": { "version": "1.0.0", "inBundle": true, @@ -35500,27 +35593,6 @@ "node": ">=18" } }, - "packages/shared/node_modules/@nangohq/utils/node_modules/debug": { - "version": "4.3.7", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/debug/node_modules/ms": { - "version": "2.1.3", - "inBundle": true, - "license": "MIT" - }, "packages/shared/node_modules/@nangohq/utils/node_modules/delay": { "version": "5.0.0", "inBundle": true, @@ -35532,14 +35604,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/shared/node_modules/@nangohq/utils/node_modules/delayed-stream": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "packages/shared/node_modules/@nangohq/utils/node_modules/detect-newline": { "version": "3.1.0", "inBundle": true, @@ -35558,16 +35622,6 @@ "inBundle": true, "license": "MIT" }, - "packages/shared/node_modules/@nangohq/utils/node_modules/exponential-backoff": { - "version": "3.1.1", - "inBundle": true, - "license": "Apache-2.0" - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/fast-safe-stringify": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT" - }, "packages/shared/node_modules/@nangohq/utils/node_modules/fecha": { "version": "4.2.3", "inBundle": true, @@ -35578,70 +35632,6 @@ "inBundle": true, "license": "MIT" }, - "packages/shared/node_modules/@nangohq/utils/node_modules/follow-redirects": { - "version": "1.15.9", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/form-data": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/guess-json-indent": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18.18.0" - } - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/http-proxy-agent": { - "version": "7.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/https-proxy-agent": { - "version": "7.0.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "packages/shared/node_modules/@nangohq/utils/node_modules/ieee754": { "version": "1.2.1", "funding": [ @@ -35775,25 +35765,6 @@ "node": ">=12" } }, - "packages/shared/node_modules/@nangohq/utils/node_modules/mime-db": { - "version": "1.52.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/mime-types": { - "version": "2.1.35", - "inBundle": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "packages/shared/node_modules/@nangohq/utils/node_modules/module-details-from-path": { "version": "1.0.3", "inBundle": true, @@ -35908,11 +35879,6 @@ "node": ">=12.0.0" } }, - "packages/shared/node_modules/@nangohq/utils/node_modules/proxy-from-env": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT" - }, "packages/shared/node_modules/@nangohq/utils/node_modules/readable-stream": { "version": "3.6.2", "inBundle": true, @@ -36026,22 +35992,6 @@ "safe-buffer": "~5.2.0" } }, - "packages/shared/node_modules/@nangohq/utils/node_modules/string-byte-length": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18.18.0" - } - }, - "packages/shared/node_modules/@nangohq/utils/node_modules/string-byte-slice": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18.18.0" - } - }, "packages/shared/node_modules/@nangohq/utils/node_modules/text-hex": { "version": "1.0.0", "inBundle": true, @@ -36060,19 +36010,6 @@ "node": ">= 14.0.0" } }, - "packages/shared/node_modules/@nangohq/utils/node_modules/truncate-json": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "guess-json-indent": "^3.0.0", - "string-byte-length": "^3.0.0", - "string-byte-slice": "^3.0.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, "packages/shared/node_modules/@nangohq/utils/node_modules/type-fest": { "version": "2.19.0", "inBundle": true, diff --git a/packages/connect-ui/package.json b/packages/connect-ui/package.json index 98c811ee241..8c53695f019 100644 --- a/packages/connect-ui/package.json +++ b/packages/connect-ui/package.json @@ -9,11 +9,14 @@ "preview": "vite preview" }, "devDependencies": { + "@nangohq/frontend": "file:../frontend", "@nangohq/types": "file:../types", - "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-slot": "^1.1.0", "@tabler/icons-react": "3.17.0", "@tanstack/react-query": "5.56.2", "@tanstack/react-router": "1.58.3", + "@hookform/resolvers": "^3.9.0", + "@radix-ui/react-label": "^2.1.0", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@vitejs/plugin-react-swc": "3.5.0", @@ -21,15 +24,18 @@ "class-variance-authority": "0.7.0", "clsx": "2.1.1", "globals": "15.9.0", - "lucide-react": "0.441.0", "postcss": "8.4.45", "react": "18.3.1", "react-dom": "18.3.1", "react-error-boundary": "4.0.13", + "react-hook-form": "^7.53.0", "tailwind-merge": "2.5.2", "tailwindcss": "3.4.11", "tailwindcss-animate": "1.0.7", "typescript": "5.5.3", - "vite": "5.4.6" - } + "vite": "5.4.6", + "zod": "^3.23.8", + "zustand": "5.0.0-rc.2" + }, + "dependencies": {} } diff --git a/packages/connect-ui/src/App.tsx b/packages/connect-ui/src/App.tsx index 93c7d48dd1d..bfc49807030 100644 --- a/packages/connect-ui/src/App.tsx +++ b/packages/connect-ui/src/App.tsx @@ -1,18 +1,10 @@ import { QueryClientProvider, QueryErrorResetBoundary } from '@tanstack/react-query'; -import { RouterProvider, createRouter, createRootRoute } from '@tanstack/react-router'; +import { RouterProvider } from '@tanstack/react-router'; import { ErrorBoundary } from 'react-error-boundary'; import { ErrorFallback } from './components/ErrorFallback.js'; import { queryClient } from './lib/query.js'; -import { IntegrationsList } from './views/IntegrationsList.js'; - -const rootRoute = createRootRoute({ - component: IntegrationsList -}); - -const routeTree = rootRoute.addChildren([]); - -const router = createRouter({ routeTree }); +import { router } from './lib/routes.js'; export const App: React.FC = () => { return ( diff --git a/packages/connect-ui/src/components/ui/button.tsx b/packages/connect-ui/src/components/ui/button.tsx index c9a14371f3c..9371547db32 100644 --- a/packages/connect-ui/src/components/ui/button.tsx +++ b/packages/connect-ui/src/components/ui/button.tsx @@ -1,4 +1,5 @@ import { Slot } from '@radix-ui/react-slot'; +import { IconLoader2 } from '@tabler/icons-react'; import { cva } from 'class-variance-authority'; import * as React from 'react'; @@ -30,11 +31,17 @@ const buttonVariants = cva( export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; + loading?: boolean; } -const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { +const Button = React.forwardRef(({ className, variant, size, asChild = false, loading, children, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; - return ; + return ( + + {loading && } + {children} + + ); }); Button.displayName = 'Button'; diff --git a/packages/connect-ui/src/components/ui/form.tsx b/packages/connect-ui/src/components/ui/form.tsx new file mode 100644 index 00000000000..31f8a91477b --- /dev/null +++ b/packages/connect-ui/src/components/ui/form.tsx @@ -0,0 +1,117 @@ +import { Slot } from '@radix-ui/react-slot'; +import { IconExclamationCircle } from '@tabler/icons-react'; +import * as React from 'react'; +import { Controller, FormProvider, useFormContext } from 'react-hook-form'; + +import { Label } from '@/components/ui/label'; +import { cn } from '@/lib/utils'; + +import type * as LabelPrimitive from '@radix-ui/react-label'; +import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; + +const Form = FormProvider; + +interface FormFieldContextValue = FieldPath> { + name: TName; +} + +const FormFieldContext = React.createContext({} as FormFieldContextValue); + +const FormField = = FieldPath>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState + }; +}; + +interface FormItemContextValue { + id: string; +} + +const FormItemContext = React.createContext({} as FormItemContextValue); + +const FormItem = React.forwardRef>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = 'FormItem'; + +const FormLabel = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => { + const { formItemId } = useFormField(); + + return
-
diff --git a/packages/connect-ui/tailwind.config.js b/packages/connect-ui/tailwind.config.js index 27f80dd74a7..e7ffd6b41de 100644 --- a/packages/connect-ui/tailwind.config.js +++ b/packages/connect-ui/tailwind.config.js @@ -7,6 +7,9 @@ export default { sans: ['Inter', 'sans-serif'] }, extend: { + aria: { + invalid: 'invalid="true"' + }, borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', diff --git a/packages/connect-ui/tsconfig.json b/packages/connect-ui/tsconfig.json index 35ddeaea714..6e8f3e4090c 100644 --- a/packages/connect-ui/tsconfig.json +++ b/packages/connect-ui/tsconfig.json @@ -24,6 +24,9 @@ } }, "references": [ + { + "path": "../frontend" + }, { "path": "../types" } diff --git a/packages/frontend/lib/index.ts b/packages/frontend/lib/index.ts index fa749200f85..6591e350152 100644 --- a/packages/frontend/lib/index.ts +++ b/packages/frontend/lib/index.ts @@ -12,10 +12,19 @@ const enum WSMessageType { Success = 'success' } +export type AuthErrorType = + | 'missingPublicKey' + | 'blocked_by_browser' + | 'invalidHostUrl' + | 'windowIsOpened' + | 'missingCredentials' + | 'windowClosed' + | 'request_error' + | 'missing_ws_client_id'; export class AuthError extends Error { - type: string; + type; - constructor(message: string, type: string) { + constructor(message: string, type: AuthErrorType) { super(message); this.type = type; } @@ -31,17 +40,20 @@ type AuthOptions = { detectClosedAuthWindow?: boolean; // If true, `nango.auth()` would fail if the login window is closed before the authorization flow is completed } & (ConnectionConfig | OAuth2ClientCredentials | OAuthCredentialsOverride | BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials); +type ErrorHandler = (errorType: AuthErrorType, errorDesc: string) => void; + export default class Nango { private hostBaseUrl: string; private websocketsBaseUrl: string; private status: AuthorizationStatus; private publicKey: string; private debug = false; - public win: null | AuthorizationModal = null; private width: number | null = null; private height: number | null = null; private tm: null | NodeJS.Timer = null; + public win: AuthorizationModal | null = null; + constructor(config: { host?: string; websocketsPath?: string; publicKey: string; width?: number; height?: number; debug?: boolean }) { config.host = config.host || prodHost; // Default to Nango Cloud. config.websocketsPath = config.websocketsPath || '/'; // Default to root path. @@ -111,7 +123,7 @@ export default class Nango { if (!res.ok) { const errorResponse = (await res.json()) as PostPublicUnauthenticatedAuthorization['Errors']; - throw new AuthError(errorResponse.error.message || errorResponse.error.code, errorResponse.error.code); + throw new AuthError(errorResponse.error.message || errorResponse.error.code, 'request_error'); } return (await res.json()) as PostPublicUnauthenticatedAuthorization['Success']; @@ -159,7 +171,7 @@ export default class Nango { throw new AuthError('Invalid URL provided for the Nango host.', 'invalidHostUrl'); } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const successHandler = (providerConfigKey: string, connectionId: string, isPending = false) => { if (this.status !== AuthorizationStatus.BUSY) { return; @@ -167,14 +179,15 @@ export default class Nango { this.status = AuthorizationStatus.DONE; - return resolve({ + resolve({ providerConfigKey: providerConfigKey, connectionId: connectionId, isPending }); + return; }; - const errorHandler = (errorType: string, errorDesc: string) => { + const errorHandler: ErrorHandler = (errorType, errorDesc) => { if (this.status !== AuthorizationStatus.BUSY) { return; } @@ -182,12 +195,14 @@ export default class Nango { this.status = AuthorizationStatus.DONE; const error = new AuthError(errorDesc, errorType); - return reject(error); + reject(error); + return; }; if (this.status === AuthorizationStatus.BUSY) { - const error = new AuthError('The authorization window is already opened', 'windowIsOppened'); + const error = new AuthError('The authorization window is already opened', 'windowIsOpened'); reject(error); + return; } // Save authorization status (for handler) @@ -202,25 +217,53 @@ export default class Nango { { width: this.width, height: this.height }, this.debug ); - if (options?.detectClosedAuthWindow || false) { + + if (options?.detectClosedAuthWindow) { this.tm = setInterval(() => { - if (!this.win?.modal.window || this.win.modal.window.closed) { - if (this.win?.isProcessingMessage === true) { - // Modal is still processing a web socket message from the server - // We ignore the window being closed for now - return; - } - clearTimeout(this.tm as unknown as number); - this.win = null; - this.status = AuthorizationStatus.CANCELED; - const error = new AuthError('The authorization window was closed before the authorization flow was completed', 'windowClosed'); - reject(error); + if (!this.win || !this.win.modal) { + return; + } + + if (this.win.modal.window && !this.win.modal.window.closed) { + return; } + + if (this.win.isProcessingMessage) { + // Modal is still processing a web socket message from the server + // We ignore the window being closed for now + return; + } + + clearInterval(this.tm as unknown as number); + this.win = null; + this.status = AuthorizationStatus.CANCELED; + const error = new AuthError('The authorization window was closed before the authorization flow was completed', 'windowClosed'); + reject(error); }, 500); } }); } + /** + * Clear state of the frontend SDK + */ + public clear() { + if (this.tm) { + clearInterval(this.tm as unknown as number); + } + + if (this.win) { + try { + this.win.close(); + } catch { + // do nothing + } + this.win = null; + } + + this.status = AuthorizationStatus.IDLE; + } + /** * Converts the provided credentials to a Connection configuration object * @param credentials - The credentials to convert @@ -576,16 +619,18 @@ class AuthorizationModal { private features: Record; private width = 500; private height = 600; - public modal: Window; private swClient: WebSocket; private debug: boolean; + private wsClientId: string | undefined; + private errorHandler: ErrorHandler; + public modal: Window | undefined; public isProcessingMessage = false; constructor( webSocketUrl: string, url: string, successHandler: (providerConfigKey: string, connectionId: string) => any, - errorHandler: (errorType: string, errorDesc: string) => any, + errorHandler: ErrorHandler, { width, height }: { width?: number | null; height?: number | null }, debug?: boolean ) { @@ -611,13 +656,12 @@ class AuthorizationModal { directories: 'no' }; - this.modal = window.open('', '_blank', this.featuresToString())!; - this.swClient = new WebSocket(webSocketUrl); + this.errorHandler = errorHandler; this.swClient.onmessage = (message: MessageEvent) => { this.isProcessingMessage = true; - this.handleMessage(message, successHandler, errorHandler); + this.handleMessage(message, successHandler); this.isProcessingMessage = false; }; } @@ -626,13 +670,8 @@ class AuthorizationModal { * Handles the messages received from the Nango server via WebSocket * @param message - The message event containing data from the server * @param successHandler - The success handler function to be called when a success message is received - * @param errorHandler - The error handler function to be called when an error message is received */ - handleMessage( - message: MessageEvent, - successHandler: (providerConfigKey: string, connectionId: string) => any, - errorHandler: (errorType: string, errorDesc: string) => any - ) { + handleMessage(message: MessageEvent, successHandler: (providerConfigKey: string, connectionId: string) => any) { const data = JSON.parse(message.data); switch (data.message_type) { @@ -641,8 +680,8 @@ class AuthorizationModal { console.log(debugLogPrefix, 'Connection ack received. Opening modal...'); } - const wsClientId = data.ws_client_id; - this.open(wsClientId); + this.wsClientId = data.ws_client_id; + this.open(); break; } case WSMessageType.Error: @@ -650,7 +689,7 @@ class AuthorizationModal { console.log(debugLogPrefix, 'Error received. Rejecting authorization...'); } - errorHandler(data.error_type, data.error_desc); + this.errorHandler(data.error_type, data.error_desc); this.swClient.close(); break; case WSMessageType.Success: @@ -689,12 +728,21 @@ class AuthorizationModal { /** * Opens a modal window with the specified WebSocket client ID - * @param wsClientId - The WebSocket client ID to include in the URL - * @returns The modal object */ - open(wsClientId: string) { - this.modal.location = this.url + '&ws_client_id=' + wsClientId; - return this.modal; + open() { + if (!this.wsClientId) { + this.errorHandler('missing_ws_client_id', 'Missing WS Client ID while opening modal'); + return; + } + + const popup = window.open(this.url + '&ws_client_id=' + this.wsClientId, '_blank', this.featuresToString()); + + if (!popup || popup.closed || typeof popup.closed == 'undefined') { + this.errorHandler('blocked_by_browser', 'Modal blocked by browser'); + return; + } + + this.modal = popup; } /** @@ -711,4 +759,13 @@ class AuthorizationModal { return featuresAsString.join(','); } + + /** + * Close modal, if opened + */ + close() { + if (this.modal && !this.modal.closed) { + this.modal.close(); + } + } } diff --git a/packages/server/lib/controllers/config.controller.ts b/packages/server/lib/controllers/config.controller.ts index 2568219552a..1b4c497b7c2 100644 --- a/packages/server/lib/controllers/config.controller.ts +++ b/packages/server/lib/controllers/config.controller.ts @@ -144,11 +144,7 @@ class ConfigController { ); res.status(200).send({ - integrations: integrations.sort((a: Integration, b: Integration) => { - const creationDateA = a.creationDate || new Date(0); - const creationDateB = b.creationDate || new Date(0); - return creationDateB.getTime() - creationDateA.getTime(); - }) + integrations: integrations }); } catch (err) { next(err); diff --git a/packages/webapp/public/images/template-logos/unauthenticated.svg b/packages/webapp/public/images/template-logos/unauthenticated.svg new file mode 100644 index 00000000000..d40390c8ad3 --- /dev/null +++ b/packages/webapp/public/images/template-logos/unauthenticated.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/webapp/src/pages/Connection/Create.tsx b/packages/webapp/src/pages/Connection/Create.tsx index 8005e713c57..1eb6a7666e1 100644 --- a/packages/webapp/src/pages/Connection/Create.tsx +++ b/packages/webapp/src/pages/Connection/Create.tsx @@ -758,13 +758,13 @@ nango.${integration?.authMode === 'NONE' ? 'create' : 'auth'}('${integration?.un
diff --git a/vite.config.ts b/vite.config.ts index baced4daab9..eac1fd44891 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,5 +18,10 @@ export default defineConfig({ chaiConfig: { truncateThreshold: 10000 } + }, + server: { + headers: { + 'Cross-Origin-Embedder-Policy': 'unsafe-none' + } } }); From b84e72a364b3dcadf0e175d1eb7089fa0a8c4784 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:28:13 +0200 Subject: [PATCH 2/3] fix pkg --- package-lock.json | 8 ++++---- packages/connect-ui/package.json | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index e635764bdd8..202356fd1e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33033,11 +33033,11 @@ "name": "@nangohq/connect-ui", "version": "0.0.0", "devDependencies": { - "@hookform/resolvers": "^3.9.0", + "@hookform/resolvers": "3.9.0", "@nangohq/frontend": "file:../frontend", "@nangohq/types": "file:../types", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-label": "2.1.0", + "@radix-ui/react-slot": "1.1.0", "@tabler/icons-react": "3.17.0", "@tanstack/react-query": "5.56.2", "@tanstack/react-router": "1.58.3", @@ -33058,7 +33058,7 @@ "tailwindcss-animate": "1.0.7", "typescript": "5.5.3", "vite": "5.4.6", - "zod": "^3.23.8", + "zod": "3.23.8", "zustand": "5.0.0-rc.2" } }, diff --git a/packages/connect-ui/package.json b/packages/connect-ui/package.json index 8c53695f019..02d83062c05 100644 --- a/packages/connect-ui/package.json +++ b/packages/connect-ui/package.json @@ -11,12 +11,12 @@ "devDependencies": { "@nangohq/frontend": "file:../frontend", "@nangohq/types": "file:../types", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-slot": "1.1.0", "@tabler/icons-react": "3.17.0", "@tanstack/react-query": "5.56.2", "@tanstack/react-router": "1.58.3", - "@hookform/resolvers": "^3.9.0", - "@radix-ui/react-label": "^2.1.0", + "@hookform/resolvers": "3.9.0", + "@radix-ui/react-label": "2.1.0", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@vitejs/plugin-react-swc": "3.5.0", @@ -34,7 +34,7 @@ "tailwindcss-animate": "1.0.7", "typescript": "5.5.3", "vite": "5.4.6", - "zod": "^3.23.8", + "zod": "3.23.8", "zustand": "5.0.0-rc.2" }, "dependencies": {} From e4b02d2f6894d5a2e28adaecfea90a2c5edff26a Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:28:33 +0200 Subject: [PATCH 3/3] lint --- packages/connect-ui/src/views/Go.tsx | 2 +- packages/connect-ui/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/connect-ui/src/views/Go.tsx b/packages/connect-ui/src/views/Go.tsx index ddb13e2914d..31ed71e5ff6 100644 --- a/packages/connect-ui/src/views/Go.tsx +++ b/packages/connect-ui/src/views/Go.tsx @@ -103,7 +103,7 @@ export const Go: React.FC = () => { setLoading(false); } }, - [integration] + [integration, loading] ); useEffect(() => { diff --git a/packages/connect-ui/tsconfig.json b/packages/connect-ui/tsconfig.json index 6e8f3e4090c..9e88fd3eb84 100644 --- a/packages/connect-ui/tsconfig.json +++ b/packages/connect-ui/tsconfig.json @@ -31,5 +31,5 @@ "path": "../types" } ], - "include": ["src"] + "include": ["src", "vite.config.ts"] }