diff --git a/.env.tauri b/.env.tauri new file mode 100644 index 0000000..a77109b --- /dev/null +++ b/.env.tauri @@ -0,0 +1,7 @@ +# THIS ARE PUBLISHABLE PROD ENV VARIABLES + +# CLERK PROD ENV KEY +VITE_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsubGFicmljLmNvJA + +# BACKEND API URL +VITE_SERVER_URL=https://platform.labric.co \ No newline at end of file diff --git a/package.json b/package.json index bc26c12..be4ddd4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "tauri": "tauri" }, "dependencies": { + "@clerk/clerk-react": "^5.35.4", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 368c1df..5f4dfcf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,18 @@ importers: .: dependencies: + '@clerk/clerk-react': + specifier: ^5.35.4 + version: 5.35.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.2.9 version: 1.2.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -256,6 +265,29 @@ packages: resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} engines: {node: '>=6.9.0'} + '@clerk/clerk-react@5.35.4': + resolution: {integrity: sha512-tXTZK8UQBTWxzyR0mhx6T46b5xh/3mzLoQGtx7xZdZ8Ueuw50/AG44B4wlCQU7a+9wXTb1HiXIs5AhuVf36cAA==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + + '@clerk/shared@3.14.0': + resolution: {integrity: sha512-y0glIZBMAQTCcoZ2LEA7rMKfhHrElaTZkSv3Jj7oAU3TAZO0/S5uuWUPaGAHVoxR3OUbQiIJEwo6v1K01FKZ9A==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@clerk/types@4.69.0': + resolution: {integrity: sha512-fkOJ4wvRZDl/RLzVrKw7lEVy+iDcQTzIHxjMTaYlXIfyQiiOIRh473JlG7wZe8wGFnDcl0MGDryWkOvgvx9k/g==} + engines: {node: '>=18.17.0'} + '@esbuild/aix-ppc64@0.25.6': resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} engines: {node: '>=18'} @@ -463,6 +495,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + 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 + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -516,6 +574,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.15': + resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} + 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 + '@radix-ui/react-focus-guards@1.1.2': resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} peerDependencies: @@ -547,6 +618,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-menu@2.1.15': + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} + 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 + '@radix-ui/react-popper@1.2.7': resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} peerDependencies: @@ -599,6 +683,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + 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 + '@radix-ui/react-scroll-area@1.2.9': resolution: {integrity: sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==} peerDependencies: @@ -683,6 +780,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -1205,6 +1311,10 @@ packages: supports-color: optional: true + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -1269,6 +1379,9 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + goober@2.1.16: resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} peerDependencies: @@ -1301,6 +1414,10 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1544,6 +1661,14 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + swr@2.3.4: + resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} @@ -1882,6 +2007,30 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@clerk/clerk-react@5.35.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/shared': 3.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.69.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@clerk/shared@3.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/types': 4.69.0 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.9.0 + swr: 2.3.4(react@18.3.1) + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@clerk/types@4.69.0': + dependencies: + csstype: 3.1.3 + '@esbuild/aix-ppc64@0.25.6': optional: true @@ -2008,6 +2157,31 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-avatar@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 @@ -2061,6 +2235,21 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 @@ -2085,6 +2274,32 @@ snapshots: optionalDependencies: '@types/react': 18.3.23 + '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2132,6 +2347,23 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-scroll-area@1.2.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 @@ -2219,6 +2451,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.23 + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 @@ -2685,6 +2924,8 @@ snapshots: dependencies: ms: 2.1.3 + dequal@2.0.3: {} + detect-libc@2.0.4: {} detect-node-es@1.1.0: {} @@ -2754,6 +2995,8 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + goober@2.1.16(csstype@3.1.3): dependencies: csstype: 3.1.3 @@ -2776,6 +3019,8 @@ snapshots: jiti@2.4.2: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} jsesc@3.1.0: {} @@ -2986,6 +3231,14 @@ snapshots: source-map@0.7.4: {} + std-env@3.9.0: {} + + swr@2.3.4(react@18.3.1): + dependencies: + dequal: 2.0.3 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + tailwind-merge@3.3.1: {} tailwindcss@4.1.11: {} diff --git a/src-tauri/src/upload.rs b/src-tauri/src/upload.rs index 62e0db5..0038e8b 100644 --- a/src-tauri/src/upload.rs +++ b/src-tauri/src/upload.rs @@ -20,7 +20,7 @@ impl Default for UploadConfig { fn default() -> Self { Self { enabled: true, - server_url: "http://localhost:8000".to_string(), + server_url: "https://platform.labric.co".to_string(), ignored_patterns: vec![ "*.tmp".to_string(), "*.log".to_string(), @@ -28,7 +28,7 @@ impl Default for UploadConfig { "node_modules/**".to_string(), ".DS_Store".to_string(), ], - upload_delay_ms: 2000, // 2 seconds delay + upload_delay_ms: 2000, // 2 seconds delay max_concurrent_uploads: 5, // Default to 5 concurrent uploads } } diff --git a/src/components/Simple.tsx b/src/components/Simple.tsx index a4e4312..dab5401 100644 --- a/src/components/Simple.tsx +++ b/src/components/Simple.tsx @@ -160,7 +160,7 @@ export default function Simple() { return (
-

