diff --git a/boilerplate/frontend/supertokens-react-custom/.gitignore b/boilerplate/frontend/supertokens-react-custom/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/boilerplate/frontend/supertokens-react-custom/README.md b/boilerplate/frontend/supertokens-react-custom/README.md
new file mode 100644
index 00000000..b6897e3f
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/README.md
@@ -0,0 +1,50 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
+
+- Configure the top-level `parserOptions` property like this:
+
+```js
+export default tseslint.config({
+ languageOptions: {
+ // other options...
+ parserOptions: {
+ project: ["./tsconfig.node.json", "./tsconfig.app.json"],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+});
+```
+
+- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
+- Optionally add `...tseslint.configs.stylisticTypeChecked`
+- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
+
+```js
+// eslint.config.js
+import react from "eslint-plugin-react";
+
+export default tseslint.config({
+ // Set the react version
+ settings: { react: { version: "18.3" } },
+ plugins: {
+ // Add the react plugin
+ react,
+ },
+ rules: {
+ // other rules...
+ // Enable its recommended rules
+ ...react.configs.recommended.rules,
+ ...react.configs["jsx-runtime"].rules,
+ },
+});
+```
diff --git a/boilerplate/frontend/supertokens-react-custom/eslint.config.js b/boilerplate/frontend/supertokens-react-custom/eslint.config.js
new file mode 100644
index 00000000..e1770e4a
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/eslint.config.js
@@ -0,0 +1,25 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ { ignores: ["dist"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
+ },
+ }
+);
diff --git a/boilerplate/frontend/supertokens-react-custom/index.html b/boilerplate/frontend/supertokens-react-custom/index.html
new file mode 100644
index 00000000..1f2712c1
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Supertokens Demo App | React
+
+
+
+
+
+
diff --git a/boilerplate/frontend/supertokens-react-custom/package.json b/boilerplate/frontend/supertokens-react-custom/package.json
new file mode 100644
index 00000000..b3d7cb9c
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "start": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-icons": "^5.3.0",
+ "react-loader-spinner": "^6.1.6",
+ "react-router-dom": "^6.26.2",
+ "react-toastify": "^10.0.5",
+ "supertokens-web-js": "^0.13.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.11.1",
+ "@types/node": "^22.7.5",
+ "@types/react": "^18.3.10",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.2",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.11.1",
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+ "eslint-plugin-react-refresh": "^0.4.12",
+ "globals": "^15.9.0",
+ "postcss": "^8.4.47",
+ "tailwindcss": "^3.4.13",
+ "typescript": "^5.5.3",
+ "typescript-eslint": "^8.7.0",
+ "vite": "^5.4.8"
+ }
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/postcss.config.js b/boilerplate/frontend/supertokens-react-custom/postcss.config.js
new file mode 100644
index 00000000..49c0612d
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/boilerplate/frontend/supertokens-react-custom/public/favicon.ico b/boilerplate/frontend/supertokens-react-custom/public/favicon.ico
new file mode 100644
index 00000000..d14dbd1d
Binary files /dev/null and b/boilerplate/frontend/supertokens-react-custom/public/favicon.ico differ
diff --git a/boilerplate/frontend/supertokens-react-custom/public/logo.webp b/boilerplate/frontend/supertokens-react-custom/public/logo.webp
new file mode 100644
index 00000000..0ac5bd44
Binary files /dev/null and b/boilerplate/frontend/supertokens-react-custom/public/logo.webp differ
diff --git a/boilerplate/frontend/supertokens-react-custom/src/App.tsx b/boilerplate/frontend/supertokens-react-custom/src/App.tsx
new file mode 100644
index 00000000..a298fb5b
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/App.tsx
@@ -0,0 +1,33 @@
+import SuperTokens from "supertokens-web-js";
+import { superTokensConfig } from "./config";
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+import HomePage from "@/pages/Home";
+import AuthRoutes from "@/pages/Auth";
+import DashboardPage from "@/pages/Dashboard";
+import Protected from "@/auth/Protected";
+
+SuperTokens.init(superTokensConfig);
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ },
+ {
+ path: "/dashboard",
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ ],
+ },
+ ...AuthRoutes,
+]);
+
+function App() {
+ return ;
+}
+
+export default App;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/auth/Protected.tsx b/boilerplate/frontend/supertokens-react-custom/src/auth/Protected.tsx
new file mode 100644
index 00000000..a93c8dcc
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/auth/Protected.tsx
@@ -0,0 +1,34 @@
+import { useEffect, useState } from "react";
+import { Outlet, useLocation, useNavigate } from "react-router-dom";
+import Session from "supertokens-web-js/recipe/session";
+
+export default function Protected() {
+ const [isSessionStatusLoading, setIsSessionStatusLoading] = useState(true);
+ const navigate = useNavigate();
+ const location = useLocation();
+ useEffect(() => {
+ async function checkSession() {
+ setIsSessionStatusLoading(true);
+ try {
+ const isSessionValid = await Session.doesSessionExist();
+ if (!isSessionValid) {
+ navigate(`/authenticate?redirectTo=${location.pathname}`);
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setIsSessionStatusLoading(false);
+ }
+ }
+ checkSession();
+ }, []);
+
+ if (isSessionStatusLoading) {
+ return Loading...
;
+ }
+ return (
+
+
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/components/Footer.tsx b/boilerplate/frontend/supertokens-react-custom/src/components/Footer.tsx
new file mode 100644
index 00000000..fddf4d5b
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/components/Footer.tsx
@@ -0,0 +1,7 @@
+interface FooterProps {
+ title?: string;
+}
+
+export default function Footer({ title }: FooterProps) {
+ return ;
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/components/Header.tsx b/boilerplate/frontend/supertokens-react-custom/src/components/Header.tsx
new file mode 100644
index 00000000..bcb2e631
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/components/Header.tsx
@@ -0,0 +1,11 @@
+interface HeaderProps {
+ title?: string;
+}
+export default function Header({ title }: HeaderProps) {
+ return (
+
+
+ {title &&
{title} }
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/components/Input.tsx b/boilerplate/frontend/supertokens-react-custom/src/components/Input.tsx
new file mode 100644
index 00000000..0720fc97
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/components/Input.tsx
@@ -0,0 +1,9 @@
+import React, { forwardRef } from "react";
+
+type InputProps = React.InputHTMLAttributes;
+
+const Input = forwardRef((props, ref) => {
+ return ;
+});
+
+export default Input;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/components/Spinner.tsx b/boilerplate/frontend/supertokens-react-custom/src/components/Spinner.tsx
new file mode 100644
index 00000000..6396b4ab
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/components/Spinner.tsx
@@ -0,0 +1,17 @@
+import { ColorRing } from "react-loader-spinner";
+
+export default function Spinner() {
+ return (
+
+
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/config.ts b/boilerplate/frontend/supertokens-react-custom/src/config.ts
new file mode 100644
index 00000000..3a41f738
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/config.ts
@@ -0,0 +1,25 @@
+import EmailPassword from "supertokens-web-js/recipe/emailpassword";
+import Session from "supertokens-web-js/recipe/session";
+import Passwordless from "supertokens-web-js/recipe/passwordless";
+import ThirdParty from "supertokens-web-js/recipe/thirdparty";
+
+export function getApiDomain() {
+ const apiPort = import.meta.env.VITE_APP_API_PORT || 3001;
+ const apiUrl = import.meta.env.VITE_APP_API_URL || `http://localhost:${apiPort}`;
+ return apiUrl;
+}
+
+export function getWebsiteDomain() {
+ const websitePort = import.meta.env.VITE_APP_WEBSITE_PORT || 5173;
+ const websiteUrl = import.meta.env.VITE_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
+ return websiteUrl;
+}
+
+export const superTokensConfig = {
+ appInfo: {
+ apiDomain: "http://localhost:3001",
+ apiBasePath: "/auth",
+ appName: "Custom UI Demo",
+ },
+ recipeList: [Session.init(), EmailPassword.init(), Passwordless.init(), ThirdParty.init()],
+};
diff --git a/boilerplate/frontend/supertokens-react-custom/src/config/emailpassword.tsx b/boilerplate/frontend/supertokens-react-custom/src/config/emailpassword.tsx
new file mode 100644
index 00000000..ecaa03a3
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/config/emailpassword.tsx
@@ -0,0 +1,23 @@
+import EmailPassword from "supertokens-web-js/recipe/emailpassword";
+import Session from "supertokens-web-js/recipe/session";
+
+export function getApiDomain() {
+ const apiPort = import.meta.env.VITE_APP_API_PORT || 3001;
+ const apiUrl = import.meta.env.VITE_APP_API_URL || `http://localhost:${apiPort}`;
+ return apiUrl;
+}
+
+export function getWebsiteDomain() {
+ const websitePort = import.meta.env.VITE_APP_WEBSITE_PORT || 3000;
+ const websiteUrl = import.meta.env.VITE_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
+ return websiteUrl;
+}
+
+export const superTokensConfig = {
+ appInfo: {
+ apiBasePath: "/auth",
+ appName: "SuperTokens Demo App",
+ apiDomain: getApiDomain(),
+ },
+ recipeList: [Session.init(), EmailPassword.init()],
+};
diff --git a/boilerplate/frontend/supertokens-react-custom/src/config/passwordless.tsx b/boilerplate/frontend/supertokens-react-custom/src/config/passwordless.tsx
new file mode 100644
index 00000000..a540bb1a
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/config/passwordless.tsx
@@ -0,0 +1,23 @@
+import Session from "supertokens-web-js/recipe/session";
+import Passwordless from "supertokens-web-js/recipe/passwordless";
+
+export function getApiDomain() {
+ const apiPort = import.meta.env.VITE_APP_API_PORT || 3001;
+ const apiUrl = import.meta.env.VITE_APP_API_URL || `http://localhost:${apiPort}`;
+ return apiUrl;
+}
+
+export function getWebsiteDomain() {
+ const websitePort = import.meta.env.VITE_APP_WEBSITE_PORT || 3000;
+ const websiteUrl = import.meta.env.VITE_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
+ return websiteUrl;
+}
+
+export const superTokensConfig = {
+ appInfo: {
+ apiBasePath: "/auth",
+ appName: "SuperTokens Demo App",
+ apiDomain: getApiDomain(),
+ },
+ recipeList: [Session.init(), Passwordless.init()],
+};
diff --git a/boilerplate/frontend/supertokens-react-custom/src/config/thirdparty.tsx b/boilerplate/frontend/supertokens-react-custom/src/config/thirdparty.tsx
new file mode 100644
index 00000000..299f3447
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/config/thirdparty.tsx
@@ -0,0 +1,23 @@
+import Session from "supertokens-web-js/recipe/session";
+import ThirdParty from "supertokens-web-js/recipe/thirdparty";
+
+export function getApiDomain() {
+ const apiPort = import.meta.env.VITE_APP_API_PORT || 3001;
+ const apiUrl = import.meta.env.VITE_APP_API_URL || `http://localhost:${apiPort}`;
+ return apiUrl;
+}
+
+export function getWebsiteDomain() {
+ const websitePort = import.meta.env.VITE_APP_WEBSITE_PORT || 3000;
+ const websiteUrl = import.meta.env.VITE_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
+ return websiteUrl;
+}
+
+export const superTokensConfig = {
+ appInfo: {
+ apiBasePath: "/auth",
+ appName: "SuperTokens Demo App",
+ apiDomain: getApiDomain(),
+ },
+ recipeList: [Session.init(), ThirdParty.init()],
+};
diff --git a/boilerplate/frontend/supertokens-react-custom/src/config/thirdpartyemailpassword.tsx b/boilerplate/frontend/supertokens-react-custom/src/config/thirdpartyemailpassword.tsx
new file mode 100644
index 00000000..16a411c4
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/config/thirdpartyemailpassword.tsx
@@ -0,0 +1,24 @@
+import Session from "supertokens-web-js/recipe/session";
+import ThirdParty from "supertokens-web-js/recipe/thirdparty";
+import EmailPassword from "supertokens-web-js/recipe/emailpassword";
+
+export function getApiDomain() {
+ const apiPort = import.meta.env.VITE_APP_API_PORT || 3001;
+ const apiUrl = import.meta.env.VITE_APP_API_URL || `http://localhost:${apiPort}`;
+ return apiUrl;
+}
+
+export function getWebsiteDomain() {
+ const websitePort = import.meta.env.VITE_APP_WEBSITE_PORT || 3000;
+ const websiteUrl = import.meta.env.VITE_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
+ return websiteUrl;
+}
+
+export const superTokensConfig = {
+ appInfo: {
+ apiBasePath: "/auth",
+ appName: "SuperTokens Demo App",
+ apiDomain: getApiDomain(),
+ },
+ recipeList: [Session.init(), ThirdParty.init(), EmailPassword.init()],
+};
diff --git a/boilerplate/frontend/supertokens-react-custom/src/config/thirdpartypasswordless.tsx b/boilerplate/frontend/supertokens-react-custom/src/config/thirdpartypasswordless.tsx
new file mode 100644
index 00000000..60250b35
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/config/thirdpartypasswordless.tsx
@@ -0,0 +1,24 @@
+import Session from "supertokens-web-js/recipe/session";
+import ThirdParty from "supertokens-web-js/recipe/thirdparty";
+import Passwordless from "supertokens-web-js/recipe/passwordless";
+
+export function getApiDomain() {
+ const apiPort = import.meta.env.VITE_APP_API_PORT || 3001;
+ const apiUrl = import.meta.env.VITE_APP_API_URL || `http://localhost:${apiPort}`;
+ return apiUrl;
+}
+
+export function getWebsiteDomain() {
+ const websitePort = import.meta.env.VITE_APP_WEBSITE_PORT || 3000;
+ const websiteUrl = import.meta.env.VITE_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
+ return websiteUrl;
+}
+
+export const superTokensConfig = {
+ appInfo: {
+ apiBasePath: "/auth",
+ appName: "SuperTokens Demo App",
+ apiDomain: getApiDomain(),
+ },
+ recipeList: [Session.init(), ThirdParty.init(), Passwordless.init()],
+};
diff --git a/boilerplate/frontend/supertokens-react-custom/src/hooks/useSessionInfo.ts b/boilerplate/frontend/supertokens-react-custom/src/hooks/useSessionInfo.ts
new file mode 100644
index 00000000..82922240
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/hooks/useSessionInfo.ts
@@ -0,0 +1,37 @@
+import { useEffect, useState } from "react";
+import Session from "supertokens-web-js/recipe/session";
+
+type UserLoggedIn =
+ | {
+ isLoading: true;
+ }
+ | {
+ isLoading: false;
+ isLoggedIn: boolean;
+ };
+
+function useSessionInfo() {
+ const [isUserLoggedIn, setIsUserLoggedIn] = useState({
+ isLoading: true,
+ });
+ useEffect(() => {
+ async function checkSession() {
+ try {
+ const isSessionValid = await Session.doesSessionExist();
+ setIsUserLoggedIn({
+ isLoading: false,
+ isLoggedIn: isSessionValid,
+ });
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ checkSession();
+ }, []);
+ return {
+ sessionExists: isUserLoggedIn,
+ };
+}
+
+export default useSessionInfo;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/index.css b/boilerplate/frontend/supertokens-react-custom/src/index.css
new file mode 100644
index 00000000..76e7ab84
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/index.css
@@ -0,0 +1,7 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ background-color: #121212;
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/main.tsx b/boilerplate/frontend/supertokens-react-custom/src/main.tsx
new file mode 100644
index 00000000..ef2f2fe7
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/main.tsx
@@ -0,0 +1,13 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App.tsx";
+import "./index.css";
+import { ToastContainer } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+);
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/emailpassword/LoginAndRegister.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/emailpassword/LoginAndRegister.tsx
new file mode 100644
index 00000000..71ed630f
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/emailpassword/LoginAndRegister.tsx
@@ -0,0 +1,226 @@
+import { useEffect, useState } from "react";
+import useSessionInfo from "@/hooks/useSessionInfo";
+import { useNavigate } from "react-router-dom";
+import Header from "@/components/Header";
+import Footer from "@/components/Footer";
+import Input from "@/components/Input";
+import { toast } from "react-toastify";
+import { signIn, signUp } from "supertokens-web-js/recipe/emailpassword";
+import STGeneralError from "supertokens-web-js/utils/error";
+import Spinner from "@/components/Spinner";
+
+type AuthenticationType = "login" | "registration";
+interface FormInputState {
+ email: string;
+ password: string;
+ confirmPassword?: string;
+}
+
+const initialFormInputState: FormInputState = {
+ email: "",
+ password: "",
+ confirmPassword: "",
+};
+
+interface LoginAndRegisterProps {
+ showHeader?: boolean;
+ showFooter?: boolean;
+ rootStyle?: React.CSSProperties;
+}
+
+export default function LoginAndRegister({ showFooter = true, showHeader = true, rootStyle }: LoginAndRegisterProps) {
+ /**
+ * The authentication type state. This would be used to switch between login and registration.
+ */
+ const [authenticationType, setAuthenticationType] = useState("login");
+
+ const navigation = useNavigate();
+ const [formInputState, setFormInputState] = useState(initialFormInputState);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { sessionExists } = useSessionInfo();
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ if (isLoading) return;
+ if (authenticationType === "registration" && formInputState.password !== formInputState.confirmPassword) {
+ toast.error("Passwords do not match");
+ return;
+ }
+ setIsLoading(true);
+ try {
+ /**
+ * User SignIn/Login flow
+ */
+ if (authenticationType === "login") {
+ const response = await signIn({
+ formFields: [
+ {
+ id: "email",
+ value: formInputState.email,
+ },
+ {
+ id: "password",
+ value: formInputState.password,
+ },
+ ],
+ });
+
+ if (response.status === "FIELD_ERROR") {
+ for (const formField of response.formFields) {
+ throw new Error(formField.error);
+ }
+ } else if (response.status === "WRONG_CREDENTIALS_ERROR") {
+ throw new Error("Email password combination is incorrect.");
+ } else if (response.status === "SIGN_IN_NOT_ALLOWED") {
+ throw new Error(response.reason);
+ } else {
+ toast.success("Login Successful");
+ navigation("/dashboard");
+ return;
+ }
+ }
+
+ /**
+ * User SignUp/Registration flow
+ */
+ const response = await signUp({
+ formFields: [
+ {
+ id: "email",
+ value: formInputState.email,
+ },
+ {
+ id: "password",
+ value: formInputState.password,
+ },
+ ],
+ });
+ if (response.status === "FIELD_ERROR") {
+ for (const formField of response.formFields) {
+ throw new Error(formField.error);
+ }
+ } else if (response.status === "SIGN_UP_NOT_ALLOWED") {
+ throw new Error(response.reason);
+ } else {
+ toast.success("Registration Successful. Login to continue.");
+ setAuthenticationType("login");
+ }
+ } catch (error) {
+ console.error(error);
+ const errorMessage = (error as STGeneralError | Error)?.message || "Oops! Something went wrong.";
+ toast.error(errorMessage);
+ } finally {
+ /**
+ * Reset the form input state and loading state
+ */
+ setFormInputState(initialFormInputState);
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!sessionExists.isLoading && sessionExists.isLoggedIn) {
+ navigation("/dashboard");
+ }
+ }, [sessionExists]);
+
+ useEffect(() => {
+ setFormInputState(initialFormInputState);
+ }, [authenticationType]);
+
+ if (sessionExists.isLoading) {
+ return ;
+ }
+ return (
+
+
+ {showHeader && }
+
+
+ {showFooter &&
}
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/emailpassword/index.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/emailpassword/index.tsx
new file mode 100644
index 00000000..a131db2c
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/emailpassword/index.tsx
@@ -0,0 +1,11 @@
+import { RouteObject } from "react-router-dom";
+import LoginAndRegister from "./LoginAndRegister";
+
+const AuthRoutes: RouteObject[] = [
+ {
+ path: "/authenticate",
+ element: ,
+ },
+];
+
+export default AuthRoutes;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/index.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/index.tsx
new file mode 100644
index 00000000..62cd549a
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/index.tsx
@@ -0,0 +1,8 @@
+/**
+ * This is a dummy file
+ */
+import { RouteObject } from "react-router-dom";
+
+const AuthRoutes: RouteObject[] = [];
+
+export default AuthRoutes;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/passwordless/PasswordlessSignIn.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/passwordless/PasswordlessSignIn.tsx
new file mode 100644
index 00000000..31ef7450
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/passwordless/PasswordlessSignIn.tsx
@@ -0,0 +1,208 @@
+import Footer from "@/components/Footer";
+import Header from "@/components/Header";
+import Input from "@/components/Input";
+import Spinner from "@/components/Spinner";
+import useSessionInfo from "@/hooks/useSessionInfo";
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { toast } from "react-toastify";
+import { createCode, resendCode, clearLoginAttemptInfo, consumeCode } from "supertokens-web-js/recipe/passwordless";
+import STGeneralError from "supertokens-web-js/utils/error";
+
+enum SCREEN {
+ EMAIL,
+ OTP,
+}
+
+interface PasswordlessSignInProps {
+ showHeader?: boolean;
+ showFooter?: boolean;
+ rootStyle?: React.CSSProperties;
+}
+
+export default function PasswordlessSignIn({
+ showFooter = true,
+ showHeader = true,
+ rootStyle,
+}: PasswordlessSignInProps) {
+ const [email, setEmail] = useState("");
+ const [otp, setOtp] = useState("");
+ const [screen, setScreen] = useState(SCREEN.EMAIL);
+ const [isLoading, setIsLoading] = useState(false);
+ const navigation = useNavigate();
+ const { sessionExists } = useSessionInfo();
+
+ const handleResendOTP = async () => {
+ setIsLoading(true);
+ try {
+ const response = await resendCode();
+
+ if (response.status === "RESTART_FLOW_ERROR") {
+ // this can happen if the user has already successfully logged in into
+ // another device whilst also trying to login to this one.
+
+ // we clear the login attempt info that was added when the createCode function
+ // was called - so that if the user does a page reload, they will now see the
+ // enter email / phone UI again.
+ await clearLoginAttemptInfo();
+ toast.error("Login failed. Please try again");
+ return;
+ }
+ toast.success("OTP Resent Successfully");
+ return;
+ } catch (error) {
+ console.error(error);
+ const errorMessage = (error as STGeneralError | Error)?.message || "Oops! Something went wrong.";
+ toast.error(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ setIsLoading(true);
+ try {
+ /**
+ * Email Screen
+ */
+ if (screen === SCREEN.EMAIL) {
+ const response = await createCode({
+ email,
+ });
+ /**
+ * For phone number, use this:
+
+ let response = await createPasswordlessCode({
+ phoneNumber: "+1234567890"
+ });
+
+ */
+
+ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") {
+ // the reason string is a user friendly message
+ // about what went wrong. It can also contain a support code which users
+ // can tell you so you know why their sign in / up was not allowed.
+ throw new Error(response.reason);
+ }
+ setScreen(SCREEN.OTP);
+ toast.success("OTP Sent Successfully");
+ return;
+ }
+
+ /**
+ * OTP Screen
+ */
+ const response = await consumeCode({
+ userInputCode: otp,
+ });
+
+ if (response.status === "OK") {
+ // we clear the login attempt info that was added when the createCode function
+ // was called since the login was successful.
+ await clearLoginAttemptInfo();
+ if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) {
+ toast.success("New User Created. Please Link your email and password");
+ } else {
+ toast.success("Logged In Successfully");
+ }
+ navigation("/dashboard");
+ return;
+ } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") {
+ throw new Error(
+ "Wrong OTP! Please try again. Number of attempts left: " +
+ (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount)
+ );
+ } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") {
+ throw new Error("Old OTP entered. Please regenerate a new one and try again");
+ } else {
+ // this can happen if the user tried an incorrect OTP too many times.
+ // or if it was denied due to security reasons in case of automatic account linking
+
+ // we clear the login attempt info that was added when the createCode function
+ // was called - so that if the user does a page reload, they will now see the
+ // enter email / phone UI again.
+ await clearLoginAttemptInfo();
+ setScreen(SCREEN.EMAIL);
+ throw new Error("Login failed. Please try again");
+ }
+ } catch (error: unknown) {
+ console.error(error);
+ const errorMessage = (error as STGeneralError | Error)?.message || "Oops! Something went wrong.";
+ toast.error(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ useEffect(() => {
+ if (!sessionExists.isLoading && sessionExists.isLoggedIn) {
+ navigation("/dashboard");
+ }
+ }, [sessionExists]);
+
+ if (sessionExists.isLoading) {
+ return ;
+ }
+
+ return (
+
+
+ {showHeader && }
+
+
+ {showFooter &&
}
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/passwordless/index.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/passwordless/index.tsx
new file mode 100644
index 00000000..47ac9a4a
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/passwordless/index.tsx
@@ -0,0 +1,11 @@
+import { RouteObject } from "react-router-dom";
+import PasswordlessSignIn from "./PasswordlessSignIn";
+
+const AuthRoutes: RouteObject[] = [
+ {
+ path: "/authenticate",
+ element: ,
+ },
+];
+
+export default AuthRoutes;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/CallbackHandler.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/CallbackHandler.tsx
new file mode 100644
index 00000000..a962290c
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/CallbackHandler.tsx
@@ -0,0 +1,53 @@
+import { useEffect, useRef } from "react";
+import { useNavigate } from "react-router-dom";
+import STGeneralError from "supertokens-web-js/utils/error";
+import { signInAndUp } from "supertokens-web-js/recipe/thirdparty";
+import { toast } from "react-toastify";
+import Spinner from "@/components/Spinner";
+
+function CallbackHandler() {
+ const alreadyExecuted = useRef(false);
+ const navigate = useNavigate();
+ useEffect(() => {
+ async function handleCallback() {
+ try {
+ const response = await signInAndUp();
+
+ if (response.status === "OK") {
+ if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) {
+ toast.success("Account created successfully");
+ } else {
+ toast.success("Logged in successfully");
+ }
+ navigate("/dashboard");
+ return;
+ } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") {
+ // the reason string is a user friendly message
+ // about what went wrong. It can also contain a support code which users
+ // can tell you so you know why their sign in / up was not allowed.
+ throw new Error(response.reason);
+ } else {
+ // SuperTokens requires that the third party provider
+ // gives an email for the user. If that's not the case, sign up / in
+ // will fail.
+
+ // As a hack to solve this, you can override the backend functions to create a fake email for the user.
+
+ throw new Error("No email provided by social login. Please use another form of login");
+ }
+ } catch (error: unknown) {
+ console.error(error);
+ const errorMessage = (error as STGeneralError | Error)?.message || "Oops! Something went wrong.";
+ toast.error(errorMessage);
+ navigate("/authenticate");
+ }
+ }
+ if (!alreadyExecuted.current) {
+ handleCallback();
+ alreadyExecuted.current = true;
+ }
+ }, []);
+ return ;
+}
+
+export default CallbackHandler;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/SocialLogin.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/SocialLogin.tsx
new file mode 100644
index 00000000..5e57d4e0
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/SocialLogin.tsx
@@ -0,0 +1,85 @@
+import Footer from "@/components/Footer";
+import Header from "@/components/Header";
+import useSessionInfo from "@/hooks/useSessionInfo";
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { SUPPORTED_PROVIDERS } from "./constants";
+import { getWebsiteDomain } from "@/config";
+import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty";
+import STGeneralError from "supertokens-web-js/utils/error";
+import { toast } from "react-toastify";
+import Spinner from "@/components/Spinner";
+
+interface SocialLoginProps {
+ showHeader?: boolean;
+ showFooter?: boolean;
+ rootStyle?: React.CSSProperties;
+ view?: "full" | "compact";
+}
+
+export default function SocialLogin({
+ showFooter = true,
+ showHeader = true,
+ rootStyle,
+ view = "full",
+}: SocialLoginProps) {
+ const { sessionExists } = useSessionInfo();
+ const [isLoading, setIsLoading] = useState(false);
+ const navigation = useNavigate();
+
+ const handleLoginRequest = async (provider: string) => {
+ setIsLoading(true);
+ try {
+ const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({
+ thirdPartyId: provider,
+ frontendRedirectURI: `${getWebsiteDomain()}/authenticate/callback/${provider}`,
+ });
+ window.location.assign(authUrl);
+ } catch (error: unknown) {
+ console.error(error);
+ const errorMessage = (error as STGeneralError | Error)?.message || "Oops! Something went wrong.";
+ toast.error(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!sessionExists.isLoading && sessionExists.isLoggedIn) {
+ navigation("/dashboard");
+ }
+ }, [sessionExists]);
+
+ if (sessionExists.isLoading) {
+ return ;
+ }
+
+ return (
+
+
+ {showHeader &&
}
+
+ {SUPPORTED_PROVIDERS.map((provider, index) => (
+ handleLoginRequest(provider.key)}
+ >
+ {provider.icon}
+ {view === "full" && `Sign In With ${provider.name}`}
+
+ ))}
+
+
+ {showFooter &&
}
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/constants.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/constants.tsx
new file mode 100644
index 00000000..687d0665
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/constants.tsx
@@ -0,0 +1,33 @@
+import { FaGoogle, FaGithub, FaApple } from "react-icons/fa";
+import { RiTwitterXLine } from "react-icons/ri";
+
+interface ISupportedProviders {
+ key: string;
+ name: string;
+ icon: React.ReactNode;
+}
+
+const SUPPORTED_PROVIDERS: ISupportedProviders[] = [
+ {
+ key: "google",
+ name: "Google",
+ icon: ,
+ },
+ {
+ key: "github",
+ name: "Github",
+ icon: ,
+ },
+ {
+ key: "twitter",
+ name: "X(Twitter)",
+ icon: ,
+ },
+ {
+ key: "apple",
+ name: "Apple",
+ icon: ,
+ },
+];
+
+export { SUPPORTED_PROVIDERS };
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/index.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/index.tsx
new file mode 100644
index 00000000..3b489cdb
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdparty/index.tsx
@@ -0,0 +1,16 @@
+import { RouteObject } from "react-router-dom";
+import SocialLogin from "./SocialLogin";
+import CallbackHandler from "./CallbackHandler";
+
+const AuthRoutes: RouteObject[] = [
+ {
+ path: "/authenticate",
+ element: ,
+ },
+ {
+ path: "/authenticate/callback/:provider",
+ element: ,
+ },
+];
+
+export default AuthRoutes;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/SocialAndEmailPassword.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/SocialAndEmailPassword.tsx
new file mode 100644
index 00000000..b6e511e3
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/SocialAndEmailPassword.tsx
@@ -0,0 +1,19 @@
+import Social from "./thirdparty/SocialLogin";
+import LoginAndRegister from "./emailpassword/LoginAndRegister";
+import Header from "@/components/Header";
+import Footer from "@/components/Footer";
+
+export default function SocialAndPasswordless() {
+ return (
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/emailpassword/LoginAndRegister.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/emailpassword/LoginAndRegister.tsx
new file mode 100644
index 00000000..9c06f52a
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/emailpassword/LoginAndRegister.tsx
@@ -0,0 +1,12 @@
+/**
+ * This is a dummy component and would be replaced during runtime
+ */
+interface LoginAndRegisterProps {
+ showHeader?: boolean;
+ showFooter?: boolean;
+ rootStyle?: React.CSSProperties;
+}
+
+const LoginAndRegister: React.FC = () => <>>;
+
+export default LoginAndRegister;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/index.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/index.tsx
new file mode 100644
index 00000000..14c3ffe9
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/index.tsx
@@ -0,0 +1,15 @@
+import { RouteObject } from "react-router-dom";
+import SocialAndEmailPassword from "./SocialAndEmailPassword";
+import CallbackHandler from "./thirdparty/CallbackHandler";
+const AuthRoutes: RouteObject[] = [
+ {
+ path: "/authenticate",
+ element: ,
+ },
+ {
+ path: "/authenticate/callback/:provider",
+ element: ,
+ },
+];
+
+export default AuthRoutes;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/thirdparty/CallbackHandler.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/thirdparty/CallbackHandler.tsx
new file mode 100644
index 00000000..e518dea5
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/thirdparty/CallbackHandler.tsx
@@ -0,0 +1,9 @@
+/**
+ * This is a dummy component which would be replaced during runtime
+ */
+
+function CallbackHandler() {
+ return <>>;
+}
+
+export default CallbackHandler;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/thirdparty/SocialLogin.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/thirdparty/SocialLogin.tsx
new file mode 100644
index 00000000..5034ea5f
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartyemailpassword/thirdparty/SocialLogin.tsx
@@ -0,0 +1,14 @@
+/**
+ * This is a dummy component which would be replaced during runtime
+ */
+
+interface SocialLoginProps {
+ showHeader?: boolean;
+ showFooter?: boolean;
+ rootStyle?: React.CSSProperties;
+ view?: "full" | "compact";
+}
+
+const SocialLogin: React.FC = () => <>>;
+
+export default SocialLogin;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/SocialAndPasswordless.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/SocialAndPasswordless.tsx
new file mode 100644
index 00000000..feb51263
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/SocialAndPasswordless.tsx
@@ -0,0 +1,19 @@
+import Social from "./thirdparty/SocialLogin";
+import PasswordLess from "./passwordless/PasswordlessSignIn";
+import Header from "@/components/Header";
+import Footer from "@/components/Footer";
+
+export default function SocialAndPasswordless() {
+ return (
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/index.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/index.tsx
new file mode 100644
index 00000000..03374c59
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/index.tsx
@@ -0,0 +1,15 @@
+import { RouteObject } from "react-router-dom";
+import SocialAndPasswordless from "./SocialAndPasswordless";
+import CallbackHandler from "./thirdparty/CallbackHandler";
+const AuthRoutes: RouteObject[] = [
+ {
+ path: "/authenticate",
+ element: ,
+ },
+ {
+ path: "/authenticate/callback/:provider",
+ element: ,
+ },
+];
+
+export default AuthRoutes;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/passwordless/PasswordlessSignIn.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/passwordless/PasswordlessSignIn.tsx
new file mode 100644
index 00000000..58a5ba14
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/passwordless/PasswordlessSignIn.tsx
@@ -0,0 +1,13 @@
+/**
+ * This is a dummy component which would be replaced during runtime
+ */
+
+interface PasswordlessSignInProps {
+ showHeader?: boolean;
+ showFooter?: boolean;
+ rootStyle?: React.CSSProperties;
+}
+
+const PasswordlessSignIn: React.FC = () => <>>;
+
+export default PasswordlessSignIn;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/thirdparty/CallbackHandler.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/thirdparty/CallbackHandler.tsx
new file mode 100644
index 00000000..e518dea5
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/thirdparty/CallbackHandler.tsx
@@ -0,0 +1,9 @@
+/**
+ * This is a dummy component which would be replaced during runtime
+ */
+
+function CallbackHandler() {
+ return <>>;
+}
+
+export default CallbackHandler;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/thirdparty/SocialLogin.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/thirdparty/SocialLogin.tsx
new file mode 100644
index 00000000..5034ea5f
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Auth/thirdpartypasswordless/thirdparty/SocialLogin.tsx
@@ -0,0 +1,14 @@
+/**
+ * This is a dummy component which would be replaced during runtime
+ */
+
+interface SocialLoginProps {
+ showHeader?: boolean;
+ showFooter?: boolean;
+ rootStyle?: React.CSSProperties;
+ view?: "full" | "compact";
+}
+
+const SocialLogin: React.FC = () => <>>;
+
+export default SocialLogin;
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Dashboard/index.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Dashboard/index.tsx
new file mode 100644
index 00000000..4ba48440
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Dashboard/index.tsx
@@ -0,0 +1,27 @@
+import { useNavigate } from "react-router-dom";
+import { signOut } from "supertokens-web-js/recipe/session";
+
+export default function DashboardPage() {
+ return (
+
+
This is a protected page you are viewing
+
+
+ );
+}
+
+function SignOutButton() {
+ const navigate = useNavigate();
+ const handleClick = async () => {
+ await signOut();
+ navigate("/authenticate");
+ };
+ return (
+
+ Sign Out
+
+ );
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/pages/Home/index.tsx b/boilerplate/frontend/supertokens-react-custom/src/pages/Home/index.tsx
new file mode 100644
index 00000000..da942848
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/pages/Home/index.tsx
@@ -0,0 +1,10 @@
+import { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+
+export default function HomePage() {
+ const navigate = useNavigate();
+ useEffect(() => {
+ navigate("/dashboard");
+ }, []);
+ return <>>;
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/src/vite-env.d.ts b/boilerplate/frontend/supertokens-react-custom/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/boilerplate/frontend/supertokens-react-custom/tailwind.config.js b/boilerplate/frontend/supertokens-react-custom/tailwind.config.js
new file mode 100644
index 00000000..b10fc3f0
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/tailwind.config.js
@@ -0,0 +1,25 @@
+/** @type {import('tailwindcss').Config} */
+
+export default {
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ colors: {
+ "golden-bell": {
+ 50: "#fdf9ef",
+ 100: "#fbf0d9",
+ 200: "#f5ddb3",
+ 300: "#efc682",
+ 400: "#e7a550",
+ 500: "#e28d31",
+ 600: "#d37223",
+ 700: "#af591f",
+ 800: "#8c4720",
+ 900: "#713b1d",
+ 950: "#3d1d0d",
+ },
+ },
+ },
+ },
+ plugins: [],
+};
diff --git a/boilerplate/frontend/supertokens-react-custom/tsconfig.app.json b/boilerplate/frontend/supertokens-react-custom/tsconfig.app.json
new file mode 100644
index 00000000..34f4a1b7
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/tsconfig.json b/boilerplate/frontend/supertokens-react-custom/tsconfig.json
new file mode 100644
index 00000000..b69e28ef
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "forceConsistentCasingInFileNames": true,
+ "strict": true
+ }
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/tsconfig.node.json b/boilerplate/frontend/supertokens-react-custom/tsconfig.node.json
new file mode 100644
index 00000000..716bc288
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/tsconfig.node.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/boilerplate/frontend/supertokens-react-custom/vite.config.ts b/boilerplate/frontend/supertokens-react-custom/vite.config.ts
new file mode 100644
index 00000000..07227d53
--- /dev/null
+++ b/boilerplate/frontend/supertokens-react-custom/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import path from "path";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ },
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});
diff --git a/lib/build/config.js b/lib/build/config.js
index 451ce50e..9b7a9609 100644
--- a/lib/build/config.js
+++ b/lib/build/config.js
@@ -1,7 +1,11 @@
+import { UIBuildType } from "./types.js";
+import { FILTER_CHOICES_STRATEGY, filterChoices } from "./filterChoicesUtils.js";
import { validateFolderName } from "./userArgumentUtils.js";
import {
getDjangoPythonRunScripts,
+ getFrontendPromptMessage,
getPythonRunScripts,
+ getRecipePromptMessage,
mapOptionsToChoices,
shouldSkipBackendQuestion,
} from "./questionUtils.js";
@@ -20,7 +24,7 @@ export async function getFrontendOptions({ manager }) {
value: "react",
displayName: "React",
location: {
- main: "frontend/supertokens-react",
+ main: `frontend/supertokens-react`,
config: [{ finalConfig: "/src/config.tsx", configFiles: "/config" }],
},
script: {
@@ -28,6 +32,18 @@ export async function getFrontendOptions({ manager }) {
run: [`${manager} run start`],
},
},
+ {
+ value: "react-custom",
+ displayName: "React",
+ location: {
+ main: `frontend/supertokens-react-custom`,
+ config: [{ finalConfig: "/src/config.ts", configFiles: "/src/config" }],
+ },
+ script: {
+ setup: [`${manager} install`],
+ run: [`${manager} run start`],
+ },
+ },
{
value: "react-multitenancy",
displayName: "React",
@@ -458,6 +474,16 @@ export const recipeOptions = [
displayName: "Multi-factor Authentication",
},
];
+export const uiBuildOptions = [
+ {
+ value: UIBuildType.PRE_BUILT,
+ displayName: "Pre-built UI (Recommended)",
+ },
+ {
+ value: UIBuildType.CUSTOM,
+ displayName: "Custom UI",
+ },
+];
/**
* Export for all the questions to ask the user, should follow the exact format mentioned here https://github.com/SBoudrias/Inquirer.js#objects because this config is passed to inquirer. The order of questions depends on the position of the object in the array
*/
@@ -468,7 +494,7 @@ export async function getQuestions(flags) {
type: "input",
message: "What is your app called?",
default: "my-app",
- when: flags.appname === undefined,
+ when: (answers) => !answers.appname,
validate: function (input) {
const validations = validateFolderName(input);
if (validations.valid) {
@@ -482,24 +508,31 @@ export async function getQuestions(flags) {
return "Invalid project name: " + validations.problems[0];
},
},
+ {
+ name: "ui",
+ type: "list",
+ message: "Choose the ui build type for your frontend:",
+ choices: mapOptionsToChoices(uiBuildOptions),
+ when: (answers) => !answers.ui,
+ },
{
name: "frontend",
type: "list",
- message: "Choose a frontend framework (Visit our documentation for integration with other frameworks):",
- choices: mapOptionsToChoices(await getFrontendOptions(flags)),
- when: flags.frontend === undefined,
+ message: (answers) => getFrontendPromptMessage(answers),
+ choices: async (answers) =>
+ filterChoices(
+ mapOptionsToChoices(await getFrontendOptions(flags)),
+ answers,
+ FILTER_CHOICES_STRATEGY.filterFrontendByUiType
+ ),
+ when: (answers) => !answers.frontend && !!answers.ui,
},
{
name: "frontendNext",
type: "list",
message: "Choose how you want to organise your Next.js routes:",
choices: mapOptionsToChoices(await getNextJSOptions(flags)),
- when: (answers) => {
- if (flags.frontend !== undefined && flags.frontend === "next") {
- return true;
- }
- return answers.frontend === "next";
- },
+ when: (answers) => answers.frontend === "next",
},
{
name: "backend",
@@ -517,17 +550,17 @@ export async function getQuestions(flags) {
message: "Choose a Python framework:",
choices: mapOptionsToChoices(pythonOptions),
when: (answers) => {
- if (flags.backend !== undefined && flags.backend !== "python") {
+ if (answers.backend !== undefined && answers.backend !== "python") {
return false;
}
if (
- (flags.frontend !== undefined && flags.frontend.startsWith("next")) ||
+ (answers.frontend !== undefined && answers.frontend.startsWith("next")) ||
(answers.frontend !== undefined && answers.frontend.startsWith("next"))
) {
// This means that they want to use nextjs fullstack
return false;
}
- if (answers.backend !== "python" && flags.backend !== "python") {
+ if (answers.backend !== "python" && answers.backend !== "python") {
return false;
}
return true;
@@ -536,14 +569,19 @@ export async function getQuestions(flags) {
{
name: "recipe",
type: "list",
- message: "What type of authentication do you want to use?",
- choices: mapOptionsToChoices(recipeOptions),
+ message: (answers) => getRecipePromptMessage(answers),
+ choices: async (answers) =>
+ filterChoices(
+ mapOptionsToChoices(recipeOptions),
+ answers,
+ FILTER_CHOICES_STRATEGY.filterRecipeByUiType
+ ),
when: (answers) => {
// For capacitor we don't ask this question because it has its own way of swapping between recipes
if (answers.frontend === "capacitor") {
return false;
}
- return flags.recipe === undefined;
+ return !answers.recipe;
},
},
];
diff --git a/lib/build/filterChoicesUtils.js b/lib/build/filterChoicesUtils.js
new file mode 100644
index 00000000..8e59d954
--- /dev/null
+++ b/lib/build/filterChoicesUtils.js
@@ -0,0 +1,172 @@
+import { allFrontends, UIBuildType } from "./types.js";
+const getIsValueSupported = (value, supportedValues) => {
+ return supportedValues.includes(value);
+};
+/**
+ * Frontend filter strategy based on the selected UI build type.
+ */
+/**
+ * Strategy for filtering frontend choices based on the UI build type.
+ *
+ * @implements {IPromptFilterStrategy}
+ *
+ * @property {function} filterChoices - Filters the available choices based on the selected UI build type.
+ * @param {Array} choices - The list of available choices.
+ * @param {Object} answers - The answers provided by the user.
+ * @returns {Array} - The filtered list of choices.
+ *
+ * @property {function} validateUserArguments - Validates the user-provided arguments based on the UI build type.
+ * @param {Object} userArguments - The arguments provided by the user.
+ * @returns {boolean} - Returns true if the user arguments are valid, otherwise false.
+ */
+const filterFrontendByUiType = {
+ filterChoices(choices, answers) {
+ /**
+ * For pre-built UI
+ */
+ if (answers.ui === UIBuildType.PRE_BUILT) {
+ const prebuiltUiSupportedFrontends = getSupportedFrontendForUI(UIBuildType.PRE_BUILT);
+ return choices.filter((choice) => getIsValueSupported(choice.value, prebuiltUiSupportedFrontends));
+ }
+ /**
+ * For custom UI
+ */
+ const customUiSupportedFrontends = getSupportedFrontendForUI(UIBuildType.CUSTOM);
+ return choices.filter((choice) => getIsValueSupported(choice.value, customUiSupportedFrontends));
+ },
+ validateUserArguments(userArguments) {
+ /**
+ * For pre-built UI
+ */
+ const prebuiltUiSupportedFrontends = getSupportedFrontendForUI(UIBuildType.PRE_BUILT);
+ if (
+ !userArguments.frontend ||
+ (userArguments.ui === UIBuildType.PRE_BUILT &&
+ getIsValueSupported(userArguments.frontend, prebuiltUiSupportedFrontends))
+ ) {
+ return true;
+ }
+ /**
+ * For custom UI
+ */
+ const customUiSupportedFrontends = getSupportedFrontendForUI(UIBuildType.CUSTOM);
+ return getIsValueSupported(userArguments.frontend, customUiSupportedFrontends);
+ },
+};
+/**
+ * Recipe filter strategy based on the selected UI build type.
+ */
+const filterRecipeByUiType = {
+ filterChoices(choices, answers) {
+ /**
+ * For pre-built UI
+ */
+ const prebuiltUiSupportedRecipes = getSupportedRecipeForUI(UIBuildType.PRE_BUILT);
+ if (answers.ui === UIBuildType.PRE_BUILT) {
+ return choices.filter((choice) => getIsValueSupported(choice.value, prebuiltUiSupportedRecipes));
+ }
+ /**
+ * For custom UI
+ */
+ const customUiSupportedRecipes = getSupportedRecipeForUI(UIBuildType.CUSTOM);
+ return choices.filter((choice) => getIsValueSupported(choice.value, customUiSupportedRecipes));
+ },
+ validateUserArguments(userArguments) {
+ /**
+ * For pre-built UI
+ */
+ const prebuiltUiSupportedRecipes = getSupportedRecipeForUI(UIBuildType.PRE_BUILT);
+ if (
+ !userArguments.recipe ||
+ (userArguments.ui === UIBuildType.PRE_BUILT &&
+ getIsValueSupported(userArguments.recipe, prebuiltUiSupportedRecipes))
+ )
+ return true;
+ /**
+ * For custom UI
+ */
+ const customUiSupportedRecipes = getSupportedRecipeForUI(UIBuildType.CUSTOM);
+ return getIsValueSupported(userArguments.recipe, customUiSupportedRecipes);
+ },
+};
+export const FILTER_CHOICES_STRATEGY = {
+ filterFrontendByUiType,
+ filterRecipeByUiType,
+};
+/**
+ * Filters a list of choices based on the provided strategy or strategies.
+ *
+ * @param choices - An array of `PromptListChoice` objects or a promise that resolves to such an array.
+ * @param answers - An object containing the answers to previous prompts.
+ * @param strategy - An optional filtering strategy or an array of strategies. This can be one of the predefined
+ * `FILTER_CHOICES_STRATEGY` values, an array of such values, or a custom function that takes an
+ * array of choices and returns a promise that resolves to a filtered array of choices.
+ * @returns A promise that resolves to the filtered array of `PromptListChoice` objects.
+ */
+export const filterChoices = async (choices, answers, strategy) => {
+ if (!strategy || (Array.isArray(strategy) && strategy.length === 0)) {
+ return choices;
+ }
+ if (!Array.isArray(strategy)) {
+ strategy = [strategy];
+ }
+ for (const filter of strategy) {
+ choices = filter.filterChoices(choices, answers);
+ }
+ return choices;
+};
+export const validateUserArgumentsByFilterStrategy = (userArguments, argumentToValidateKey, strategy) => {
+ if (!strategy) {
+ return true;
+ }
+ if (!Array.isArray(strategy)) {
+ strategy = [strategy];
+ }
+ if (!userArguments?.[argumentToValidateKey]) {
+ throw new Error(`Invalid ${argumentToValidateKey} provided`);
+ }
+ for (const filter of strategy) {
+ if (!filter.validateUserArguments(userArguments)) {
+ return false;
+ }
+ }
+ return true;
+};
+export const isValidUiType = (userArguments) => {
+ if (!Object.values(UIBuildType).includes(userArguments.ui)) return false;
+ return validateUserArgumentsByFilterStrategy(userArguments, "ui", [
+ FILTER_CHOICES_STRATEGY.filterFrontendByUiType,
+ FILTER_CHOICES_STRATEGY.filterRecipeByUiType,
+ ]);
+};
+/**
+ * Retrieves the supported frontend frameworks for a given UI build type.
+ *
+ * @param {UIBuildType} ui - The type of UI build.
+ * @returns {SupportedFrontends[]} An array of supported frontend frameworks for the given UI.
+ */
+const getSupportedFrontendForUI = (ui) => {
+ const CUSTOM_ONLY = ["react-custom"];
+ if (ui === UIBuildType.PRE_BUILT) {
+ // Return all frontends except the custom only ones
+ return allFrontends.map((frontend) => frontend.id).filter((frontend) => !CUSTOM_ONLY.includes(frontend));
+ }
+ return CUSTOM_ONLY;
+};
+/**
+ * Retrieves the list of supported recipes for the given UI build type.
+ *
+ * @param {UIBuildType} ui - The type of UI build (e.g., PRE_BUILT or CUSTOM).
+ * @returns {string[]} An array of supported recipe names for the given UI Build type.
+ */
+const getSupportedRecipeForUI = (ui) => {
+ const CUSTOM_SUPPORTED_RECIPES = [
+ "emailpassword",
+ "thirdparty",
+ "passwordless",
+ "thirdpartypasswordless",
+ "thirdpartyemailpassword",
+ ];
+ const PREBUILT_SUPPORTED_RECIPES = [...CUSTOM_SUPPORTED_RECIPES, "all_auth", "multitenancy", "multifactorauth"];
+ return ui === UIBuildType.PRE_BUILT ? PREBUILT_SUPPORTED_RECIPES : CUSTOM_SUPPORTED_RECIPES;
+};
diff --git a/lib/build/index.js b/lib/build/index.js
index 9251c200..7a81fe0b 100755
--- a/lib/build/index.js
+++ b/lib/build/index.js
@@ -5,8 +5,9 @@ import { getDownloadLocationFromAnswers, downloadApp, setupProject, runProjectOr
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
- modifyAnswersBasedOnFlags,
+ generateInitialAnswers,
modifyAnswersBasedOnSelection,
+ modifyUserArgumentsForAliasFlags,
validateUserArguments,
} from "./userArgumentUtils.js";
import { Logger } from "./logger.js";
@@ -88,7 +89,8 @@ async function run() {
--manager: Which package manager to use
--autostart: Whether the CLI should start the project after setting up
*/
- const userArgumentsRaw = await yargs(hideBin(process.argv)).argv;
+ let userArgumentsRaw = await yargs(hideBin(process.argv)).argv;
+ userArgumentsRaw = modifyUserArgumentsForAliasFlags(userArgumentsRaw);
validateUserArguments(userArgumentsRaw);
const userArguments = {
...userArgumentsRaw,
@@ -103,8 +105,8 @@ async function run() {
eventName: "cli_started",
});
// Inquirer prompts all the questions to the user, answers will be an object that contains all the responses
- answers = await inquirer.prompt(await getQuestions(userArguments));
- answers = modifyAnswersBasedOnFlags(answers, userArguments);
+ const initialAnswers = generateInitialAnswers(userArguments);
+ answers = await inquirer.prompt(await getQuestions(userArguments), initialAnswers);
answers = modifyAnswersForPythonFrameworks(answers);
answers = modifyAnswersBasedOnSelection(answers);
AnalyticsManager.sendAnalyticsEvent({
diff --git a/lib/build/questionUtils.js b/lib/build/questionUtils.js
index 59ad0987..b725ef85 100644
--- a/lib/build/questionUtils.js
+++ b/lib/build/questionUtils.js
@@ -1,4 +1,5 @@
import os from "os";
+import { UIBuildType } from "./types.js";
export function getPythonRunScripts() {
if (os.platform() === "win32") {
return [
@@ -85,3 +86,21 @@ export function shouldSkipBackendQuestion(answers, userFlags) {
answers.frontend === "sveltekit"
);
}
+export function getFrontendPromptMessage(answers) {
+ if (answers.ui === UIBuildType.CUSTOM) {
+ return (
+ "Pick a frontend framework. For other frameworks, check the docs or" +
+ " select 'Pre-built UI' in the previous 'UI Build Type' step."
+ );
+ }
+ return "Choose a frontend framework (Visit our documentation for integration with other frameworks):";
+}
+export function getRecipePromptMessage(answers) {
+ if (answers.ui === UIBuildType.CUSTOM) {
+ return (
+ "What type of authentication do you want to use?. For other methods, check the docs or select 'Pre-built UI'" +
+ " in the previous 'UI Build Type' step."
+ );
+ }
+ return "What type of authentication do you want to use?";
+}
diff --git a/lib/build/scriptsUtils.js b/lib/build/scriptsUtils.js
new file mode 100644
index 00000000..238f8583
--- /dev/null
+++ b/lib/build/scriptsUtils.js
@@ -0,0 +1,68 @@
+import { UIBuildType } from "./types.js";
+import fs from "fs/promises";
+import path from "path";
+export async function executeSetupStepsIfExists(directoryPath, answers) {
+ if (answers.ui === UIBuildType.CUSTOM && answers.frontend === "react-custom") {
+ await modifyDirectoryCustomReact(directoryPath, answers);
+ }
+}
+async function modifyDirectoryCustomReact(frontendDir, answers) {
+ const recipe = answers.recipe;
+ const authPath = path.join(frontendDir, "src/pages/Auth");
+ const allItems = await fs.readdir(authPath);
+ /**
+ * Verify if the recipe folder exists
+ */
+ const recipeFolder = allItems.filter((item) => item !== recipe);
+ if (!recipeFolder || recipeFolder.length === 0) {
+ throw new Error("recipe folder not found");
+ }
+ /**
+ * Get Recipe Dependencies which would be injected into the recipe folder
+ * Step1: check if the dependency Exists
+ * Step2: Remove existing folder with the dependency name from the recipe folder
+ * Step3: Delete the index.tsx file from the dependency folder
+ * Step4: Copy the dependency folder from the Auth folder to the recipe folder
+ */
+ const dependencies = getRecipeDependencies(recipe);
+ for (const dependency of dependencies) {
+ if (!allItems.includes(dependency)) {
+ throw new Error(`Dependency ${dependency} not found`);
+ }
+ const dependencyPath = path.join(authPath, dependency);
+ const recipeDependencyPath = path.join(authPath, recipe, dependency);
+ await fs.rm(recipeDependencyPath, { recursive: true });
+ await fs.rm(path.join(dependencyPath, "index.tsx"));
+ await fs.cp(dependencyPath, recipeDependencyPath, { recursive: true });
+ }
+ /**
+ * Remove all the folders except the recipe folder
+ */
+ for (const item of allItems) {
+ if (item === recipe) {
+ continue;
+ }
+ const itemPath = path.join(authPath, item);
+ await fs.rm(itemPath, { recursive: true });
+ }
+ /**
+ * Move all the files from the recipe folder to Auth folder
+ */
+ const allItemsInsideRecipe = await fs.readdir(path.join(authPath, recipe));
+ for (const item of allItemsInsideRecipe) {
+ const itemPath = path.join(authPath, recipe, item);
+ const newPath = path.join(authPath, item);
+ await fs.cp(itemPath, newPath, { recursive: true });
+ }
+ /**
+ * Remove the recipe folder as part of cleanup
+ */
+ await fs.rm(path.join(authPath, recipe), { recursive: true });
+}
+export function getRecipeDependencies(recipe) {
+ const recipeDependencies = {
+ thirdpartyemailpassword: ["thirdparty", "emailpassword"],
+ thirdpartypasswordless: ["thirdparty", "passwordless"],
+ };
+ return recipeDependencies?.[recipe] || [];
+}
diff --git a/lib/build/types.js b/lib/build/types.js
index fc0e15df..99cb85e7 100644
--- a/lib/build/types.js
+++ b/lib/build/types.js
@@ -18,6 +18,9 @@ export const allFrontends = [
{
id: "react",
},
+ {
+ id: "react-custom",
+ },
{
id: "next",
},
@@ -79,6 +82,11 @@ export function isValidBackend(backend) {
return false;
}
export const allPackageManagers = ["npm", "yarn", "pnpm", "bun"];
+export var UIBuildType;
+(function (UIBuildType) {
+ UIBuildType["CUSTOM"] = "custom";
+ UIBuildType["PRE_BUILT"] = "pre-built";
+})(UIBuildType || (UIBuildType = {}));
export function isValidPackageManager(manager) {
if (allPackageManagers.includes(manager)) {
return true;
diff --git a/lib/build/userArgumentUtils.js b/lib/build/userArgumentUtils.js
index 178a1e8f..d988c649 100644
--- a/lib/build/userArgumentUtils.js
+++ b/lib/build/userArgumentUtils.js
@@ -7,9 +7,11 @@ import {
isValidFrontend,
isValidPackageManager,
isValidRecipeName,
+ UIBuildType,
} from "./types.js";
import validateProjectName from "validate-npm-package-name";
import path from "path";
+import { isValidUiType } from "./filterChoicesUtils.js";
export function validateNpmName(name) {
const nameValidation = validateProjectName(name);
if (nameValidation.validForNewPackages) {
@@ -60,6 +62,15 @@ export function validateUserArguments(userArguments) {
throw new Error("Invalid package manager provided, valid values:\n" + availableManagers);
}
}
+ if (userArguments.ui !== undefined) {
+ if (!isValidUiType(userArguments)) {
+ throw new Error(
+ `Invalid UI type provided, valid values: ${Object.values(UIBuildType).join(
+ ", "
+ )} or provided frontend or recipe is not compatible with the UI type`
+ );
+ }
+ }
}
export function modifyAnswersBasedOnSelection(answers) {
let _answers = answers;
@@ -79,14 +90,17 @@ export function modifyAnswersBasedOnSelection(answers) {
}
return _answers;
}
-export function modifyAnswersBasedOnFlags(answers, userArguments) {
- let _answers = answers;
+export function generateInitialAnswers(userArguments) {
+ let _answers = {};
if (userArguments.appname !== undefined) {
_answers.appname = userArguments.appname;
}
if (userArguments.recipe !== undefined) {
_answers.recipe = userArguments.recipe;
}
+ if (userArguments.ui !== undefined) {
+ _answers.ui = userArguments.ui;
+ }
if (userArguments.frontend !== undefined) {
const selectedFrontend = allFrontends.filter((i) => userArguments.frontend === i.id);
if (selectedFrontend.length === 0) {
@@ -112,3 +126,10 @@ export function getShouldAutoStartFromArgs(userArguments) {
}
return false;
}
+export function modifyUserArgumentsForAliasFlags(userArguments) {
+ let _userArguments = structuredClone(userArguments);
+ if (_userArguments.ui === UIBuildType.CUSTOM && _userArguments.frontend === "react") {
+ _userArguments.frontend = "react-custom";
+ }
+ return _userArguments;
+}
diff --git a/lib/build/utils.js b/lib/build/utils.js
index 399f756e..4290934d 100644
--- a/lib/build/utils.js
+++ b/lib/build/utils.js
@@ -12,6 +12,7 @@ import chalk from "chalk";
import { fileURLToPath } from "url";
import fetch from "node-fetch";
import { addPackageCommand } from "./packageManager.js";
+import { executeSetupStepsIfExists } from "./scriptsUtils.js";
const pipeline = promisify(stream.pipeline);
const defaultSetupErrorString = "Project Setup Failed!";
function normaliseLocationPath(path) {
@@ -294,6 +295,8 @@ async function setupFrontendBackendApp(answers, folderName, locations, userArgum
force: true,
});
}
+ spinner.text = "Executing setup scripts";
+ await executeSetupStepsIfExists(`./${folderName}/frontend`, answers);
spinner.text = "Installing frontend dependencies";
const frontendSetup = new Promise((res) => {
let stderr = [];
diff --git a/lib/ts/config.ts b/lib/ts/config.ts
index 5a862297..2bcb1eb0 100644
--- a/lib/ts/config.ts
+++ b/lib/ts/config.ts
@@ -1,8 +1,11 @@
-import { Answers, QuestionOption, RecipeQuestionOption, UserFlags } from "./types.js";
+import { Answers, QuestionOption, RecipeQuestionOption, UIBuildType, UIBuildTypeOption, UserFlags } from "./types.js";
+import { FILTER_CHOICES_STRATEGY, filterChoices } from "./filterChoicesUtils.js";
import { validateFolderName } from "./userArgumentUtils.js";
import {
getDjangoPythonRunScripts,
+ getFrontendPromptMessage,
getPythonRunScripts,
+ getRecipePromptMessage,
mapOptionsToChoices,
shouldSkipBackendQuestion,
} from "./questionUtils.js";
@@ -23,7 +26,7 @@ export async function getFrontendOptions({ manager }: UserFlags): Promise !answers.appname,
validate: function (input: any) {
const validations = validateFolderName(input);
@@ -496,25 +522,31 @@ export async function getQuestions(flags: UserFlags) {
return "Invalid project name: " + validations.problems![0];
},
},
+ {
+ name: "ui",
+ type: "list",
+ message: "Choose the ui build type for your frontend:",
+ choices: mapOptionsToChoices(uiBuildOptions),
+ when: (answers: Answers) => !answers.ui,
+ },
{
name: "frontend",
type: "list",
- message: "Choose a frontend framework (Visit our documentation for integration with other frameworks):",
- choices: mapOptionsToChoices(await getFrontendOptions(flags)),
- when: flags.frontend === undefined,
+ message: (answers: Answers) => getFrontendPromptMessage(answers),
+ choices: async (answers: Answers) =>
+ filterChoices(
+ mapOptionsToChoices(await getFrontendOptions(flags)),
+ answers,
+ FILTER_CHOICES_STRATEGY.filterFrontendByUiType
+ ),
+ when: (answers: Answers) => !answers.frontend && !!answers.ui,
},
{
name: "frontendNext",
type: "list",
message: "Choose how you want to organise your Next.js routes:",
choices: mapOptionsToChoices(await getNextJSOptions(flags)),
- when: (answers: Answers) => {
- if (flags.frontend !== undefined && flags.frontend === "next") {
- return true;
- }
-
- return answers.frontend === "next";
- },
+ when: (answers: Answers) => answers.frontend === "next",
},
{
name: "backend",
@@ -532,19 +564,19 @@ export async function getQuestions(flags: UserFlags) {
message: "Choose a Python framework:",
choices: mapOptionsToChoices(pythonOptions),
when: (answers: Answers) => {
- if (flags.backend !== undefined && flags.backend !== "python") {
+ if (answers.backend !== undefined && answers.backend !== "python") {
return false;
}
if (
- (flags.frontend !== undefined && flags.frontend.startsWith("next")) ||
+ (answers.frontend !== undefined && answers.frontend.startsWith("next")) ||
(answers.frontend !== undefined && answers.frontend.startsWith("next"))
) {
// This means that they want to use nextjs fullstack
return false;
}
- if (answers.backend !== "python" && flags.backend !== "python") {
+ if (answers.backend !== "python" && answers.backend !== "python") {
return false;
}
@@ -554,15 +586,20 @@ export async function getQuestions(flags: UserFlags) {
{
name: "recipe",
type: "list",
- message: "What type of authentication do you want to use?",
- choices: mapOptionsToChoices(recipeOptions),
+ message: (answers: Answers) => getRecipePromptMessage(answers),
+ choices: async (answers: Answers) =>
+ filterChoices(
+ mapOptionsToChoices(recipeOptions),
+ answers,
+ FILTER_CHOICES_STRATEGY.filterRecipeByUiType
+ ),
when: (answers: Answers) => {
// For capacitor we don't ask this question because it has its own way of swapping between recipes
if (answers.frontend === "capacitor") {
return false;
}
- return flags.recipe === undefined;
+ return !answers.recipe;
},
},
];
diff --git a/lib/ts/filterChoicesUtils.ts b/lib/ts/filterChoicesUtils.ts
new file mode 100644
index 00000000..dccdccb8
--- /dev/null
+++ b/lib/ts/filterChoicesUtils.ts
@@ -0,0 +1,201 @@
+import {
+ allFrontends,
+ Answers,
+ IPromptFilterStrategy,
+ PromptListChoice,
+ SupportedFrontends,
+ UIBuildType,
+ UserFlagsRaw,
+} from "./types.js";
+
+const getIsValueSupported = (value: string, supportedValues: string[]) => {
+ return supportedValues.includes(value);
+};
+
+/**
+ * Frontend filter strategy based on the selected UI build type.
+ */
+/**
+ * Strategy for filtering frontend choices based on the UI build type.
+ *
+ * @implements {IPromptFilterStrategy}
+ *
+ * @property {function} filterChoices - Filters the available choices based on the selected UI build type.
+ * @param {Array} choices - The list of available choices.
+ * @param {Object} answers - The answers provided by the user.
+ * @returns {Array} - The filtered list of choices.
+ *
+ * @property {function} validateUserArguments - Validates the user-provided arguments based on the UI build type.
+ * @param {Object} userArguments - The arguments provided by the user.
+ * @returns {boolean} - Returns true if the user arguments are valid, otherwise false.
+ */
+const filterFrontendByUiType: IPromptFilterStrategy = {
+ filterChoices(choices, answers) {
+ /**
+ * For pre-built UI
+ */
+ if (answers.ui === UIBuildType.PRE_BUILT) {
+ const prebuiltUiSupportedFrontends = getSupportedFrontendForUI(UIBuildType.PRE_BUILT);
+ return choices.filter((choice) => getIsValueSupported(choice.value, prebuiltUiSupportedFrontends));
+ }
+
+ /**
+ * For custom UI
+ */
+ const customUiSupportedFrontends = getSupportedFrontendForUI(UIBuildType.CUSTOM);
+ return choices.filter((choice) => getIsValueSupported(choice.value, customUiSupportedFrontends));
+ },
+ validateUserArguments(userArguments) {
+ /**
+ * For pre-built UI
+ */
+ const prebuiltUiSupportedFrontends = getSupportedFrontendForUI(UIBuildType.PRE_BUILT);
+ if (
+ !userArguments.frontend ||
+ (userArguments.ui === UIBuildType.PRE_BUILT &&
+ getIsValueSupported(userArguments.frontend, prebuiltUiSupportedFrontends))
+ ) {
+ return true;
+ }
+
+ /**
+ * For custom UI
+ */
+ const customUiSupportedFrontends = getSupportedFrontendForUI(UIBuildType.CUSTOM);
+ return getIsValueSupported(userArguments.frontend, customUiSupportedFrontends);
+ },
+};
+
+/**
+ * Recipe filter strategy based on the selected UI build type.
+ */
+const filterRecipeByUiType: IPromptFilterStrategy = {
+ filterChoices(choices, answers) {
+ /**
+ * For pre-built UI
+ */
+ const prebuiltUiSupportedRecipes = getSupportedRecipeForUI(UIBuildType.PRE_BUILT);
+ if (answers.ui === UIBuildType.PRE_BUILT) {
+ return choices.filter((choice) => getIsValueSupported(choice.value, prebuiltUiSupportedRecipes));
+ }
+ /**
+ * For custom UI
+ */
+ const customUiSupportedRecipes = getSupportedRecipeForUI(UIBuildType.CUSTOM);
+ return choices.filter((choice) => getIsValueSupported(choice.value, customUiSupportedRecipes));
+ },
+ validateUserArguments(userArguments) {
+ /**
+ * For pre-built UI
+ */
+ const prebuiltUiSupportedRecipes = getSupportedRecipeForUI(UIBuildType.PRE_BUILT);
+ if (
+ !userArguments.recipe ||
+ (userArguments.ui === UIBuildType.PRE_BUILT &&
+ getIsValueSupported(userArguments.recipe, prebuiltUiSupportedRecipes))
+ )
+ return true;
+ /**
+ * For custom UI
+ */
+ const customUiSupportedRecipes = getSupportedRecipeForUI(UIBuildType.CUSTOM);
+ return getIsValueSupported(userArguments.recipe, customUiSupportedRecipes);
+ },
+};
+
+export const FILTER_CHOICES_STRATEGY = {
+ filterFrontendByUiType,
+ filterRecipeByUiType,
+};
+
+/**
+ * Filters a list of choices based on the provided strategy or strategies.
+ *
+ * @param choices - An array of `PromptListChoice` objects or a promise that resolves to such an array.
+ * @param answers - An object containing the answers to previous prompts.
+ * @param strategy - An optional filtering strategy or an array of strategies. This can be one of the predefined
+ * `FILTER_CHOICES_STRATEGY` values, an array of such values, or a custom function that takes an
+ * array of choices and returns a promise that resolves to a filtered array of choices.
+ * @returns A promise that resolves to the filtered array of `PromptListChoice` objects.
+ */
+export const filterChoices = async (
+ choices: PromptListChoice[],
+ answers: Answers,
+ strategy?: IPromptFilterStrategy | IPromptFilterStrategy[]
+): Promise => {
+ if (!strategy || (Array.isArray(strategy) && strategy.length === 0)) {
+ return choices;
+ }
+ if (!Array.isArray(strategy)) {
+ strategy = [strategy];
+ }
+ for (const filter of strategy) {
+ choices = filter.filterChoices(choices, answers);
+ }
+ return choices;
+};
+
+export const validateUserArgumentsByFilterStrategy = (
+ userArguments: UserFlagsRaw,
+ argumentToValidateKey: keyof UserFlagsRaw,
+ strategy?: IPromptFilterStrategy | IPromptFilterStrategy[]
+): boolean => {
+ if (!strategy) {
+ return true;
+ }
+ if (!Array.isArray(strategy)) {
+ strategy = [strategy];
+ }
+ if (!userArguments?.[argumentToValidateKey]) {
+ throw new Error(`Invalid ${argumentToValidateKey} provided`);
+ }
+
+ for (const filter of strategy) {
+ if (!filter.validateUserArguments(userArguments)) {
+ return false;
+ }
+ }
+ return true;
+};
+
+export const isValidUiType = (userArguments: UserFlagsRaw): boolean => {
+ if (!Object.values(UIBuildType).includes(userArguments.ui!)) return false;
+ return validateUserArgumentsByFilterStrategy(userArguments, "ui", [
+ FILTER_CHOICES_STRATEGY.filterFrontendByUiType,
+ FILTER_CHOICES_STRATEGY.filterRecipeByUiType,
+ ]);
+};
+
+/**
+ * Retrieves the supported frontend frameworks for a given UI build type.
+ *
+ * @param {UIBuildType} ui - The type of UI build.
+ * @returns {SupportedFrontends[]} An array of supported frontend frameworks for the given UI.
+ */
+const getSupportedFrontendForUI = (ui: UIBuildType): SupportedFrontends[] => {
+ const CUSTOM_ONLY: SupportedFrontends[] = ["react-custom"];
+ if (ui === UIBuildType.PRE_BUILT) {
+ // Return all frontends except the custom only ones
+ return allFrontends.map((frontend) => frontend.id).filter((frontend) => !CUSTOM_ONLY.includes(frontend));
+ }
+ return CUSTOM_ONLY;
+};
+
+/**
+ * Retrieves the list of supported recipes for the given UI build type.
+ *
+ * @param {UIBuildType} ui - The type of UI build (e.g., PRE_BUILT or CUSTOM).
+ * @returns {string[]} An array of supported recipe names for the given UI Build type.
+ */
+const getSupportedRecipeForUI = (ui: UIBuildType): string[] => {
+ const CUSTOM_SUPPORTED_RECIPES = [
+ "emailpassword",
+ "thirdparty",
+ "passwordless",
+ "thirdpartypasswordless",
+ "thirdpartyemailpassword",
+ ];
+ const PREBUILT_SUPPORTED_RECIPES = [...CUSTOM_SUPPORTED_RECIPES, "all_auth", "multitenancy", "multifactorauth"];
+
+ return ui === UIBuildType.PRE_BUILT ? PREBUILT_SUPPORTED_RECIPES : CUSTOM_SUPPORTED_RECIPES;
+};
diff --git a/lib/ts/index.ts b/lib/ts/index.ts
index 2f4c9110..7e496662 100755
--- a/lib/ts/index.ts
+++ b/lib/ts/index.ts
@@ -6,8 +6,9 @@ import { getDownloadLocationFromAnswers, downloadApp, setupProject, runProjectOr
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
- modifyAnswersBasedOnFlags,
+ generateInitialAnswers,
modifyAnswersBasedOnSelection,
+ modifyUserArgumentsForAliasFlags,
validateUserArguments,
} from "./userArgumentUtils.js";
import { Logger } from "./logger.js";
@@ -98,7 +99,8 @@ async function run() {
--manager: Which package manager to use
--autostart: Whether the CLI should start the project after setting up
*/
- const userArgumentsRaw = (await yargs(hideBin(process.argv)).argv) as UserFlagsRaw;
+ let userArgumentsRaw = (await yargs(hideBin(process.argv)).argv) as UserFlagsRaw;
+ userArgumentsRaw = modifyUserArgumentsForAliasFlags(userArgumentsRaw);
validateUserArguments(userArgumentsRaw);
const userArguments: UserFlags = {
...userArgumentsRaw,
@@ -116,9 +118,9 @@ async function run() {
});
// Inquirer prompts all the questions to the user, answers will be an object that contains all the responses
- answers = await inquirer.prompt(await getQuestions(userArguments));
+ const initialAnswers = generateInitialAnswers(userArguments);
+ answers = await inquirer.prompt(await getQuestions(userArguments), initialAnswers);
- answers = modifyAnswersBasedOnFlags(answers, userArguments);
answers = modifyAnswersForPythonFrameworks(answers);
answers = modifyAnswersBasedOnSelection(answers);
diff --git a/lib/ts/questionUtils.ts b/lib/ts/questionUtils.ts
index cb3a4a44..5901d0c4 100644
--- a/lib/ts/questionUtils.ts
+++ b/lib/ts/questionUtils.ts
@@ -1,5 +1,5 @@
import os from "os";
-import { Answers, QuestionOption, RecipeQuestionOption, UserFlags } from "./types.js";
+import { Answers, QuestionOption, RecipeQuestionOption, UIBuildType, UserFlags } from "./types.js";
export function getPythonRunScripts(): string[] {
if (os.platform() === "win32") {
@@ -96,3 +96,23 @@ export function shouldSkipBackendQuestion(answers: Answers, userFlags: UserFlags
answers.frontend === "sveltekit"
);
}
+
+export function getFrontendPromptMessage(answers: Answers): string {
+ if (answers.ui === UIBuildType.CUSTOM) {
+ return (
+ "Pick a frontend framework. For other frameworks, check the docs or" +
+ " select 'Pre-built UI' in the previous 'UI Build Type' step."
+ );
+ }
+ return "Choose a frontend framework (Visit our documentation for integration with other frameworks):";
+}
+
+export function getRecipePromptMessage(answers: Answers): string {
+ if (answers.ui === UIBuildType.CUSTOM) {
+ return (
+ "What type of authentication do you want to use?. For other methods, check the docs or select 'Pre-built UI'" +
+ " in the previous 'UI Build Type' step."
+ );
+ }
+ return "What type of authentication do you want to use?";
+}
diff --git a/lib/ts/scriptsUtils.ts b/lib/ts/scriptsUtils.ts
new file mode 100644
index 00000000..63f78069
--- /dev/null
+++ b/lib/ts/scriptsUtils.ts
@@ -0,0 +1,76 @@
+import { Answers, UIBuildType } from "./types.js";
+import fs from "fs/promises";
+import path from "path";
+
+export async function executeSetupStepsIfExists(directoryPath: string, answers: Answers): Promise {
+ if (answers.ui === UIBuildType.CUSTOM && answers.frontend === "react-custom") {
+ await modifyDirectoryCustomReact(directoryPath, answers);
+ }
+}
+
+async function modifyDirectoryCustomReact(frontendDir: string, answers: Answers): Promise {
+ const recipe = answers.recipe;
+ const authPath = path.join(frontendDir, "src/pages/Auth");
+ const allItems = await fs.readdir(authPath);
+
+ /**
+ * Verify if the recipe folder exists
+ */
+ const recipeFolder = allItems.filter((item) => item !== recipe);
+ if (!recipeFolder || recipeFolder.length === 0) {
+ throw new Error("recipe folder not found");
+ }
+
+ /**
+ * Get Recipe Dependencies which would be injected into the recipe folder
+ * Step1: check if the dependency Exists
+ * Step2: Remove existing folder with the dependency name from the recipe folder
+ * Step3: Delete the index.tsx file from the dependency folder
+ * Step4: Copy the dependency folder from the Auth folder to the recipe folder
+ */
+ const dependencies = getRecipeDependencies(recipe);
+ for (const dependency of dependencies) {
+ if (!allItems.includes(dependency)) {
+ throw new Error(`Dependency ${dependency} not found`);
+ }
+ const dependencyPath = path.join(authPath, dependency);
+ const recipeDependencyPath = path.join(authPath, recipe, dependency);
+ await fs.rm(recipeDependencyPath, { recursive: true });
+ await fs.rm(path.join(dependencyPath, "index.tsx"));
+ await fs.cp(dependencyPath, recipeDependencyPath, { recursive: true });
+ }
+
+ /**
+ * Remove all the folders except the recipe folder
+ */
+ for (const item of allItems) {
+ if (item === recipe) {
+ continue;
+ }
+ const itemPath = path.join(authPath, item);
+ await fs.rm(itemPath, { recursive: true });
+ }
+
+ /**
+ * Move all the files from the recipe folder to Auth folder
+ */
+ const allItemsInsideRecipe = await fs.readdir(path.join(authPath, recipe));
+ for (const item of allItemsInsideRecipe) {
+ const itemPath = path.join(authPath, recipe, item);
+ const newPath = path.join(authPath, item);
+ await fs.cp(itemPath, newPath, { recursive: true });
+ }
+
+ /**
+ * Remove the recipe folder as part of cleanup
+ */
+ await fs.rm(path.join(authPath, recipe), { recursive: true });
+}
+
+export function getRecipeDependencies(recipe: string): string[] {
+ const recipeDependencies: Record = {
+ thirdpartyemailpassword: ["thirdparty", "emailpassword"],
+ thirdpartypasswordless: ["thirdparty", "passwordless"],
+ };
+ return recipeDependencies?.[recipe] || [];
+}
diff --git a/lib/ts/types.ts b/lib/ts/types.ts
index fd676bbc..ff06ed5a 100644
--- a/lib/ts/types.ts
+++ b/lib/ts/types.ts
@@ -29,6 +29,7 @@ export function isValidRecipeName(recipe: string): recipe is Recipe {
export type SupportedFrontends =
| "react"
+ | "react-custom"
| "next"
| "next-multitenancy"
| "next-app-directory"
@@ -52,6 +53,9 @@ export const allFrontends: {
{
id: "react",
},
+ {
+ id: "react-custom",
+ },
{
id: "next",
},
@@ -192,7 +196,10 @@ export type RecipeQuestionOption = {
shouldDisplay?: boolean;
};
+export type UIBuildTypeOption = RecipeQuestionOption;
+
export type Answers = {
+ ui: UIBuildType;
frontend?: SupportedFrontends;
backend?: SupportedBackends;
recipe: string;
@@ -216,6 +223,11 @@ export type SupportedPackageManagers = "npm" | "yarn" | "pnpm" | "bun";
export const allPackageManagers: SupportedPackageManagers[] = ["npm", "yarn", "pnpm", "bun"];
+export enum UIBuildType {
+ CUSTOM = "custom",
+ PRE_BUILT = "pre-built",
+}
+
export function isValidPackageManager(manager: string): manager is SupportedPackageManagers {
if (allPackageManagers.includes(manager as SupportedPackageManagers)) {
return true;
@@ -232,6 +244,7 @@ export type UserFlagsRaw = {
frontend?: SupportedFrontends;
backend?: SupportedBackends;
manager?: SupportedPackageManagers;
+ ui?: UIBuildType;
autostart?: string | boolean;
};
@@ -268,3 +281,13 @@ export type AnalyticsEventWithCommonProperties = AnalyticsEvent & {
os: string;
cliversion: string;
};
+
+export type PromptListChoice = {
+ name: string;
+ value: string;
+};
+
+export interface IPromptFilterStrategy {
+ filterChoices: (choices: PromptListChoice[], answers: Answers) => PromptListChoice[];
+ validateUserArguments: (userArguments: UserFlagsRaw) => boolean;
+}
diff --git a/lib/ts/userArgumentUtils.ts b/lib/ts/userArgumentUtils.ts
index a3277f32..2d57f2b1 100644
--- a/lib/ts/userArgumentUtils.ts
+++ b/lib/ts/userArgumentUtils.ts
@@ -8,11 +8,13 @@ import {
isValidFrontend,
isValidPackageManager,
isValidRecipeName,
+ UIBuildType,
UserFlags,
UserFlagsRaw,
} from "./types.js";
import validateProjectName from "validate-npm-package-name";
import path from "path";
+import { isValidUiType } from "./filterChoicesUtils.js";
export function validateNpmName(name: string): {
valid: boolean;
@@ -78,6 +80,16 @@ export function validateUserArguments(userArguments: UserFlagsRaw) {
throw new Error("Invalid package manager provided, valid values:\n" + availableManagers);
}
}
+
+ if (userArguments.ui !== undefined) {
+ if (!isValidUiType(userArguments)) {
+ throw new Error(
+ `Invalid UI type provided, valid values: ${Object.values(UIBuildType).join(
+ ", "
+ )} or provided frontend or recipe is not compatible with the UI type`
+ );
+ }
+ }
}
export function modifyAnswersBasedOnSelection(answers: Answers): Answers {
@@ -102,8 +114,8 @@ export function modifyAnswersBasedOnSelection(answers: Answers): Answers {
return _answers;
}
-export function modifyAnswersBasedOnFlags(answers: Answers, userArguments: UserFlags): Answers {
- let _answers = answers;
+export function generateInitialAnswers(userArguments: UserFlags): Partial {
+ let _answers: Partial = {};
if (userArguments.appname !== undefined) {
_answers.appname = userArguments.appname;
@@ -113,6 +125,10 @@ export function modifyAnswersBasedOnFlags(answers: Answers, userArguments: UserF
_answers.recipe = userArguments.recipe;
}
+ if (userArguments.ui !== undefined) {
+ _answers.ui = userArguments.ui;
+ }
+
if (userArguments.frontend !== undefined) {
const selectedFrontend = allFrontends.filter((i) => userArguments.frontend === i.id);
@@ -146,3 +162,13 @@ export function getShouldAutoStartFromArgs(userArguments: UserFlags): boolean {
return false;
}
+
+export function modifyUserArgumentsForAliasFlags(userArguments: UserFlagsRaw): UserFlagsRaw {
+ let _userArguments = structuredClone(userArguments);
+
+ if (_userArguments.ui === UIBuildType.CUSTOM && _userArguments.frontend === "react") {
+ _userArguments.frontend = "react-custom";
+ }
+
+ return _userArguments;
+}
diff --git a/lib/ts/utils.ts b/lib/ts/utils.ts
index 2d485190..041394e1 100644
--- a/lib/ts/utils.ts
+++ b/lib/ts/utils.ts
@@ -2,7 +2,7 @@ import { getBackendOptionForProcessing, getFrontendOptionsForProcessing } from "
import tar from "tar";
import { promisify } from "util";
import stream from "node:stream";
-import { Answers, DownloadLocations, ExecOutput, QuestionOption, UserFlags } from "./types";
+import { Answers, DownloadLocations, ExecOutput, QuestionOption, UserFlags } from "./types.js";
import fs from "fs";
import path from "path";
import { exec } from "child_process";
@@ -14,6 +14,7 @@ import chalk from "chalk";
import { fileURLToPath } from "url";
import fetch from "node-fetch";
import { addPackageCommand } from "./packageManager.js";
+import { executeSetupStepsIfExists } from "./scriptsUtils.js";
const pipeline = promisify(stream.pipeline);
const defaultSetupErrorString = "Project Setup Failed!";
@@ -374,7 +375,12 @@ async function setupFrontendBackendApp(
});
}
+ spinner.text = "Executing setup scripts";
+
+ await executeSetupStepsIfExists(`./${folderName}/frontend`, answers);
+
spinner.text = "Installing frontend dependencies";
+
const frontendSetup = new Promise((res) => {
let stderr: string[] = [];
@@ -427,6 +433,7 @@ async function setupFrontendBackendApp(
await performAdditionalSetupForFrontendIfNeeded(selectedFrontend, folderName, userArguments);
spinner.text = "Installing backend dependencies";
+
const backendSetup = new Promise((res) => {
let stderr: string[] = [];