From f1a29212314a41e2d48d30fe7c8beb7bb18d8ad2 Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Sun, 29 May 2022 19:26:13 +0800 Subject: [PATCH 01/10] chore: add uuid --- .gitignore | 1 + package.json | 1 + yarn.lock | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4d29575d..6c9142c6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ .env.development.local .env.test.local .env.production.local +/.idea npm-debug.log* yarn-debug.log* diff --git a/package.json b/package.json index d135df5a..e724d048 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "styled-reset": "^4.4.1", "terser-webpack-plugin": "^5.3.3", "tippy.js": "^6.3.7", + "uuid": "^8.3.2", "webpack": "^5.73.0", "webpack-dev-server": "^4.9.1", "webpack-manifest-plugin": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 1155338e..c4985f88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9298,7 +9298,7 @@ utils-merge@1.0.1: uuid@^8.3.2: version "8.3.2" - resolved "https://mirrors.tencent.com/npm/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-compile-cache-lib@^3.0.0: From c26000f3f5734e47dd3db1097f928d2946c3f5e8 Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Sun, 29 May 2022 19:26:48 +0800 Subject: [PATCH 02/10] feat: implement onboarding page --- src/assets/icons/play.svg | 3 + src/common/component/styled/Radio.js | 91 ++++++++ src/routes/index.js | 205 +++++++++--------- src/routes/onboarding/index.js | 89 ++++++++ .../onboarding/steps/adminCredentials.js | 58 +++++ src/routes/onboarding/steps/first.js | 26 +++ src/routes/onboarding/steps/inviteLink.js | 70 ++++++ src/routes/onboarding/steps/inviteRule.js | 33 +++ src/routes/onboarding/steps/last.js | 44 ++++ src/routes/onboarding/steps/spaceName.js | 34 +++ src/routes/onboarding/styled.js | 92 ++++++++ 11 files changed, 642 insertions(+), 103 deletions(-) create mode 100644 src/assets/icons/play.svg create mode 100644 src/common/component/styled/Radio.js create mode 100644 src/routes/onboarding/index.js create mode 100644 src/routes/onboarding/steps/adminCredentials.js create mode 100644 src/routes/onboarding/steps/first.js create mode 100644 src/routes/onboarding/steps/inviteLink.js create mode 100644 src/routes/onboarding/steps/inviteRule.js create mode 100644 src/routes/onboarding/steps/last.js create mode 100644 src/routes/onboarding/steps/spaceName.js create mode 100644 src/routes/onboarding/styled.js diff --git a/src/assets/icons/play.svg b/src/assets/icons/play.svg new file mode 100644 index 00000000..0bd11821 --- /dev/null +++ b/src/assets/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/component/styled/Radio.js b/src/common/component/styled/Radio.js new file mode 100644 index 00000000..6e0cfd06 --- /dev/null +++ b/src/common/component/styled/Radio.js @@ -0,0 +1,91 @@ +import styled from "styled-components"; +import { v4 as UUIDv4 } from "uuid"; +import { useRef, useState } from "react"; + +const StyledForm = styled.form` + > .option { + &:not(:last-child) { + margin-bottom: 8px; + } + + > input[type="radio"] { + display: none; + + & + .box { + width: 512px; + background: #ffffff; + border: 1px solid #d0d5dd; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); + border-radius: 8px; + transition: all ease-in-out 250ms; + + & > label { + display: flex; + flex-direction: row; + align-items: center; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #667085; + cursor: pointer; + user-select: none; + transition: all ease-in-out 250ms; + + &:before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border-radius: 8px; + background: #ffffff; + box-shadow: inset 0 0 0 4px #ffffff; + border: 1px solid #d0d5dd; + margin: 14px 8px 14px 14px; + transition: all ease-in-out 500ms; + } + } + } + + &:checked + .box { + background: #22ccee; + border: 1px solid #d0d5dd; + + & > label { + color: #ffffff; + + &:before { + background: #ffffff; + box-shadow: inset 0 0 0 4px #22ccee; + border: 1px solid #ffffff; + } + } + } + } + } +`; + +export default function Radio({ options, value = undefined, onChange = undefined }) { + const [innerValue, setInnerValue] = useState(0); + const id = useRef(UUIDv4()); + + return ( + + {options.map((item, index) => ( +
+ { + value === undefined && setInnerValue(index); + onChange !== null && onChange(index); + }} + id={`${id.current}-${index}`} + /> +
+ +
+
+ ))} +
+ ); +} diff --git a/src/routes/index.js b/src/routes/index.js index d98fbe32..58d40f9e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -21,115 +21,114 @@ import store from "../app/store"; import InvitePage from "./invite"; import SettingPage from "./setting"; import SettingChannelPage from "./settingChannel"; +import OnboardingPage from "./onboarding"; import toast from "react-hot-toast"; import ResourceManagement from "./resources"; const PageRoutes = () => { - const { - ui: { online }, - fileMessages, - } = useSelector((store) => { - return { ui: store.ui, fileMessages: store.fileMessage }; - }); - // 掉线检测 - useEffect(() => { - let toastId = 0; - if (!online) { - toast.error("Network Offline!", { duration: Infinity }); - } else { - toast.dismiss(toastId); - } - }, [online]); + const { + ui: { online }, + fileMessages + } = useSelector((store) => { + return { ui: store.ui, fileMessages: store.fileMessage }; + }); + // 掉线检测 + useEffect(() => { + let toastId = 0; + if (!online) { + toast.error("Network Offline!", { duration: Infinity }); + } else { + toast.dismiss(toastId); + } + }, [online]); - return ( - - - } /> - - - - } - /> - - - - } - /> - - - - } - > - } /> - - } /> - } /> - - - - - - } - /> - } /> - - - - } - > - - } /> - } /> - - } /> - - } /> - } /> - } /> - - - } /> - } /> - - }> - } - > - - } /> - - - ); + return ( + + + } /> + + + + } + /> + + + + } + /> + + + + } + > + } /> + + } /> + } /> + + + + + + } + /> + } /> + } /> + + + + } + > + + } /> + } /> + + } /> + + } /> + } /> + } /> + + + } /> + } /> + + }> + }> + + } /> + + + ); }; // const local_key = "AUTH_DATA"; export default function ReduxRoutes() { - // const [authData, setAuthData] = useState( - // JSON.parse(localStorage.getItem(local_key)) - // ); - // const updateAuthData = (data) => { - // localStorage.setItem(local_key, JSON.stringify(data)); - // setAuthData(data); - // }; - return ( - - - - - ); + // const [authData, setAuthData] = useState( + // JSON.parse(localStorage.getItem(local_key)) + // ); + // const updateAuthData = (data) => { + // localStorage.setItem(local_key, JSON.stringify(data)); + // setAuthData(data); + // }; + return ( + + + + + ); } diff --git a/src/routes/onboarding/index.js b/src/routes/onboarding/index.js new file mode 100644 index 00000000..7223f3e9 --- /dev/null +++ b/src/routes/onboarding/index.js @@ -0,0 +1,89 @@ +import { useState } from "react"; +import toast from "react-hot-toast"; +import StyledButton from "../../common/component/styled/Button"; +import FirstStep from "./steps/first"; +import SpaceNameStep from "./steps/spaceName"; +import AdminCredentialsStep from "./steps/adminCredentials"; +import InviteRuleStep from "./steps/inviteRule"; +import InviteLinkStep from "./steps/inviteLink"; +import LastStep from "./steps/last"; +import StyledOnboardingPage from "./styled"; + +export default function OnboardingPage() { + const [step, setStep] = useState(0); + const [data, setData] = useState({ + spaceName: "", + adminEmail: "", + adminPassword: "", + adminPassword2: "", + inviteRule: null + }); + + return ( + +
+
+ {step > 0 && step < 5 && ( + <> + setStep(step - 1)}> + Back + + { + if (step === 1) { + // Verification for space name + if (data.spaceName === "") { + toast.error("Please enter space name!"); + return; + } + setStep(step + 1); + } else if (step === 2) { + // Verification for admin credentials + if (data.adminEmail === "") { + toast.error("Please enter admin email!"); + return; + } else if (data.adminPassword === "") { + toast.error("Please enter admin password!"); + return; + } else if (data.adminPassword !== data.adminPassword2) { + toast.error("Two passwords do not match!"); + return; + } + setStep(step + 1); + } else if (step === 3) { + // Verification for invitation rule + if (data.inviteRule === null) { + toast.error("Please choose one option!"); + return; + } + setStep(step + 1); + } else if (step === 4) { + // Verification for invitation link + setStep(step + 1); + } + }} + > + Next + + + )} + {step === 0 && setStep(step + 1)} />} + {step === 1 && } + {step === 2 && } + {step === 3 && } + {step === 4 && } + {step === 5 && ( + { + // TODO: finish it + console.log("onboarding steps completed:", data); + }} + /> + )} +
+
+
+ ); +} diff --git a/src/routes/onboarding/steps/adminCredentials.js b/src/routes/onboarding/steps/adminCredentials.js new file mode 100644 index 00000000..4caafef0 --- /dev/null +++ b/src/routes/onboarding/steps/adminCredentials.js @@ -0,0 +1,58 @@ +import styled from "styled-components"; +import StyledInput from "../../../common/component/styled/Input"; + +const StyledAdminCredentialsStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .input:not(:last-child) { + margin-bottom: 20px; + } +`; + +export default function AdminCredentialsStep({ data, setData }) { + return ( + + Now let’s set up your admin account + You are the 1st user and admin of your space! + + setData({ + ...data, + adminEmail: e.target.value + }) + } + /> + + setData({ + ...data, + adminPassword: e.target.value + }) + } + /> + + setData({ + ...data, + adminPassword2: e.target.value + }) + } + /> + + ); +} diff --git a/src/routes/onboarding/steps/first.js b/src/routes/onboarding/steps/first.js new file mode 100644 index 00000000..21b96cf2 --- /dev/null +++ b/src/routes/onboarding/steps/first.js @@ -0,0 +1,26 @@ +import styled from "styled-components"; +import StyledButton from "../../../common/component/styled/Button"; +import PlayIcon from "../../../assets/icons/play.svg?url"; + +const StyledFirstStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export default function FirstStep({ onButtonClick }) { + return ( + + Welcome to your Rustchat! + + Everything in this space is owned by you. Let’s set up your space! + + + play icon + Start + + + ); +} diff --git a/src/routes/onboarding/steps/inviteLink.js b/src/routes/onboarding/steps/inviteLink.js new file mode 100644 index 00000000..57b751de --- /dev/null +++ b/src/routes/onboarding/steps/inviteLink.js @@ -0,0 +1,70 @@ +import styled from "styled-components"; +import StyledInput from "../../../common/component/styled/Input"; +import StyledButton from "../../../common/component/styled/Button"; +import useInviteLink from "../../../common/hook/useInviteLink"; + +const StyledInviteLinkStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + margin-bottom: 40px; + } + + > .tip { + font-weight: 600; + font-size: 14px; + line-height: 20px; + color: #475467; + margin-bottom: 8px; + } + + > .link { + position: relative; + background: #ffffff; + border: 1px solid #f4f4f5; + box-shadow: 0 1px 2px rgba(31, 41, 55, 0.08); + border-radius: 4px; + width: 374px; + display: flex; + + > input { + border: none; + box-shadow: none; + padding: 11px 0 11px 8px; + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: #78787c; + } + + > button { + padding: 0 8px; + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: #22ccee; + } + } +`; + +export default function InviteLinkStep() { + const { link, linkCopied, copyLink } = useInviteLink(); + + return ( + + Last step: invite others! + Now let’s invite others! + Send invitation link to your future community members: +
+ + + {linkCopied ? "Copied" : `Copy`} + +
+
+ ); +} diff --git a/src/routes/onboarding/steps/inviteRule.js b/src/routes/onboarding/steps/inviteRule.js new file mode 100644 index 00000000..a7e9b9d0 --- /dev/null +++ b/src/routes/onboarding/steps/inviteRule.js @@ -0,0 +1,33 @@ +import styled from "styled-components"; +import StyledRadio from "../../../common/component/styled/Radio"; + +const StyledInviteRuleStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .input:not(:last-child) { + margin-bottom: 20px; + } +`; + +export default function InviteRuleStep({ data, setData }) { + return ( + + Last step: invite others! + Firstly, who can sign up to this server? + + setData({ + ...data, + inviteRule: v + }) + } + /> + + ); +} diff --git a/src/routes/onboarding/steps/last.js b/src/routes/onboarding/steps/last.js new file mode 100644 index 00000000..5be297cc --- /dev/null +++ b/src/routes/onboarding/steps/last.js @@ -0,0 +1,44 @@ +import styled from "styled-components"; +import StyledButton from "../../../common/component/styled/Button"; +import PlayIcon from "../../../assets/icons/play.svg?url"; + +const StyledLastStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + margin-bottom: 48px; + } + + > .tip { + width: 588px; + font-size: 20px; + line-height: 24px; + text-align: center; + margin-bottom: 96px; + + > .strong { + font-weight: 700; + } + } +`; + +export default function LastStep({ data, onButtonClick }) { + return ( + + Welcome to {data.spaceName} + Proudly presented by Rustchat + + More settings, including domain resolution, privileges, securities, and invites are available in{" "} + Settings + + + play icon + Start + + + ); +} diff --git a/src/routes/onboarding/steps/spaceName.js b/src/routes/onboarding/steps/spaceName.js new file mode 100644 index 00000000..170ddc73 --- /dev/null +++ b/src/routes/onboarding/steps/spaceName.js @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import StyledInput from "../../../common/component/styled/Input"; + +const StyledSpaceNameStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + color: #667085; + } +`; + +export default function SpaceNameStep({ data, setData }) { + return ( + + Let’s start with name: + This space is called: + + setData({ + ...data, + spaceName: e.target.value + }) + } + /> + + ); +} diff --git a/src/routes/onboarding/styled.js b/src/routes/onboarding/styled.js new file mode 100644 index 00000000..7328f390 --- /dev/null +++ b/src/routes/onboarding/styled.js @@ -0,0 +1,92 @@ +import styled from "styled-components"; + +const StyledOnboardingPage = styled.div` + height: 100vh; + overflow-y: auto; + + > .horizontalBox { + width: calc(100vw - 40px); + max-width: 860px; + margin: 0 auto; + padding: max(50vh - 340px, 50px) 0; + + > .verticalBox { + position: relative; + height: 680px; + border: 1px solid #f2f4f7; + border-radius: 12px; + box-shadow: 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06); + + > .buttonBack, + > .buttonNext { + position: absolute; + bottom: 32px; + } + + > .buttonBack { + left: 30px; + color: #98a2b3; + } + + > .buttonNext { + right: 30px; + } + } + } + + // shared with child components + .primaryText, + .secondaryText { + text-align: center; + } + + .primaryText { + font-weight: 700; + font-size: 24px; + line-height: 30px; + margin-bottom: 8px; + } + + .secondaryText { + font-size: 14px; + line-height: 20px; + margin-bottom: 24px; + } + + .startButton { + width: 128px; + display: flex; + flex-direction: column; + align-items: center; + padding: 15px 0 12px; + + > img { + margin-bottom: 7px; + } + + > span { + font-weight: 500; + font-size: 14px; + line-height: 20px; + } + } + + .input { + width: 360px; + height: 44px; + border: none; + box-shadow: none; + } + + input.input { + font-weight: 400; + font-size: 16px; + line-height: 24px; + padding: 10px 14px; + border: 1px solid #d0d5dd; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); + border-radius: 8px; + } +`; + +export default StyledOnboardingPage; From 78a585f1375966b3b934c858d327be5e24337dbb Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Sun, 5 Jun 2022 20:50:10 +0800 Subject: [PATCH 03/10] feat: update onboarding ui --- src/routes/onboarding/index.js | 82 ++++--------------- .../steps/{first.js => 1-welcome.js} | 4 +- src/routes/onboarding/steps/2-spaceName.js | 55 +++++++++++++ ...inCredentials.js => 3-adminCredentials.js} | 29 ++++++- .../steps/{inviteRule.js => 4-inviteRule.js} | 17 ++-- .../steps/{inviteLink.js => 5-inviteLink.js} | 5 +- .../steps/{last.js => 6-completed.js} | 6 +- src/routes/onboarding/steps/spaceName.js | 34 -------- src/routes/onboarding/styled.js | 30 ++++--- 9 files changed, 132 insertions(+), 130 deletions(-) rename src/routes/onboarding/steps/{first.js => 1-welcome.js} (82%) create mode 100644 src/routes/onboarding/steps/2-spaceName.js rename src/routes/onboarding/steps/{adminCredentials.js => 3-adminCredentials.js} (61%) rename src/routes/onboarding/steps/{inviteRule.js => 4-inviteRule.js} (62%) rename src/routes/onboarding/steps/{inviteLink.js => 5-inviteLink.js} (90%) rename src/routes/onboarding/steps/{last.js => 6-completed.js} (86%) delete mode 100644 src/routes/onboarding/steps/spaceName.js diff --git a/src/routes/onboarding/index.js b/src/routes/onboarding/index.js index 7223f3e9..042e5346 100644 --- a/src/routes/onboarding/index.js +++ b/src/routes/onboarding/index.js @@ -1,12 +1,11 @@ import { useState } from "react"; -import toast from "react-hot-toast"; -import StyledButton from "../../common/component/styled/Button"; -import FirstStep from "./steps/first"; -import SpaceNameStep from "./steps/spaceName"; -import AdminCredentialsStep from "./steps/adminCredentials"; -import InviteRuleStep from "./steps/inviteRule"; -import InviteLinkStep from "./steps/inviteLink"; -import LastStep from "./steps/last"; +import { Navigate } from "react-router-dom"; +import WelcomeStep from "./steps/1-welcome"; +import SpaceNameStep from "./steps/2-spaceName"; +import AdminCredentialsStep from "./steps/3-adminCredentials"; +import InviteRuleStep from "./steps/4-inviteRule"; +import InviteLinkStep from "./steps/5-inviteLink"; +import CompletedStep from "./steps/6-completed"; import StyledOnboardingPage from "./styled"; export default function OnboardingPage() { @@ -18,70 +17,19 @@ export default function OnboardingPage() { adminPassword2: "", inviteRule: null }); + const props = { step, setStep, data, setData }; return (
- {step > 0 && step < 5 && ( - <> - setStep(step - 1)}> - Back - - { - if (step === 1) { - // Verification for space name - if (data.spaceName === "") { - toast.error("Please enter space name!"); - return; - } - setStep(step + 1); - } else if (step === 2) { - // Verification for admin credentials - if (data.adminEmail === "") { - toast.error("Please enter admin email!"); - return; - } else if (data.adminPassword === "") { - toast.error("Please enter admin password!"); - return; - } else if (data.adminPassword !== data.adminPassword2) { - toast.error("Two passwords do not match!"); - return; - } - setStep(step + 1); - } else if (step === 3) { - // Verification for invitation rule - if (data.inviteRule === null) { - toast.error("Please choose one option!"); - return; - } - setStep(step + 1); - } else if (step === 4) { - // Verification for invitation link - setStep(step + 1); - } - }} - > - Next - - - )} - {step === 0 && setStep(step + 1)} />} - {step === 1 && } - {step === 2 && } - {step === 3 && } - {step === 4 && } - {step === 5 && ( - { - // TODO: finish it - console.log("onboarding steps completed:", data); - }} - /> - )} + {step === 0 && } + {step === 1 && } + {step === 2 && } + {step === 3 && } + {step === 4 && } + {step === 5 && } + {step === 6 && }
diff --git a/src/routes/onboarding/steps/first.js b/src/routes/onboarding/steps/1-welcome.js similarity index 82% rename from src/routes/onboarding/steps/first.js rename to src/routes/onboarding/steps/1-welcome.js index 21b96cf2..a9debbfb 100644 --- a/src/routes/onboarding/steps/first.js +++ b/src/routes/onboarding/steps/1-welcome.js @@ -10,14 +10,14 @@ const StyledFirstStep = styled.div` align-items: center; `; -export default function FirstStep({ onButtonClick }) { +export default function WelcomeStep({ step, setStep }) { return ( Welcome to your Rustchat! Everything in this space is owned by you. Let’s set up your space! - + setStep(step + 1)}> play icon Start diff --git a/src/routes/onboarding/steps/2-spaceName.js b/src/routes/onboarding/steps/2-spaceName.js new file mode 100644 index 00000000..e31a938d --- /dev/null +++ b/src/routes/onboarding/steps/2-spaceName.js @@ -0,0 +1,55 @@ +import styled from "styled-components"; +import toast from "react-hot-toast"; +import StyledInput from "../../../common/component/styled/Input"; +import StyledButton from "../../../common/component/styled/Button"; + +const StyledSpaceNameStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + color: #667085; + } + + > .button { + margin-top: 24px; + } +`; + +export default function SpaceNameStep({ step, setStep, data, setData }) { + return ( + + Create a new server + + Servers are shared environments where teams can work on projects and chat. + + + setData({ + ...data, + spaceName: e.target.value + }) + } + /> + { + // Verification for space name + if (data.spaceName === "") { + toast.error("Please enter server name!"); + return; + } + setStep(step + 1); + }} + > + Create Server + + + ); +} diff --git a/src/routes/onboarding/steps/adminCredentials.js b/src/routes/onboarding/steps/3-adminCredentials.js similarity index 61% rename from src/routes/onboarding/steps/adminCredentials.js rename to src/routes/onboarding/steps/3-adminCredentials.js index 4caafef0..3b581816 100644 --- a/src/routes/onboarding/steps/adminCredentials.js +++ b/src/routes/onboarding/steps/3-adminCredentials.js @@ -1,5 +1,7 @@ import styled from "styled-components"; import StyledInput from "../../../common/component/styled/Input"; +import StyledButton from "../../../common/component/styled/Button"; +import toast from "react-hot-toast"; const StyledAdminCredentialsStep = styled.div` height: 100%; @@ -8,12 +10,16 @@ const StyledAdminCredentialsStep = styled.div` justify-content: center; align-items: center; - > .input:not(:last-child) { + > .input:not(:nth-last-child(2)) { margin-bottom: 20px; } + + > .button { + margin-top: 24px; + } `; -export default function AdminCredentialsStep({ data, setData }) { +export default function AdminCredentialsStep({ step, setStep, data, setData }) { return ( Now let’s set up your admin account @@ -53,6 +59,25 @@ export default function AdminCredentialsStep({ data, setData }) { }) } /> + { + // Verification for admin credentials + if (data.adminEmail === "") { + toast.error("Please enter admin email!"); + return; + } else if (data.adminPassword === "") { + toast.error("Please enter admin password!"); + return; + } else if (data.adminPassword !== data.adminPassword2) { + toast.error("Two passwords do not match!"); + return; + } + setStep(step + 1); + }} + > + Sign Up + ); } diff --git a/src/routes/onboarding/steps/inviteRule.js b/src/routes/onboarding/steps/4-inviteRule.js similarity index 62% rename from src/routes/onboarding/steps/inviteRule.js rename to src/routes/onboarding/steps/4-inviteRule.js index a7e9b9d0..7738f25c 100644 --- a/src/routes/onboarding/steps/inviteRule.js +++ b/src/routes/onboarding/steps/4-inviteRule.js @@ -1,5 +1,6 @@ import styled from "styled-components"; import StyledRadio from "../../../common/component/styled/Radio"; +import StyledButton from "../../../common/component/styled/Button"; const StyledInviteRuleStep = styled.div` height: 100%; @@ -8,12 +9,12 @@ const StyledInviteRuleStep = styled.div` justify-content: center; align-items: center; - > .input:not(:last-child) { + > .input:not(:nth-last-child(2)) { margin-bottom: 20px; } `; -export default function InviteRuleStep({ data, setData }) { +export default function InviteRuleStep({ step, setStep, data, setData }) { return ( Last step: invite others! @@ -21,13 +22,19 @@ export default function InviteRuleStep({ data, setData }) { + onChange={(v) => { setData({ ...data, inviteRule: v - }) - } + }); + setTimeout(() => { + setStep(step + 1); + }, 750); + }} /> + setStep(step + 1)}> + Skip + ); } diff --git a/src/routes/onboarding/steps/inviteLink.js b/src/routes/onboarding/steps/5-inviteLink.js similarity index 90% rename from src/routes/onboarding/steps/inviteLink.js rename to src/routes/onboarding/steps/5-inviteLink.js index 57b751de..72bd6d1d 100644 --- a/src/routes/onboarding/steps/inviteLink.js +++ b/src/routes/onboarding/steps/5-inviteLink.js @@ -51,7 +51,7 @@ const StyledInviteLinkStep = styled.div` } `; -export default function InviteLinkStep() { +export default function InviteLinkStep({ step, setStep }) { const { link, linkCopied, copyLink } = useInviteLink(); return ( @@ -65,6 +65,9 @@ export default function InviteLinkStep() { {linkCopied ? "Copied" : `Copy`} + setStep(step + 1)}> + Skip + ); } diff --git a/src/routes/onboarding/steps/last.js b/src/routes/onboarding/steps/6-completed.js similarity index 86% rename from src/routes/onboarding/steps/last.js rename to src/routes/onboarding/steps/6-completed.js index 5be297cc..9aebf237 100644 --- a/src/routes/onboarding/steps/last.js +++ b/src/routes/onboarding/steps/6-completed.js @@ -14,7 +14,7 @@ const StyledLastStep = styled.div` } > .tip { - width: 588px; + width: 560px; font-size: 20px; line-height: 24px; text-align: center; @@ -26,7 +26,7 @@ const StyledLastStep = styled.div` } `; -export default function LastStep({ data, onButtonClick }) { +export default function CompletedStep({ data, step, setStep }) { return ( Welcome to {data.spaceName} @@ -35,7 +35,7 @@ export default function LastStep({ data, onButtonClick }) { More settings, including domain resolution, privileges, securities, and invites are available in{" "} Settings - + setStep(step + 1)}> play icon Start diff --git a/src/routes/onboarding/steps/spaceName.js b/src/routes/onboarding/steps/spaceName.js deleted file mode 100644 index 170ddc73..00000000 --- a/src/routes/onboarding/steps/spaceName.js +++ /dev/null @@ -1,34 +0,0 @@ -import styled from "styled-components"; -import StyledInput from "../../../common/component/styled/Input"; - -const StyledSpaceNameStep = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - > .secondaryText { - color: #667085; - } -`; - -export default function SpaceNameStep({ data, setData }) { - return ( - - Let’s start with name: - This space is called: - - setData({ - ...data, - spaceName: e.target.value - }) - } - /> - - ); -} diff --git a/src/routes/onboarding/styled.js b/src/routes/onboarding/styled.js index 7328f390..39e01b9c 100644 --- a/src/routes/onboarding/styled.js +++ b/src/routes/onboarding/styled.js @@ -16,21 +16,6 @@ const StyledOnboardingPage = styled.div` border: 1px solid #f2f4f7; border-radius: 12px; box-shadow: 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06); - - > .buttonBack, - > .buttonNext { - position: absolute; - bottom: 32px; - } - - > .buttonBack { - left: 30px; - color: #98a2b3; - } - - > .buttonNext { - right: 30px; - } } } @@ -84,8 +69,21 @@ const StyledOnboardingPage = styled.div` line-height: 24px; padding: 10px 14px; border: 1px solid #d0d5dd; - box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); border-radius: 8px; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); + } + + .button { + width: 360px; + + &.ghost.border_less { + width: fit-content; + font-weight: 500; + font-size: 16px; + line-height: 24px; + color: #98a2b3; + margin-top: 14px; + } } `; From defacf6b63b25b33dd6867150457caa9d2fb51de Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Sun, 5 Jun 2022 23:58:13 +0800 Subject: [PATCH 04/10] feat: connect onboarding to APIs --- src/app/services/base.query.js | 186 ++++---- src/app/services/server.js | 406 +++++++++--------- src/common/component/RequireInitialized.js | 8 + src/routes/index.js | 17 +- src/routes/onboarding/index.js | 6 +- .../onboarding/steps/3-adminCredentials.js | 83 ++-- src/routes/onboarding/steps/4-inviteRule.js | 38 +- 7 files changed, 404 insertions(+), 340 deletions(-) create mode 100644 src/common/component/RequireInitialized.js diff --git a/src/app/services/base.query.js b/src/app/services/base.query.js index 70c03cf6..457fe2b5 100644 --- a/src/app/services/base.query.js +++ b/src/app/services/base.query.js @@ -4,110 +4,106 @@ import dayjs from "dayjs"; import { updateToken, resetAuthData } from "../slices/auth.data"; import BASE_URL, { tokenHeader } from "../config"; const whiteList = [ - "login", - "register", - "sendMagicLink", - "checkInviteTokenValid", - "getGoogleAuthConfig", - "getGithubAuthConfig", - "getSMTPStatus", - "getLoginConfig", - "getServerVersion", - "getServer", - "getOpenid", - "getMetamaskNonce", - "renew", + "login", + "register", + "sendMagicLink", + "checkInviteTokenValid", + "getGoogleAuthConfig", + "getGithubAuthConfig", + "getSMTPStatus", + "getLoginConfig", + "getServerVersion", + "getServer", + "getOpenid", + "getMetamaskNonce", + "renew", + "getInitialized", + "createAdmin", + "updateLoginConfig" ]; const baseQuery = fetchBaseQuery({ - baseUrl: BASE_URL, - prepareHeaders: (headers, { getState, endpoint }) => { - console.log("req", endpoint); - const { token } = getState().authData; - if (token && !whiteList.includes(endpoint)) { - headers.set(tokenHeader, token); - } - return headers; - }, + baseUrl: BASE_URL, + prepareHeaders: (headers, { getState, endpoint }) => { + console.log("req", endpoint); + const { token } = getState().authData; + if (token && !whiteList.includes(endpoint)) { + headers.set(tokenHeader, token); + } + return headers; + } }); let waitingForRenew = null; const baseQueryWithTokenCheck = async (args, api, extraOptions) => { - if (waitingForRenew) { - await waitingForRenew; - } - // 先检查token是否过期,过期则renew - const { - token, - refreshToken, - expireTime = new Date().getTime(), - } = api.getState().authData; - let result = null; - if ( - !whiteList.includes(api.endpoint) && - dayjs().isAfter(new Date(expireTime - 20 * 1000)) - ) { - // 快过期了,renew - waitingForRenew = baseQuery( - { - url: "/token/renew", - method: "POST", - body: { - token, - refresh_token: refreshToken, - }, - }, - api, - extraOptions - ); - result = await waitingForRenew; - waitingForRenew = null; - if (result.data) { - // store the new token - api.dispatch(updateToken(result.data)); - // retry the initial query - result = await baseQuery(args, api, extraOptions); + if (waitingForRenew) { + await waitingForRenew; + } + // 先检查token是否过期,过期则renew + const { token, refreshToken, expireTime = new Date().getTime() } = api.getState().authData; + let result = null; + if (!whiteList.includes(api.endpoint) && dayjs().isAfter(new Date(expireTime - 20 * 1000))) { + // 快过期了,renew + waitingForRenew = baseQuery( + { + url: "/token/renew", + method: "POST", + body: { + token, + refresh_token: refreshToken } - } else { - result = await baseQuery(args, api, extraOptions); + }, + api, + extraOptions + ); + result = await waitingForRenew; + waitingForRenew = null; + if (result.data) { + // store the new token + api.dispatch(updateToken(result.data)); + // retry the initial query + result = await baseQuery(args, api, extraOptions); } - if (result?.error) { - console.log("api error", result.error, api.endpoint); - switch (result.error.originalStatus || result.error.status) { - case "FETCH_ERROR": - { - toast.error(`${api.endpoint}: Failed to fetch`); - } - break; - case 404: - { - toast.error("Request Not Found"); - } - break; - case 500: - { - toast.error(result.error.data || "server error"); - } - break; - case 401: - { - if (api.endpoint !== "login") { - api.dispatch(resetAuthData()); - location.href = "/#/login"; - toast.error("API Not Authenticated"); - } - // toast.error("token expired, please login again"); - // } else { - // return; - // } - } - break; - case 403: - toast.error("Request Not Allowed"); - break; - default: - break; + } else { + result = await baseQuery(args, api, extraOptions); + } + if (result?.error) { + console.log("api error", result.error, api.endpoint); + switch (result.error.originalStatus || result.error.status) { + case "FETCH_ERROR": + { + toast.error(`${api.endpoint}: Failed to fetch`); } + break; + case 404: + { + toast.error("Request Not Found"); + } + break; + case 500: + { + toast.error(result.error.data || "server error"); + } + break; + case 401: + { + if (api.endpoint !== "login") { + api.dispatch(resetAuthData()); + location.href = "/#/login"; + toast.error("API Not Authenticated"); + } + // toast.error("token expired, please login again"); + // } else { + // return; + // } + } + break; + case 403: + toast.error("Request Not Allowed"); + break; + default: + break; } - return result; + } + return result; }; export default baseQueryWithTokenCheck; diff --git a/src/app/services/server.js b/src/app/services/server.js index 21925753..6cbe4831 100644 --- a/src/app/services/server.js +++ b/src/app/services/server.js @@ -4,204 +4,218 @@ import { updateInfo } from "../slices/server"; import baseQuery from "./base.query"; const defaultExpireDuration = 7 * 24 * 60 * 60; export const serverApi = createApi({ - reducerPath: "serverApi", - baseQuery, - endpoints: (builder) => ({ - getServer: builder.query({ - query: () => ({ url: `admin/system/organization` }), - transformResponse: (data) => { - data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`; - return data; - }, - async onQueryStarted(data, { dispatch, queryFulfilled }) { - try { - const { data: server } = await queryFulfilled; - dispatch(updateInfo(server)); - } catch { - console.log("get server info error"); - } - }, - }), - getThirdPartySecret: builder.query({ - query: () => ({ - // headers: { - // "content-type": "text/plain", - // accept: "text/plain", - // }, - url: `/admin/system/third_party_secret`, - responseHandler: (response) => response.text(), - }), - keepUnusedDataFor: 0, - }), - updateThirdPartySecret: builder.mutation({ - query: () => ({ - url: `/admin/system/third_party_secret`, - method: "POST", - responseHandler: (response) => response.text(), - }), - }), - getMetrics: builder.query({ - query: () => ({ url: `/admin/system/metrics` }), - }), - getServerVersion: builder.query({ - query: () => ({ - headers: { - // "content-type": "text/plain", - accept: "text/plain", - }, - url: `/admin/system/version`, - responseHandler: (response) => response.text(), - }), - }), - getFirebaseConfig: builder.query({ - query: () => ({ url: `admin/fcm/config` }), - }), - getGoogleAuthConfig: builder.query({ - query: () => ({ url: `admin/google_auth/config` }), - }), - updateGoogleAuthConfig: builder.mutation({ - query: (data) => ({ - url: `admin/google_auth/config`, - method: "POST", - body: data, - }), - }), - getGithubAuthConfig: builder.query({ - query: () => ({ url: `admin/github_auth/config` }), - }), - updateGithubAuthConfig: builder.mutation({ - query: (data) => ({ - url: `admin/github_auth/config`, - method: "POST", - body: data, - }), - }), - sendTestEmail: builder.mutation({ - query: (data) => ({ - url: `/admin/system/send_mail`, - method: "POST", - body: data, - }), - }), - updateFirebaseConfig: builder.mutation({ - query: (data) => ({ - url: `admin/fcm/config`, - method: "POST", - body: data, - }), - }), - getAgoraConfig: builder.query({ - query: () => ({ url: `admin/agora/config` }), - }), - updateAgoraConfig: builder.mutation({ - query: (data) => ({ - url: `admin/agora/config`, - method: "POST", - body: data, - }), - }), - getSMTPConfig: builder.query({ - query: () => ({ url: `admin/smtp/config` }), - }), - getSMTPStatus: builder.query({ - query: () => ({ url: `/admin/smtp/enabled` }), - }), - updateSMTPConfig: builder.mutation({ - query: (data) => ({ - url: `admin/smtp/config`, - method: "POST", - body: data, - }), - }), - getLoginConfig: builder.query({ - query: () => ({ url: `admin/login/config` }), - }), - updateLoginConfig: builder.mutation({ - query: (data) => ({ - url: `admin/login/config`, - method: "POST", - body: data, - }), - }), - updateLogo: builder.mutation({ - query: (data) => ({ - headers: { - "content-type": "image/png", - }, - url: `admin/system/organization/logo`, - method: "POST", - body: data, - }), - async onQueryStarted(data, { dispatch, queryFulfilled }) { - try { - await queryFulfilled; - dispatch( - updateInfo({ - logo: `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`, - }) - ); - } catch { - console.log("update server logo error"); - } - }, - }), - createInviteLink: builder.query({ - query: (expired_in = defaultExpireDuration) => ({ - headers: { - "content-type": "text/plain", - accept: "text/plain", - }, - url: `/admin/system/create_invite_link?expired_in=${expired_in}`, - responseHandler: (response) => response.text(), - }), - transformResponse: (link) => { - // 替换掉域名 - const invite = new URL(link); - return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`; - }, - }), - updateServer: builder.mutation({ - query: (data) => ({ - url: `admin/system/organization`, - method: "POST", - body: data, - }), - async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { - const { name: prevName, description: prevDesc } = getState().server; - dispatch(updateInfo(data)); - try { - await queryFulfilled; - } catch { - dispatch(updateInfo({ name: prevName, description: prevDesc })); - } - }, - }), + reducerPath: "serverApi", + baseQuery, + endpoints: (builder) => ({ + getServer: builder.query({ + query: () => ({ url: `admin/system/organization` }), + transformResponse: (data) => { + data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`; + return data; + }, + async onQueryStarted(data, { dispatch, queryFulfilled }) { + try { + const { data: server } = await queryFulfilled; + dispatch(updateInfo(server)); + } catch { + console.log("get server info error"); + } + } }), + getThirdPartySecret: builder.query({ + query: () => ({ + // headers: { + // "content-type": "text/plain", + // accept: "text/plain", + // }, + url: `/admin/system/third_party_secret`, + responseHandler: (response) => response.text() + }), + keepUnusedDataFor: 0 + }), + updateThirdPartySecret: builder.mutation({ + query: () => ({ + url: `/admin/system/third_party_secret`, + method: "POST", + responseHandler: (response) => response.text() + }) + }), + getMetrics: builder.query({ + query: () => ({ url: `/admin/system/metrics` }) + }), + getServerVersion: builder.query({ + query: () => ({ + headers: { + // "content-type": "text/plain", + accept: "text/plain" + }, + url: `/admin/system/version`, + responseHandler: (response) => response.text() + }) + }), + getFirebaseConfig: builder.query({ + query: () => ({ url: `admin/fcm/config` }) + }), + getGoogleAuthConfig: builder.query({ + query: () => ({ url: `admin/google_auth/config` }) + }), + updateGoogleAuthConfig: builder.mutation({ + query: (data) => ({ + url: `admin/google_auth/config`, + method: "POST", + body: data + }) + }), + getGithubAuthConfig: builder.query({ + query: () => ({ url: `admin/github_auth/config` }) + }), + updateGithubAuthConfig: builder.mutation({ + query: (data) => ({ + url: `admin/github_auth/config`, + method: "POST", + body: data + }) + }), + sendTestEmail: builder.mutation({ + query: (data) => ({ + url: `/admin/system/send_mail`, + method: "POST", + body: data + }) + }), + updateFirebaseConfig: builder.mutation({ + query: (data) => ({ + url: `admin/fcm/config`, + method: "POST", + body: data + }) + }), + getAgoraConfig: builder.query({ + query: () => ({ url: `admin/agora/config` }) + }), + updateAgoraConfig: builder.mutation({ + query: (data) => ({ + url: `admin/agora/config`, + method: "POST", + body: data + }) + }), + getSMTPConfig: builder.query({ + query: () => ({ url: `admin/smtp/config` }) + }), + getSMTPStatus: builder.query({ + query: () => ({ url: `/admin/smtp/enabled` }) + }), + updateSMTPConfig: builder.mutation({ + query: (data) => ({ + url: `admin/smtp/config`, + method: "POST", + body: data + }) + }), + getLoginConfig: builder.query({ + query: () => ({ url: `admin/login/config` }) + }), + updateLoginConfig: builder.mutation({ + query: (data) => ({ + url: `admin/login/config`, + method: "POST", + body: data + }) + }), + updateLogo: builder.mutation({ + query: (data) => ({ + headers: { + "content-type": "image/png" + }, + url: `admin/system/organization/logo`, + method: "POST", + body: data + }), + async onQueryStarted(data, { dispatch, queryFulfilled }) { + try { + await queryFulfilled; + dispatch( + updateInfo({ + logo: `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}` + }) + ); + } catch { + console.log("update server logo error"); + } + } + }), + createInviteLink: builder.query({ + query: (expired_in = defaultExpireDuration) => ({ + headers: { + "content-type": "text/plain", + accept: "text/plain" + }, + url: `/admin/system/create_invite_link?expired_in=${expired_in}`, + responseHandler: (response) => response.text() + }), + transformResponse: (link) => { + // 替换掉域名 + const invite = new URL(link); + return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`; + } + }), + updateServer: builder.mutation({ + query: (data) => ({ + url: `admin/system/organization`, + method: "POST", + body: data + }), + async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { + const { name: prevName, description: prevDesc } = getState().server; + dispatch(updateInfo(data)); + try { + await queryFulfilled; + } catch { + dispatch(updateInfo({ name: prevName, description: prevDesc })); + } + } + }), + createAdmin: builder.mutation({ + query: (data) => ({ + url: `/admin/system/createAdmin`, + method: "POST", + body: data + }) + }), + getInitialized: builder.query({ + query: () => ({ + url: `/admin/system/initialized` + }) + }) + }) }); export const { - useGetServerVersionQuery, - useGetGithubAuthConfigQuery, - useUpdateGithubAuthConfigMutation, - useGetGoogleAuthConfigQuery, - useUpdateGoogleAuthConfigMutation, - useGetSMTPStatusQuery, - useSendTestEmailMutation, - useUpdateFirebaseConfigMutation, - useGetFirebaseConfigQuery, - useGetLoginConfigQuery, - useUpdateLoginConfigMutation, - useGetSMTPConfigQuery, - useUpdateSMTPConfigMutation, - useGetAgoraConfigQuery, - useUpdateAgoraConfigMutation, - useGetServerQuery, - useGetMetricsQuery, - useLazyGetServerQuery, - useUpdateServerMutation, - useUpdateLogoMutation, - useCreateInviteLinkQuery, - useLazyCreateInviteLinkQuery, - useGetThirdPartySecretQuery, - useUpdateThirdPartySecretMutation, + useGetServerVersionQuery, + useGetGithubAuthConfigQuery, + useUpdateGithubAuthConfigMutation, + useGetGoogleAuthConfigQuery, + useUpdateGoogleAuthConfigMutation, + useGetSMTPStatusQuery, + useSendTestEmailMutation, + useUpdateFirebaseConfigMutation, + useGetFirebaseConfigQuery, + useGetLoginConfigQuery, + useUpdateLoginConfigMutation, + useGetSMTPConfigQuery, + useUpdateSMTPConfigMutation, + useGetAgoraConfigQuery, + useUpdateAgoraConfigMutation, + useGetServerQuery, + useGetMetricsQuery, + useLazyGetServerQuery, + useUpdateServerMutation, + useUpdateLogoMutation, + useCreateInviteLinkQuery, + useLazyCreateInviteLinkQuery, + useGetThirdPartySecretQuery, + useUpdateThirdPartySecretMutation, + useCreateAdminMutation, + useGetInitializedQuery } = serverApi; diff --git a/src/common/component/RequireInitialized.js b/src/common/component/RequireInitialized.js new file mode 100644 index 00000000..035b2d16 --- /dev/null +++ b/src/common/component/RequireInitialized.js @@ -0,0 +1,8 @@ +import { Navigate } from "react-router-dom"; +import { useGetInitializedQuery } from "../../app/services/server"; + +export default function RequireInitialized({ children, redirectTo = "/" }) { + const { data } = useGetInitializedQuery(); + console.log("initialized?", data); + return data === false ? : children; +} diff --git a/src/routes/index.js b/src/routes/index.js index 58d40f9e..b8a4d70d 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -24,6 +24,7 @@ import SettingChannelPage from "./settingChannel"; import OnboardingPage from "./onboarding"; import toast from "react-hot-toast"; import ResourceManagement from "./resources"; +import RequireInitialized from "../common/component/RequireInitialized"; const PageRoutes = () => { const { @@ -49,9 +50,11 @@ const PageRoutes = () => { - - + + + + + } /> { - - + + + + + } > diff --git a/src/routes/onboarding/index.js b/src/routes/onboarding/index.js index 042e5346..e588b09b 100644 --- a/src/routes/onboarding/index.js +++ b/src/routes/onboarding/index.js @@ -11,11 +11,7 @@ import StyledOnboardingPage from "./styled"; export default function OnboardingPage() { const [step, setStep] = useState(0); const [data, setData] = useState({ - spaceName: "", - adminEmail: "", - adminPassword: "", - adminPassword2: "", - inviteRule: null + spaceName: "" }); const props = { step, setStep, data, setData }; diff --git a/src/routes/onboarding/steps/3-adminCredentials.js b/src/routes/onboarding/steps/3-adminCredentials.js index 3b581816..cadb2c20 100644 --- a/src/routes/onboarding/steps/3-adminCredentials.js +++ b/src/routes/onboarding/steps/3-adminCredentials.js @@ -1,7 +1,10 @@ +import { useEffect, useState } from "react"; import styled from "styled-components"; +import toast from "react-hot-toast"; import StyledInput from "../../../common/component/styled/Input"; import StyledButton from "../../../common/component/styled/Button"; -import toast from "react-hot-toast"; +import { useCreateAdminMutation } from "../../../app/services/server"; +import { useLoginMutation } from "../../../app/services/auth"; const StyledAdminCredentialsStep = styled.div` height: 100%; @@ -19,7 +22,34 @@ const StyledAdminCredentialsStep = styled.div` } `; -export default function AdminCredentialsStep({ step, setStep, data, setData }) { +export default function AdminCredentialsStep({ step, setStep }) { + const [ + createAdmin, + { isLoading: isSignUpLoading, isSuccess: isSignUpSuccess, error: signUpError } + ] = useCreateAdminMutation(); + const [login, { isLoading: isLoginLoading, isSuccess: isLoginSuccess, error: loginError }] = + useLoginMutation(); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + + // Display error + useEffect(() => { + if (signUpError === undefined) return; + toast.error(`Failed to sign up: ${signUpError.data}`); + }, [signUpError]); + + useEffect(() => { + if (loginError === undefined) return; + toast.error(`Login failed: ${loginError.data}`); + }, [loginError]); + + // Increment `step` when both signing up and logging in have completed + useEffect(() => { + if (isSignUpSuccess && isLoginSuccess) setStep(step + 1); + }, [isSignUpSuccess, isLoginSuccess]); + return ( Now let’s set up your admin account @@ -27,56 +57,51 @@ export default function AdminCredentialsStep({ step, setStep, data, setData }) { - setData({ - ...data, - adminEmail: e.target.value - }) - } + value={email} + onChange={(e) => setEmail(e.target.value)} /> - setData({ - ...data, - adminPassword: e.target.value - }) - } + value={password} + onChange={(e) => setPassword(e.target.value)} /> - setData({ - ...data, - adminPassword2: e.target.value - }) - } + value={confirm} + onChange={(e) => setConfirm(e.target.value)} /> { + onClick={async () => { // Verification for admin credentials - if (data.adminEmail === "") { + if (email === "") { toast.error("Please enter admin email!"); return; - } else if (data.adminPassword === "") { + } else if (password === "") { toast.error("Please enter admin password!"); return; - } else if (data.adminPassword !== data.adminPassword2) { + } else if (password !== confirm) { toast.error("Two passwords do not match!"); return; } - setStep(step + 1); + await createAdmin({ + email, + name: "Admin", + password, + gender: 0 + }); + await login({ + email, + password, + type: "password" + }); }} > - Sign Up + {!(isSignUpLoading || isLoginLoading) ? "Sign Up" : "..."} ); diff --git a/src/routes/onboarding/steps/4-inviteRule.js b/src/routes/onboarding/steps/4-inviteRule.js index 7738f25c..9b1aa36f 100644 --- a/src/routes/onboarding/steps/4-inviteRule.js +++ b/src/routes/onboarding/steps/4-inviteRule.js @@ -1,6 +1,9 @@ +import { useState, useEffect } from "react"; +import toast from "react-hot-toast"; import styled from "styled-components"; import StyledRadio from "../../../common/component/styled/Radio"; import StyledButton from "../../../common/component/styled/Button"; +import { useGetLoginConfigQuery, useUpdateLoginConfigMutation } from "../../../app/services/server"; const StyledInviteRuleStep = styled.div` height: 100%; @@ -14,22 +17,39 @@ const StyledInviteRuleStep = styled.div` } `; -export default function InviteRuleStep({ step, setStep, data, setData }) { +export default function InviteRuleStep({ step, setStep }) { + const { data: loginConfig } = useGetLoginConfigQuery(); + const [updateLoginConfig, { isSuccess, error }] = useUpdateLoginConfigMutation(); + + const [value, setValue] = useState(null); + + // Display error + useEffect(() => { + if (error === undefined) return; + toast.error(`Failed to update invitation rule: ${error.data}`); + }, [error]); + + // Increment `step` when updating has completed + useEffect(() => { + if (isSuccess) setStep(step + 1); + }, [isSuccess]); + return ( Last step: invite others! Firstly, who can sign up to this server? { - setData({ - ...data, - inviteRule: v - }); - setTimeout(() => { - setStep(step + 1); - }, 750); + setValue(v); + if (loginConfig !== undefined) { + const whoCanSignUp = ["EveryOne", "InvitationOnly"][v]; + updateLoginConfig({ + ...loginConfig, + who_can_sign_up: whoCanSignUp + }); + } }} /> setStep(step + 1)}> From befe35f3d6355a12546865de42d3a5ab18c1ab90 Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Mon, 6 Jun 2022 15:07:17 +0800 Subject: [PATCH 05/10] fix: get onboarding working --- src/app/services/base.query.js | 3 +- src/app/services/server.js | 2 +- src/common/component/RequireInitialized.js | 2 +- src/routes/onboarding/index.js | 36 +++++++------ src/routes/onboarding/steps/1-welcome.js | 4 +- .../steps/{2-spaceName.js => 2-serverName.js} | 10 ++-- .../onboarding/steps/3-adminCredentials.js | 52 ++++++++++++++----- src/routes/onboarding/steps/4-inviteRule.js | 12 ++--- src/routes/onboarding/steps/5-inviteLink.js | 4 +- src/routes/onboarding/steps/6-completed.js | 6 +-- 10 files changed, 82 insertions(+), 49 deletions(-) rename src/routes/onboarding/steps/{2-spaceName.js => 2-serverName.js} (84%) diff --git a/src/app/services/base.query.js b/src/app/services/base.query.js index 457fe2b5..66f12e8a 100644 --- a/src/app/services/base.query.js +++ b/src/app/services/base.query.js @@ -18,8 +18,7 @@ const whiteList = [ "getMetamaskNonce", "renew", "getInitialized", - "createAdmin", - "updateLoginConfig" + "createAdmin" ]; const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL, diff --git a/src/app/services/server.js b/src/app/services/server.js index 6cbe4831..7ab67a55 100644 --- a/src/app/services/server.js +++ b/src/app/services/server.js @@ -178,7 +178,7 @@ export const serverApi = createApi({ }), createAdmin: builder.mutation({ query: (data) => ({ - url: `/admin/system/createAdmin`, + url: `/admin/system/create_admin`, method: "POST", body: data }) diff --git a/src/common/component/RequireInitialized.js b/src/common/component/RequireInitialized.js index 035b2d16..275a3796 100644 --- a/src/common/component/RequireInitialized.js +++ b/src/common/component/RequireInitialized.js @@ -1,7 +1,7 @@ import { Navigate } from "react-router-dom"; import { useGetInitializedQuery } from "../../app/services/server"; -export default function RequireInitialized({ children, redirectTo = "/" }) { +export default function RequireInitialized({ children, redirectTo = "/onboarding" }) { const { data } = useGetInitializedQuery(); console.log("initialized?", data); return data === false ? : children; diff --git a/src/routes/onboarding/index.js b/src/routes/onboarding/index.js index e588b09b..72f83606 100644 --- a/src/routes/onboarding/index.js +++ b/src/routes/onboarding/index.js @@ -1,7 +1,8 @@ import { useState } from "react"; import { Navigate } from "react-router-dom"; +import { Helmet } from "react-helmet"; import WelcomeStep from "./steps/1-welcome"; -import SpaceNameStep from "./steps/2-spaceName"; +import ServerNameStep from "./steps/2-serverName"; import AdminCredentialsStep from "./steps/3-adminCredentials"; import InviteRuleStep from "./steps/4-inviteRule"; import InviteLinkStep from "./steps/5-inviteLink"; @@ -11,23 +12,28 @@ import StyledOnboardingPage from "./styled"; export default function OnboardingPage() { const [step, setStep] = useState(0); const [data, setData] = useState({ - spaceName: "" + serverName: "" }); - const props = { step, setStep, data, setData }; + const props = { setStep, data, setData }; return ( - -
-
- {step === 0 && } - {step === 1 && } - {step === 2 && } - {step === 3 && } - {step === 4 && } - {step === 5 && } - {step === 6 && } + <> + + Rustchat Setup + + +
+
+ {step === 0 && } + {step === 1 && } + {step === 2 && } + {step === 3 && } + {step === 4 && } + {step === 5 && } + {step === 6 && } +
-
- + + ); } diff --git a/src/routes/onboarding/steps/1-welcome.js b/src/routes/onboarding/steps/1-welcome.js index a9debbfb..6d274acc 100644 --- a/src/routes/onboarding/steps/1-welcome.js +++ b/src/routes/onboarding/steps/1-welcome.js @@ -10,14 +10,14 @@ const StyledFirstStep = styled.div` align-items: center; `; -export default function WelcomeStep({ step, setStep }) { +export default function WelcomeStep({ setStep }) { return ( Welcome to your Rustchat! Everything in this space is owned by you. Let’s set up your space! - setStep(step + 1)}> + setStep((prev) => prev + 1)}> play icon Start diff --git a/src/routes/onboarding/steps/2-spaceName.js b/src/routes/onboarding/steps/2-serverName.js similarity index 84% rename from src/routes/onboarding/steps/2-spaceName.js rename to src/routes/onboarding/steps/2-serverName.js index e31a938d..8fe3d849 100644 --- a/src/routes/onboarding/steps/2-spaceName.js +++ b/src/routes/onboarding/steps/2-serverName.js @@ -19,7 +19,7 @@ const StyledSpaceNameStep = styled.div` } `; -export default function SpaceNameStep({ step, setStep, data, setData }) { +export default function ServerNameStep({ setStep, data, setData }) { return ( Create a new server @@ -29,11 +29,11 @@ export default function SpaceNameStep({ step, setStep, data, setData }) { setData({ ...data, - spaceName: e.target.value + serverName: e.target.value }) } /> @@ -41,11 +41,11 @@ export default function SpaceNameStep({ step, setStep, data, setData }) { className="button" onClick={() => { // Verification for space name - if (data.spaceName === "") { + if (data.serverName === "") { toast.error("Please enter server name!"); return; } - setStep(step + 1); + setStep((prev) => prev + 1); }} > Create Server diff --git a/src/routes/onboarding/steps/3-adminCredentials.js b/src/routes/onboarding/steps/3-adminCredentials.js index cadb2c20..88b8fca4 100644 --- a/src/routes/onboarding/steps/3-adminCredentials.js +++ b/src/routes/onboarding/steps/3-adminCredentials.js @@ -1,10 +1,17 @@ import { useEffect, useState } from "react"; import styled from "styled-components"; import toast from "react-hot-toast"; +import { useDispatch } from "react-redux"; import StyledInput from "../../../common/component/styled/Input"; import StyledButton from "../../../common/component/styled/Button"; -import { useCreateAdminMutation } from "../../../app/services/server"; +import { + useCreateAdminMutation, + useGetInitializedQuery, + useGetServerQuery, + useUpdateServerMutation +} from "../../../app/services/server"; import { useLoginMutation } from "../../../app/services/auth"; +import { setAuthData } from "../../../app/slices/auth.data"; const StyledAdminCredentialsStep = styled.div` height: 100%; @@ -22,13 +29,18 @@ const StyledAdminCredentialsStep = styled.div` } `; -export default function AdminCredentialsStep({ step, setStep }) { +export default function AdminCredentialsStep({ data, setStep }) { + const dispatch = useDispatch(); + + const [createAdmin, { isLoading: isSigningUp, error: signUpError }] = useCreateAdminMutation(); const [ - createAdmin, - { isLoading: isSignUpLoading, isSuccess: isSignUpSuccess, error: signUpError } - ] = useCreateAdminMutation(); - const [login, { isLoading: isLoginLoading, isSuccess: isLoginSuccess, error: loginError }] = - useLoginMutation(); + login, + { isLoading: isLoggingIn, isSuccess: isLoggedIn, error: loginError, data: loginData } + ] = useLoginMutation(); + const { refetch: refetchInitialized } = useGetInitializedQuery(); + const { data: serverData } = useGetServerQuery(); + const [updateServer, { isLoading: isUpdatingServer, isSuccess: isUpdatedServer }] = + useUpdateServerMutation(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -39,16 +51,32 @@ export default function AdminCredentialsStep({ step, setStep }) { if (signUpError === undefined) return; toast.error(`Failed to sign up: ${signUpError.data}`); }, [signUpError]); - useEffect(() => { if (loginError === undefined) return; toast.error(`Login failed: ${loginError.data}`); }, [loginError]); - // Increment `step` when both signing up and logging in have completed + // After logged in useEffect(() => { - if (isSignUpSuccess && isLoginSuccess) setStep(step + 1); - }, [isSignUpSuccess, isLoginSuccess]); + if (isLoggedIn && loginData) { + // Update local auth data + dispatch(setAuthData(loginData)); + // Update initialized state + refetchInitialized(); + // Set server name + updateServer({ + ...serverData, + name: data.serverName + }); + } + }, [isLoggedIn, loginData]); + + // After updated server + useEffect(() => { + if (isUpdatedServer) { + setStep((prev) => prev + 1); + } + }, [isUpdatedServer]); return ( @@ -101,7 +129,7 @@ export default function AdminCredentialsStep({ step, setStep }) { }); }} > - {!(isSignUpLoading || isLoginLoading) ? "Sign Up" : "..."} + {!(isSigningUp || isLoggingIn || isUpdatingServer) ? "Sign Up" : "..."} ); diff --git a/src/routes/onboarding/steps/4-inviteRule.js b/src/routes/onboarding/steps/4-inviteRule.js index 9b1aa36f..0945e9a0 100644 --- a/src/routes/onboarding/steps/4-inviteRule.js +++ b/src/routes/onboarding/steps/4-inviteRule.js @@ -17,11 +17,11 @@ const StyledInviteRuleStep = styled.div` } `; -export default function InviteRuleStep({ step, setStep }) { +export default function InviteRuleStep({ setStep }) { const { data: loginConfig } = useGetLoginConfigQuery(); const [updateLoginConfig, { isSuccess, error }] = useUpdateLoginConfigMutation(); - const [value, setValue] = useState(null); + const [value, setValue] = useState(0); // Display error useEffect(() => { @@ -31,7 +31,7 @@ export default function InviteRuleStep({ step, setStep }) { // Increment `step` when updating has completed useEffect(() => { - if (isSuccess) setStep(step + 1); + if (isSuccess) setStep((prev) => prev + 1); }, [isSuccess]); return ( @@ -41,18 +41,18 @@ export default function InviteRuleStep({ step, setStep }) { { + onChange={async (v) => { setValue(v); if (loginConfig !== undefined) { const whoCanSignUp = ["EveryOne", "InvitationOnly"][v]; - updateLoginConfig({ + await updateLoginConfig({ ...loginConfig, who_can_sign_up: whoCanSignUp }); } }} /> - setStep(step + 1)}> + setStep((prev) => prev + 1)}> Skip diff --git a/src/routes/onboarding/steps/5-inviteLink.js b/src/routes/onboarding/steps/5-inviteLink.js index 72bd6d1d..14e79c85 100644 --- a/src/routes/onboarding/steps/5-inviteLink.js +++ b/src/routes/onboarding/steps/5-inviteLink.js @@ -51,7 +51,7 @@ const StyledInviteLinkStep = styled.div` } `; -export default function InviteLinkStep({ step, setStep }) { +export default function InviteLinkStep({ setStep }) { const { link, linkCopied, copyLink } = useInviteLink(); return ( @@ -65,7 +65,7 @@ export default function InviteLinkStep({ step, setStep }) { {linkCopied ? "Copied" : `Copy`}
- setStep(step + 1)}> + setStep((prev) => prev + 1)}> Skip diff --git a/src/routes/onboarding/steps/6-completed.js b/src/routes/onboarding/steps/6-completed.js index 9aebf237..c31cb764 100644 --- a/src/routes/onboarding/steps/6-completed.js +++ b/src/routes/onboarding/steps/6-completed.js @@ -26,16 +26,16 @@ const StyledLastStep = styled.div` } `; -export default function CompletedStep({ data, step, setStep }) { +export default function CompletedStep({ data, setStep }) { return ( - Welcome to {data.spaceName} + Welcome to {data.serverName} Proudly presented by Rustchat More settings, including domain resolution, privileges, securities, and invites are available in{" "} Settings - setStep(step + 1)}> + setStep((prev) => prev + 1)}> play icon Start From b490a864f19359bb92f9065d11224c640ee41367 Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Mon, 6 Jun 2022 15:12:48 +0800 Subject: [PATCH 06/10] fix: remove frame in onboarding UI --- src/routes/onboarding/index.js | 18 +++++++----------- src/routes/onboarding/styled.js | 15 --------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/routes/onboarding/index.js b/src/routes/onboarding/index.js index 72f83606..f530cba3 100644 --- a/src/routes/onboarding/index.js +++ b/src/routes/onboarding/index.js @@ -22,17 +22,13 @@ export default function OnboardingPage() { Rustchat Setup -
-
- {step === 0 && } - {step === 1 && } - {step === 2 && } - {step === 3 && } - {step === 4 && } - {step === 5 && } - {step === 6 && } -
-
+ {step === 0 && } + {step === 1 && } + {step === 2 && } + {step === 3 && } + {step === 4 && } + {step === 5 && } + {step === 6 && }
); diff --git a/src/routes/onboarding/styled.js b/src/routes/onboarding/styled.js index 39e01b9c..bcf409ad 100644 --- a/src/routes/onboarding/styled.js +++ b/src/routes/onboarding/styled.js @@ -4,21 +4,6 @@ const StyledOnboardingPage = styled.div` height: 100vh; overflow-y: auto; - > .horizontalBox { - width: calc(100vw - 40px); - max-width: 860px; - margin: 0 auto; - padding: max(50vh - 340px, 50px) 0; - - > .verticalBox { - position: relative; - height: 680px; - border: 1px solid #f2f4f7; - border-radius: 12px; - box-shadow: 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06); - } - } - // shared with child components .primaryText, .secondaryText { From 2360dba7e441e82b740ad8dcd40e828d5ae10a64 Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Mon, 6 Jun 2022 19:58:13 +0800 Subject: [PATCH 07/10] fix: minor fixes --- package.json | 1 - src/common/component/styled/Radio.js | 6 +++--- src/routes/onboarding/steps/6-completed.js | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e724d048..d135df5a 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "styled-reset": "^4.4.1", "terser-webpack-plugin": "^5.3.3", "tippy.js": "^6.3.7", - "uuid": "^8.3.2", "webpack": "^5.73.0", "webpack-dev-server": "^4.9.1", "webpack-manifest-plugin": "^5.0.0", diff --git a/src/common/component/styled/Radio.js b/src/common/component/styled/Radio.js index 6e0cfd06..ef03f715 100644 --- a/src/common/component/styled/Radio.js +++ b/src/common/component/styled/Radio.js @@ -1,6 +1,6 @@ -import styled from "styled-components"; -import { v4 as UUIDv4 } from "uuid"; import { useRef, useState } from "react"; +import styled from "styled-components"; +import { nanoid } from "@reduxjs/toolkit"; const StyledForm = styled.form` > .option { @@ -66,7 +66,7 @@ const StyledForm = styled.form` export default function Radio({ options, value = undefined, onChange = undefined }) { const [innerValue, setInnerValue] = useState(0); - const id = useRef(UUIDv4()); + const id = useRef(nanoid()); return ( diff --git a/src/routes/onboarding/steps/6-completed.js b/src/routes/onboarding/steps/6-completed.js index c31cb764..616b23b7 100644 --- a/src/routes/onboarding/steps/6-completed.js +++ b/src/routes/onboarding/steps/6-completed.js @@ -37,7 +37,7 @@ export default function CompletedStep({ data, setStep }) { setStep((prev) => prev + 1)}> play icon - Start + Enter
); From 7e9039463cc77ff54eb32b0fdf811c4502be6a5c Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Mon, 6 Jun 2022 20:07:23 +0800 Subject: [PATCH 08/10] fix: fix typos --- src/app/services/auth.js | 301 ++++++++++++++++++------------------ src/app/slices/auth.data.js | 149 +++++++++--------- 2 files changed, 217 insertions(+), 233 deletions(-) diff --git a/src/app/services/auth.js b/src/app/services/auth.js index a4b1ad01..49d3e23c 100644 --- a/src/app/services/auth.js +++ b/src/app/services/auth.js @@ -1,165 +1,160 @@ import { createApi } from "@reduxjs/toolkit/query/react"; import { nanoid } from "@reduxjs/toolkit"; import baseQuery from "./base.query"; -import { - setAuthData, - updateToken, - resetAuthData, - updateInitilize, -} from "../slices/auth.data"; +import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data"; import BASE_URL, { KEY_DEVICE_KEY } from "../config"; const getDeviceId = () => { - let d = localStorage.getItem(KEY_DEVICE_KEY); - if (!d) { - d = `web:${nanoid()}`; - localStorage.setItem(KEY_DEVICE_KEY, d); - } - return d; + let d = localStorage.getItem(KEY_DEVICE_KEY); + if (!d) { + d = `web:${nanoid()}`; + localStorage.setItem(KEY_DEVICE_KEY, d); + } + return d; }; export const authApi = createApi({ - reducerPath: "authApi", - baseQuery, - endpoints: (builder) => ({ - login: builder.mutation({ - query: (credentials) => ({ - url: "token/login", - method: "POST", - body: { - credential: credentials, - device: getDeviceId(), - device_token: "test", - }, - }), - transformResponse: (data) => { - const { avatar_updated_at } = data.user; - data.user.avatar = - avatar_updated_at == 0 - ? "" - : `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`; - return data; - }, - async onQueryStarted(params, { dispatch, queryFulfilled }) { - try { - const { data } = await queryFulfilled; - if (data) { - console.log("login resp", data); - dispatch(setAuthData(data)); - } - } catch { - console.log("login error"); - } - }, - }), - // 更新token - renew: builder.mutation({ - query: ({ token, refreshToken }) => ({ - url: "/token/renew", - method: "POST", - body: { - token, - refresh_token: refreshToken, - }, - }), - async onQueryStarted(params, { dispatch, queryFulfilled }) { - try { - const { data } = await queryFulfilled; - dispatch(updateToken(data)); - } catch { - dispatch(resetAuthData()); - console.log("renew token error"); - } - }, - }), - // 更新 device token - updateDeviceToken: builder.mutation({ - query: (device_token) => ({ - url: "/token/device_token", - method: "PUT", - body: { - device_token, - }, - }), - }), - // 获取openid - getOpenid: builder.mutation({ - query: ({ issuer, redirect_uri }) => ({ - url: "/token/openid/authorize", - method: "POST", - body: { - issuer, - redirect_uri, - }, - }), - }), - - checkInviteTokenValid: builder.mutation({ - query: (token) => ({ - url: "user/check_invite_magic_token", - method: "POST", - body: { magic_token: token }, - }), - }), - updatePassword: builder.mutation({ - query: ({ old_password, new_password }) => ({ - url: "user/change_password", - method: "POST", - body: { old_password, new_password }, - }), - }), - sendMagicLink: builder.mutation({ - query: (email) => ({ - url: "token/send_magic_link", - method: "POST", - body: { email }, - }), - }), - getMetamaskNonce: builder.query({ - query: (address) => ({ - url: `/token/metamask/nonce?public_address=${address}`, - }), - }), - getCredentials: builder.query({ - query: () => ({ - url: `/token/credentials`, - }), - }), - logout: builder.query({ - query: () => ({ url: `token/logout` }), - async onQueryStarted(params, { dispatch, queryFulfilled }) { - try { - await queryFulfilled; - dispatch(resetAuthData()); - } catch { - console.log("logout error"); - } - }, - }), - getInitialized: builder.query({ - query: () => ({ - url: `/admin/system/initialized`, - }), - async onQueryStarted(params, { dispatch, queryFulfilled }) { - try { - const { data: isInitized } = await queryFulfilled; - dispatch(updateInitilize(isInitized)); - } catch { - console.log("api initialized error"); - } - }, - }), + reducerPath: "authApi", + baseQuery, + endpoints: (builder) => ({ + login: builder.mutation({ + query: (credentials) => ({ + url: "token/login", + method: "POST", + body: { + credential: credentials, + device: getDeviceId(), + device_token: "test" + } + }), + transformResponse: (data) => { + const { avatar_updated_at } = data.user; + data.user.avatar = + avatar_updated_at == 0 + ? "" + : `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`; + return data; + }, + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const { data } = await queryFulfilled; + if (data) { + console.log("login resp", data); + dispatch(setAuthData(data)); + } + } catch { + console.log("login error"); + } + } }), + // 更新token + renew: builder.mutation({ + query: ({ token, refreshToken }) => ({ + url: "/token/renew", + method: "POST", + body: { + token, + refresh_token: refreshToken + } + }), + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const { data } = await queryFulfilled; + dispatch(updateToken(data)); + } catch { + dispatch(resetAuthData()); + console.log("renew token error"); + } + } + }), + // 更新 device token + updateDeviceToken: builder.mutation({ + query: (device_token) => ({ + url: "/token/device_token", + method: "PUT", + body: { + device_token + } + }) + }), + // 获取openid + getOpenid: builder.mutation({ + query: ({ issuer, redirect_uri }) => ({ + url: "/token/openid/authorize", + method: "POST", + body: { + issuer, + redirect_uri + } + }) + }), + + checkInviteTokenValid: builder.mutation({ + query: (token) => ({ + url: "user/check_invite_magic_token", + method: "POST", + body: { magic_token: token } + }) + }), + updatePassword: builder.mutation({ + query: ({ old_password, new_password }) => ({ + url: "user/change_password", + method: "POST", + body: { old_password, new_password } + }) + }), + sendMagicLink: builder.mutation({ + query: (email) => ({ + url: "token/send_magic_link", + method: "POST", + body: { email } + }) + }), + getMetamaskNonce: builder.query({ + query: (address) => ({ + url: `/token/metamask/nonce?public_address=${address}` + }) + }), + getCredentials: builder.query({ + query: () => ({ + url: `/token/credentials` + }) + }), + logout: builder.query({ + query: () => ({ url: `token/logout` }), + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + await queryFulfilled; + dispatch(resetAuthData()); + } catch { + console.log("logout error"); + } + } + }), + getInitialized: builder.query({ + query: () => ({ + url: `/admin/system/initialized` + }), + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const { data: isInitialized } = await queryFulfilled; + dispatch(updateInitialized(isInitialized)); + } catch { + console.log("api initialized error"); + } + } + }) + }) }); export const { - useGetInitializedQuery, - useSendMagicLinkMutation, - useGetCredentialsQuery, - useUpdateDeviceTokenMutation, - useGetOpenidMutation, - useRenewMutation, - useLazyGetMetamaskNonceQuery, - useLoginMutation, - useLazyLogoutQuery, - useCheckInviteTokenValidMutation, - useUpdatePasswordMutation, + useGetInitializedQuery, + useSendMagicLinkMutation, + useGetCredentialsQuery, + useUpdateDeviceTokenMutation, + useGetOpenidMutation, + useRenewMutation, + useLazyGetMetamaskNonceQuery, + useLoginMutation, + useLazyLogoutQuery, + useCheckInviteTokenValidMutation, + useUpdatePasswordMutation } = authApi; diff --git a/src/app/slices/auth.data.js b/src/app/slices/auth.data.js index 9de9da1f..cafd2a22 100644 --- a/src/app/slices/auth.data.js +++ b/src/app/slices/auth.data.js @@ -1,89 +1,78 @@ import { createSlice } from "@reduxjs/toolkit"; -import { - KEY_PWA_INSTALLED, - KEY_REFRESH_TOKEN, - KEY_TOKEN, - KEY_UID, - KEY_EXPIRE, -} from "../config"; +import { KEY_PWA_INSTALLED, KEY_REFRESH_TOKEN, KEY_TOKEN, KEY_UID, KEY_EXPIRE } from "../config"; const initialState = { - initialized: true, - uid: null, - token: localStorage.getItem(KEY_TOKEN), - expireTime: localStorage.getItem(KEY_EXPIRE) || new Date().getTime(), - refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN), + initialized: true, + uid: null, + token: localStorage.getItem(KEY_TOKEN), + expireTime: localStorage.getItem(KEY_EXPIRE) || new Date().getTime(), + refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN) }; const emptyState = { - initialized: true, - uid: null, - token: null, - expireTime: new Date().getTime(), - refreshToken: null, + initialized: true, + uid: null, + token: null, + expireTime: new Date().getTime(), + refreshToken: null }; const authDataSlice = createSlice({ - name: "authData", - initialState, - reducers: { - setAuthData(state, action) { - const { - initialized = true, - user: { uid }, - token, - refresh_token, - expired_in = 0, - } = action.payload; - state.initialized = initialized; - state.uid = uid; - state.token = token; - state.refreshToken = refresh_token; - // 当前时间往后推expire时长 - console.log("expire", expired_in); - const expireTime = new Date().getTime() + Number(expired_in) * 1000; - state.expireTime = expireTime; - // set local data - localStorage.setItem(KEY_EXPIRE, expireTime); - localStorage.setItem(KEY_TOKEN, token); - localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); - localStorage.setItem(KEY_UID, uid); - }, - resetAuthData() { - console.log("clear auth data"); - // remove local data - localStorage.removeItem(KEY_EXPIRE); - localStorage.removeItem(KEY_TOKEN); - localStorage.removeItem(KEY_REFRESH_TOKEN); - localStorage.removeItem(KEY_UID); - localStorage.removeItem(KEY_PWA_INSTALLED); - - return emptyState; - }, - setUid(state, action) { - const uid = action.payload; - state.uid = uid; - console.log("set uid orginal"); - }, - updateInitilize(state, action) { - const isInitized = action.payload; - state.initialized = isInitized; - }, - updateToken(state, action) { - const { token, refresh_token, expired_in } = action.payload; - console.log("refresh token"); - state.token = token; - const et = new Date().getTime() + Number(expired_in) * 1000; - state.expireTime = et; - state.refreshToken = refresh_token; - localStorage.setItem(KEY_EXPIRE, et); - localStorage.setItem(KEY_TOKEN, token); - localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); - }, + name: "authData", + initialState, + reducers: { + setAuthData(state, action) { + const { + initialized = true, + user: { uid }, + token, + refresh_token, + expired_in = 0 + } = action.payload; + state.initialized = initialized; + state.uid = uid; + state.token = token; + state.refreshToken = refresh_token; + // 当前时间往后推expire时长 + console.log("expire", expired_in); + const expireTime = new Date().getTime() + Number(expired_in) * 1000; + state.expireTime = expireTime; + // set local data + localStorage.setItem(KEY_EXPIRE, expireTime); + localStorage.setItem(KEY_TOKEN, token); + localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); + localStorage.setItem(KEY_UID, uid); }, + resetAuthData() { + console.log("clear auth data"); + // remove local data + localStorage.removeItem(KEY_EXPIRE); + localStorage.removeItem(KEY_TOKEN); + localStorage.removeItem(KEY_REFRESH_TOKEN); + localStorage.removeItem(KEY_UID); + localStorage.removeItem(KEY_PWA_INSTALLED); + + return emptyState; + }, + setUid(state, action) { + const uid = action.payload; + state.uid = uid; + console.log("set uid orginal"); + }, + updateInitialized(state, action) { + const isInitialized = action.payload; + state.initialized = isInitialized; + }, + updateToken(state, action) { + const { token, refresh_token, expired_in } = action.payload; + console.log("refresh token"); + state.token = token; + const et = new Date().getTime() + Number(expired_in) * 1000; + state.expireTime = et; + state.refreshToken = refresh_token; + localStorage.setItem(KEY_EXPIRE, et); + localStorage.setItem(KEY_TOKEN, token); + localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); + } + } }); -export const { - updateInitilize, - setAuthData, - resetAuthData, - setUid, - updateToken, -} = authDataSlice.actions; +export const { updateInitialized, setAuthData, resetAuthData, setUid, updateToken } = + authDataSlice.actions; export default authDataSlice.reducer; From 888668225b95d4c8e306714f9d6ec65f015fca5e Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Mon, 6 Jun 2022 20:09:07 +0800 Subject: [PATCH 09/10] fix: remove RequireInitialized --- src/common/component/RequireInitialized.js | 8 -------- src/routes/index.js | 17 ++++++----------- 2 files changed, 6 insertions(+), 19 deletions(-) delete mode 100644 src/common/component/RequireInitialized.js diff --git a/src/common/component/RequireInitialized.js b/src/common/component/RequireInitialized.js deleted file mode 100644 index 275a3796..00000000 --- a/src/common/component/RequireInitialized.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { useGetInitializedQuery } from "../../app/services/server"; - -export default function RequireInitialized({ children, redirectTo = "/onboarding" }) { - const { data } = useGetInitializedQuery(); - console.log("initialized?", data); - return data === false ? : children; -} diff --git a/src/routes/index.js b/src/routes/index.js index b8a4d70d..58d40f9e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -24,7 +24,6 @@ import SettingChannelPage from "./settingChannel"; import OnboardingPage from "./onboarding"; import toast from "react-hot-toast"; import ResourceManagement from "./resources"; -import RequireInitialized from "../common/component/RequireInitialized"; const PageRoutes = () => { const { @@ -50,11 +49,9 @@ const PageRoutes = () => { - - - - + + + } /> { - - - - + + + } > From fcf6bb05d78e6c22751765187974c57b64f0caf3 Mon Sep 17 00:00:00 2001 From: Liyang Zhu <1184997399@qq.com> Date: Mon, 6 Jun 2022 21:39:01 +0800 Subject: [PATCH 10/10] fix: remove setAuthData after login --- .../onboarding/steps/3-adminCredentials.js | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/routes/onboarding/steps/3-adminCredentials.js b/src/routes/onboarding/steps/3-adminCredentials.js index 88b8fca4..f0b507fd 100644 --- a/src/routes/onboarding/steps/3-adminCredentials.js +++ b/src/routes/onboarding/steps/3-adminCredentials.js @@ -6,12 +6,11 @@ import StyledInput from "../../../common/component/styled/Input"; import StyledButton from "../../../common/component/styled/Button"; import { useCreateAdminMutation, - useGetInitializedQuery, useGetServerQuery, useUpdateServerMutation } from "../../../app/services/server"; import { useLoginMutation } from "../../../app/services/auth"; -import { setAuthData } from "../../../app/slices/auth.data"; +import { updateInitialized } from "../../../app/slices/auth.data"; const StyledAdminCredentialsStep = styled.div` height: 100%; @@ -33,11 +32,8 @@ export default function AdminCredentialsStep({ data, setStep }) { const dispatch = useDispatch(); const [createAdmin, { isLoading: isSigningUp, error: signUpError }] = useCreateAdminMutation(); - const [ - login, - { isLoading: isLoggingIn, isSuccess: isLoggedIn, error: loginError, data: loginData } - ] = useLoginMutation(); - const { refetch: refetchInitialized } = useGetInitializedQuery(); + const [login, { isLoading: isLoggingIn, isSuccess: isLoggedIn, error: loginError }] = + useLoginMutation(); const { data: serverData } = useGetServerQuery(); const [updateServer, { isLoading: isUpdatingServer, isSuccess: isUpdatedServer }] = useUpdateServerMutation(); @@ -58,18 +54,17 @@ export default function AdminCredentialsStep({ data, setStep }) { // After logged in useEffect(() => { - if (isLoggedIn && loginData) { - // Update local auth data - dispatch(setAuthData(loginData)); - // Update initialized state - refetchInitialized(); - // Set server name - updateServer({ - ...serverData, - name: data.serverName - }); + if (isLoggedIn) { + dispatch(updateInitialized(true)); + setTimeout(() => { + // Set server name + updateServer({ + ...serverData, + name: data.serverName + }); + }, 0); } - }, [isLoggedIn, loginData]); + }, [isLoggedIn]); // After updated server useEffect(() => {