File Watcher

+ {/*

File Watcher

*/}
{/* Folder Selection */} @@ -268,7 +268,9 @@ export default function Simple() { )} {/* Upload Management */} - + {isWatching && ( + + )}
{/* VS Code Style Status Ribbon */} diff --git a/src/components/organization-button.tsx b/src/components/organization-button.tsx new file mode 100644 index 0000000..9f58aa3 --- /dev/null +++ b/src/components/organization-button.tsx @@ -0,0 +1,44 @@ + +import { LogOut } from "lucide-react"; +import { useClerk, useOrganization } from "@clerk/clerk-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; + +export function OrganizationButton() { + const { signOut } = useClerk(); + const { organization } = useOrganization(); + + return ( + + + + + + {organization?.name?.charAt(0)} + + + + + + {organization?.name} + + + signOut()}> + + Sign out + + + + ); +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..b7224f0 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0d6741b --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 807f30e..dc38c69 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,42 +10,58 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as LoginRouteImport } from './routes/login' +import { Route as ProtectedRouteRouteImport } from './routes/_protected/route' import { Route as IndexRouteImport } from './routes/index' +import { Route as ProtectedDashboardRouteImport } from './routes/_protected/dashboard' const LoginRoute = LoginRouteImport.update({ id: '/login', path: '/login', getParentRoute: () => rootRouteImport, } as any) +const ProtectedRouteRoute = ProtectedRouteRouteImport.update({ + id: '/_protected', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) +const ProtectedDashboardRoute = ProtectedDashboardRouteImport.update({ + id: '/dashboard', + path: '/dashboard', + getParentRoute: () => ProtectedRouteRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute + '/dashboard': typeof ProtectedDashboardRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute + '/dashboard': typeof ProtectedDashboardRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/_protected': typeof ProtectedRouteRouteWithChildren '/login': typeof LoginRoute + '/_protected/dashboard': typeof ProtectedDashboardRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/login' + fullPaths: '/' | '/login' | '/dashboard' fileRoutesByTo: FileRoutesByTo - to: '/' | '/login' - id: '__root__' | '/' | '/login' + to: '/' | '/login' | '/dashboard' + id: '__root__' | '/' | '/_protected' | '/login' | '/_protected/dashboard' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + ProtectedRouteRoute: typeof ProtectedRouteRouteWithChildren LoginRoute: typeof LoginRoute } @@ -58,6 +74,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginRouteImport parentRoute: typeof rootRouteImport } + '/_protected': { + id: '/_protected' + path: '' + fullPath: '' + preLoaderRoute: typeof ProtectedRouteRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -65,11 +88,31 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/_protected/dashboard': { + id: '/_protected/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof ProtectedDashboardRouteImport + parentRoute: typeof ProtectedRouteRoute + } } } +interface ProtectedRouteRouteChildren { + ProtectedDashboardRoute: typeof ProtectedDashboardRoute +} + +const ProtectedRouteRouteChildren: ProtectedRouteRouteChildren = { + ProtectedDashboardRoute: ProtectedDashboardRoute, +} + +const ProtectedRouteRouteWithChildren = ProtectedRouteRoute._addFileChildren( + ProtectedRouteRouteChildren, +) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + ProtectedRouteRoute: ProtectedRouteRouteWithChildren, LoginRoute: LoginRoute, } export const routeTree = rootRouteImport diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 70e4a80..ed51bc6 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,9 +1,15 @@ -import { createRootRoute, Outlet, Link } from '@tanstack/react-router' +import { createRootRoute, Outlet } from "@tanstack/react-router"; // import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' -import { useAppUpdater } from '@/hooks/useAppUpdater' +import { useAppUpdater } from "@/hooks/useAppUpdater"; import { Toaster } from "@/components/ui/sonner"; -import { ThemeProvider } from 'next-themes' +import { ThemeProvider } from "next-themes"; +import { ClerkProvider } from "@clerk/clerk-react"; +const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; + +if (!clerkPubKey) { + throw new Error("Missing Publishable Key"); +} export const Route = createRootRoute({ component: () => { @@ -11,22 +17,16 @@ export const Route = createRootRoute({ useAppUpdater(); return ( - - {/* simple header */} -
- -
- - {/* child routes render here */} - + + + {/* child routes render here */} + - {/* devtools are auto-stripped in prod builds */} - {/* {import.meta.env.DEV && } */} - - - ) + {/* devtools are auto-stripped in prod builds */} + {/* {import.meta.env.DEV && } */} + +
+ + ); }, -}) +}); diff --git a/src/routes/_protected/dashboard.tsx b/src/routes/_protected/dashboard.tsx new file mode 100644 index 0000000..b39d40f --- /dev/null +++ b/src/routes/_protected/dashboard.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router' +import Simple from '@/components/Simple' + +export const Route = createFileRoute('/_protected/dashboard')({ + component: () => , +}) diff --git a/src/routes/_protected/route.tsx b/src/routes/_protected/route.tsx new file mode 100644 index 0000000..0097030 --- /dev/null +++ b/src/routes/_protected/route.tsx @@ -0,0 +1,46 @@ +import { Outlet, useRouter, createFileRoute } from "@tanstack/react-router"; +import { SignedIn, useAuth } from "@clerk/clerk-react"; +import { useEffect } from "react"; +import { OrganizationButton } from "@/components/organization-button"; + +export const Route = createFileRoute("/_protected")({ + component: ProtectedLayout, +}); + +function ProtectedLayout() { + const { isLoaded, isSignedIn } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (isLoaded && !isSignedIn) { + router.navigate({ to: "/login" }); + } + }, [isLoaded, isSignedIn, router]); + + if (!isLoaded) + return ( +
+
+

File Watcher

+
+
+ ); + if (!isSignedIn) return null; // Prevent flash of protected content + + return ( + <> + {/* simple header */} +
+ +
+ + + ); +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 9235c6a..e419a6c 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,6 +1,9 @@ -import { createFileRoute } from '@tanstack/react-router' -import Simple from '@/components/Simple' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/')({ - component: () => , + beforeLoad: () => { + throw redirect({ + to: '/dashboard' + }) + }, }) diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 275a043..9780829 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,11 +1,14 @@ -import { createFileRoute } from '@tanstack/react-router' import { useState, useEffect } from 'react' -import { toast } from 'sonner' +import { createFileRoute } from '@tanstack/react-router' +import { useNavigate } from '@tanstack/react-router' import { invoke } from '@tauri-apps/api/core' -import logo from '@/assets/logo.svg' import { openUrl } from "@tauri-apps/plugin-opener" import { fetch } from '@tauri-apps/plugin-http'; +import { useSignIn, useUser, useClerk } from '@clerk/clerk-react' +import { Store } from '@tauri-apps/plugin-store' +import { toast } from 'sonner' import { DeviceInfo } from '@/types' +import logo from '@/assets/logo.svg' export const Route = createFileRoute('/login')({ component: RouteComponent, @@ -51,6 +54,20 @@ function RouteComponent() { const [isLoadingCode, setIsLoadingCode] = useState(true); const [isEnrolled, setIsEnrolled] = useState(false); const [organizationName, setOrganizationName] = useState(null); + const [isSigningIn, setIsSigningIn] = useState(false); + + // Clerk hooks for authentication + const { signIn, setActive } = useSignIn(); + const { user } = useUser(); + const clerk = useClerk(); + const navigate = useNavigate(); + + // If user is already signed in, redirect to home + useEffect(() => { + if (user) { + navigate({ to: '/' }); + } + }, [user, navigate]); const generateRandomCode = () => { const code = Math.floor(Math.random() * 900000) + 100000; // Generate 6-digit number @@ -60,23 +77,49 @@ function RouteComponent() { const fetchEnrollmentCode = async (deviceInfo: DeviceInfo) => { setIsLoadingCode(true); try { - const response = await fetch('http://localhost:8000/api/sync/get_code', { + // Check if org ID is in the store + let organizationId: string | null = null; + const requestBody: any = { + hostname: deviceInfo.hostname, + platform: deviceInfo.platform, + release: deviceInfo.release, + arch: deviceInfo.arch, + cpus: deviceInfo.cpus, + total_memory: deviceInfo.total_memory, + os_type: deviceInfo.os_type, + device_id: deviceInfo.device_id, + device_fingerprint: deviceInfo.device_fingerprint + }; + + try { + const store = await Store.load('app-store.json'); + const storedOrgId = await store.get('organization_id'); + console.log('Retrieved from store - org ID:', storedOrgId, 'Type:', typeof storedOrgId, 'Truthy:', !!storedOrgId); + + if (storedOrgId && typeof storedOrgId === 'string') { + organizationId = storedOrgId; + requestBody.org_id = organizationId; + console.log('Found valid organization ID in store:', organizationId); + } else { + console.log('Organization ID in store is null/undefined or invalid type:', storedOrgId); + // Let's also check if it exists but is a different type + if (storedOrgId !== null && storedOrgId !== undefined) { + console.log('Attempting to convert to string:', String(storedOrgId)); + organizationId = String(storedOrgId); + requestBody.org_id = organizationId; + } + } + } catch (error) { + console.log('No organization ID found in store or store not accessible:', error); + } + + const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/sync/get_code`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - hostname: deviceInfo.hostname, - platform: deviceInfo.platform, - release: deviceInfo.release, - arch: deviceInfo.arch, - cpus: deviceInfo.cpus, - total_memory: deviceInfo.total_memory, - os_type: deviceInfo.os_type, - device_id: deviceInfo.device_id, - device_fingerprint: deviceInfo.device_fingerprint - }) + body: JSON.stringify(requestBody) }); - if (!response.ok) { + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -91,7 +134,7 @@ function RouteComponent() { } } catch (error) { console.error('Failed to fetch enrollment code:', error); - toast.error('Failed to fetch enrollment code from server'); + toast.error('Failed to fetch enrollment code from server', {id: 'enrollment-code-error'}); // Fallback to random code setEnrollmentCode(generateRandomCode()); } finally { @@ -101,7 +144,7 @@ function RouteComponent() { const pollEnrollment = async (deviceInfo: DeviceInfo) => { try { - const response = await fetch('http://localhost:8000/api/sync/poll_enrollment', { + const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/sync/poll_enrollment`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -121,6 +164,99 @@ function RouteComponent() { setIsEnrolled(true); setOrganizationName(data.organization_name || 'Unknown Organization'); console.log('Device enrolled:', data); + + + console.log('Signin token:', data.signin_token); + console.log("signin", signIn) + console.log("setActive", setActive) + console.log("isSigningIn", isSigningIn) + + // Check if we received a sign-in token and Clerk is properly loaded + if (data.signin_token && !isSigningIn && signIn && setActive) { + setIsSigningIn(true); + toast.info('Signing you in...', { id: 'signing-in' }); + + try { + // Create the SignIn with the token using Clerk + const signInAttempt = await signIn.create({ + strategy: 'ticket', + ticket: data.signin_token, + }); + + // If the sign-in was successful, set the session to active + if (signInAttempt.status === 'complete') { + await setActive({ + session: signInAttempt.createdSessionId, + }); + + // Get user organization info from Clerk session + try { + const token = await clerk.session?.getToken(); + const user = clerk.user; + console.log('Clerk user after sign-in:', user); + + // Store organization info in Tauri store + if (user?.organizationMemberships && user.organizationMemberships.length > 0) { + const orgMembership = user.organizationMemberships[0]; + const orgId = orgMembership.organization.id; + const orgName = orgMembership.organization.name; + + console.log('Found organization from Clerk:', { orgId, orgName }); + + try { + const store = await Store.load('app-store.json'); + await store.set('organization_id', orgId); + await store.set('organization_name', orgName); + await store.save(); + + console.log('Successfully stored organization info:', { orgId, orgName }); + } catch (storeError) { + console.error('Failed to store organization info:', storeError); + } + } else { + console.warn('No organization memberships found for user'); + } + } catch (orgError) { + console.error('Error retrieving organization info:', orgError); + } + + // Call finish_enrollment API with device fingerprint + try { + // get the token + const token = await clerk.session?.getToken(); + console.log("Calling finish_enrollment") + const finishResponse = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/sync/finish_enrollment`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ + device_fingerprint: deviceInfo.device_fingerprint + }) + }); + console.log("finishResponse", finishResponse) + + if (!finishResponse.ok) { + console.warn('Failed to complete enrollment finalization:', finishResponse.status); + } + } catch (finishError) { + console.warn('Error calling finish_enrollment:', finishError); + } + + toast.success('Successfully signed in!', { id: 'sign-in-success' }); + // Navigation will happen via the useEffect above when user state updates + } else { + console.error('Sign-in attempt not complete:', signInAttempt); + toast.error('Sign-in incomplete. Please try again.', { id: 'sign-in-incomplete' }); + setIsSigningIn(false); + } + } catch (error) { + console.error('Error signing in with token:', error); + toast.error('Failed to sign in with token', { id: 'sign-in-error' }); + setIsSigningIn(false); + } + } else if (data.signin_token && !signIn) { + console.error('Clerk signIn not available - Clerk may not be properly initialized'); + toast.error('Authentication not ready. Please refresh the page.', { id: 'clerk-not-ready' }); + } } } catch (error) { console.error('Error polling enrollment status:', error); @@ -129,12 +265,12 @@ function RouteComponent() { const handleOpenEnrollPage = async () => { try { - await openUrl("https://labric.co/enroll"); + await openUrl("https://platform.labric.co/enroll"); // For now, just show a toast since we don't have the Tauri API setup - toast.info('Would open labric.co/enroll'); + toast.info('Would open labric.co/enroll', {id: 'enrollment-page-info'}); } catch (err) { console.error('Failed to open enrollment page:', err); - toast.error('Failed to open enrollment page'); + toast.error('Failed to open enrollment page', {id: 'enrollment-page-error'}); } }; @@ -148,7 +284,7 @@ function RouteComponent() { await fetchEnrollmentCode(info); } catch (error) { console.error("Failed to get device info:", error); - toast.error('Failed to get device information'); + toast.error('Failed to get device information', {id: 'device-info-error'}); // Fallback device info const fallbackInfo: DeviceInfo = { hostname: "Unknown", @@ -170,27 +306,43 @@ function RouteComponent() { }, []); useEffect(() => { - // Set up interval to generate new code every 30 seconds + // Set up interval to generate new code every 120 seconds if (!deviceInfo || isEnrolled) return; const interval = setInterval(async () => { await fetchEnrollmentCode(deviceInfo); - toast.success('New enrollment code generated'); - }, 30000); + toast.success('New enrollment code generated', { id: 'enrollment-code-generated' }); + }, 120000); return () => clearInterval(interval); }, [deviceInfo, isEnrolled]); useEffect(() => { // Set up polling for enrollment status every second - if (!deviceInfo || isEnrolled) return; + if (!deviceInfo || isEnrolled || !clerk.loaded) return; const interval = setInterval(async () => { await pollEnrollment(deviceInfo); }, 1000); return () => clearInterval(interval); - }, [deviceInfo, isEnrolled]); + }, [deviceInfo, isEnrolled, clerk.loaded]); + + // Show loading state while Clerk is initializing + if (!clerk.loaded) { + return ( +
+
+ Labric Sync +

Labric Sync

+
+
+
+

Initializing...

+
+
+ ); + } return (
@@ -199,7 +351,12 @@ function RouteComponent() {

Labric Sync

- {isEnrolled ? ( + {isSigningIn ? ( +
+

Signing you in...

+
+
+ ) : isEnrolled ? (

Device Enrolled!

{organizationName}

@@ -208,7 +365,7 @@ function RouteComponent() { )}
- {!isEnrolled && ( + {!isEnrolled && !isSigningIn && (

Enter this code at{" "}