| @@ -10,7 +10,7 @@ import { | |||||
| Stack, | Stack, | ||||
| Typography, | Typography, | ||||
| Button, StepLabel, | Button, StepLabel, | ||||
| CircularProgress, | |||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import VisibilityIcon from '@mui/icons-material/Visibility'; | import VisibilityIcon from '@mui/icons-material/Visibility'; | ||||
| @@ -32,9 +32,11 @@ const BusRegister = () => { | |||||
| const [completed, setCompleted] = useState([false]); | const [completed, setCompleted] = useState([false]); | ||||
| const [updateValid, setUpdateValid] = useState(false); | const [updateValid, setUpdateValid] = useState(false); | ||||
| const step0GuardRef = useRef(null); | const step0GuardRef = useRef(null); | ||||
| const nextBusyRef = useRef(false); | |||||
| const [username, setUsername] = useState("") | const [username, setUsername] = useState("") | ||||
| const [base64Url, setBase64Url] = useState("") | const [base64Url, setBase64Url] = useState("") | ||||
| const [checkCode, setCheckCode] = useState("") | const [checkCode, setCheckCode] = useState("") | ||||
| const [isNextBusy, setIsNextBusy] = useState(false); | |||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| const steps = [ | const steps = [ | ||||
| intl.formatMessage({id: 'personalInformation'}), | intl.formatMessage({id: 'personalInformation'}), | ||||
| @@ -98,30 +100,38 @@ const BusRegister = () => { | |||||
| } | } | ||||
| const handleNext = async () => { | const handleNext = async () => { | ||||
| if (activeStep === 0 && step0GuardRef.current) { | |||||
| const step0Ok = await Promise.resolve(step0GuardRef.current()); | |||||
| if (!step0Ok) { | |||||
| if (nextBusyRef.current) return; | |||||
| nextBusyRef.current = true; | |||||
| setIsNextBusy(true); | |||||
| try { | |||||
| if (activeStep === 0 && step0GuardRef.current) { | |||||
| const step0Ok = await Promise.resolve(step0GuardRef.current()); | |||||
| if (!step0Ok) { | |||||
| return; | |||||
| } | |||||
| } | |||||
| const captchaTest = await handleCaptcha(); | |||||
| if (!captchaTest) { | |||||
| notifyActionError(intl.formatMessage({id: 'validVerify'})) | |||||
| return; | return; | ||||
| } | } | ||||
| } | |||||
| const captchaTest = await handleCaptcha(); | |||||
| if (!captchaTest) { | |||||
| notifyActionError(intl.formatMessage({id: 'validVerify'})) | |||||
| return; | |||||
| } | |||||
| const test = await handleCheckUsername() | |||||
| if (test) { | |||||
| notifyActionError(intl.formatMessage({id: 'usernameTaken'})) | |||||
| } else { | |||||
| const newActiveStep = | |||||
| isLastStep() && !allStepsCompleted() | |||||
| ? // It's the last step, but not all steps have been completed, | |||||
| // find the first step that has been completed | |||||
| steps.findIndex((step, i) => !(i in completed)) | |||||
| : activeStep + 1; | |||||
| setActiveStep(newActiveStep); | |||||
| scrollToTop(); | |||||
| const test = await handleCheckUsername() | |||||
| if (test) { | |||||
| notifyActionError(intl.formatMessage({id: 'usernameTaken'})) | |||||
| } else { | |||||
| const newActiveStep = | |||||
| isLastStep() && !allStepsCompleted() | |||||
| ? // It's the last step, but not all steps have been completed, | |||||
| // find the first step that has been completed | |||||
| steps.findIndex((step, i) => !(i in completed)) | |||||
| : activeStep + 1; | |||||
| setActiveStep(newActiveStep); | |||||
| scrollToTop(); | |||||
| } | |||||
| } finally { | |||||
| nextBusyRef.current = false; | |||||
| setIsNextBusy(false); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -224,7 +234,7 @@ const BusRegister = () => { | |||||
| ) : ( | ) : ( | ||||
| <Button | <Button | ||||
| color="inherit" | color="inherit" | ||||
| disabled={activeStep === 0} | |||||
| disabled={activeStep === 0 || isNextBusy} | |||||
| onClick={handleBack} | onClick={handleBack} | ||||
| sx={{ mr: 1 }} | sx={{ mr: 1 }} | ||||
| variant="h5" | variant="h5" | ||||
| @@ -238,10 +248,20 @@ const BusRegister = () => { | |||||
| <Stack sx={{ flex: '1 1 auto' }} /> | <Stack sx={{ flex: '1 1 auto' }} /> | ||||
| {activeStep === totalSteps() - 2 ? | {activeStep === totalSteps() - 2 ? | ||||
| ( | ( | ||||
| <Button variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Typography variant="h5"> | |||||
| <FormattedMessage id="submit"/> | |||||
| </Typography> | |||||
| <Button | |||||
| variant="outlined" | |||||
| disabled={isNextBusy} | |||||
| onClick={handleNext} | |||||
| aria-busy={isNextBusy} | |||||
| sx={{ mr: 1, minWidth: 120 }} | |||||
| > | |||||
| {isNextBusy ? ( | |||||
| <CircularProgress size={22} color="inherit" aria-hidden /> | |||||
| ) : ( | |||||
| <Typography variant="h5"> | |||||
| <FormattedMessage id="submit"/> | |||||
| </Typography> | |||||
| )} | |||||
| </Button> | </Button> | ||||
| ) : (activeStep === totalSteps() - 1 ? | ) : (activeStep === totalSteps() - 1 ? | ||||
| ( | ( | ||||
| @@ -254,7 +274,7 @@ const BusRegister = () => { | |||||
| ) : | ) : | ||||
| ( | ( | ||||
| // <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | // <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | ||||
| <Button disabled={!updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Button disabled={!updateValid || isNextBusy} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Typography variant="h5"> | <Typography variant="h5"> | ||||
| <FormattedMessage id="continue"/> | <FormattedMessage id="continue"/> | ||||
| </Typography> | </Typography> | ||||
| @@ -13,6 +13,7 @@ import { | |||||
| Stack, | Stack, | ||||
| Typography, | Typography, | ||||
| Button, | Button, | ||||
| CircularProgress, | |||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import VisibilityIcon from '@mui/icons-material/Visibility'; | import VisibilityIcon from '@mui/icons-material/Visibility'; | ||||
| import { GET_ID, POST_VERIFY_CAPTCHA } from "utils/ApiPathConst"; | import { GET_ID, POST_VERIFY_CAPTCHA } from "utils/ApiPathConst"; | ||||
| @@ -55,9 +56,11 @@ const Register = () => { | |||||
| const [completed, setCompleted] = useState([false]); | const [completed, setCompleted] = useState([false]); | ||||
| const [updateValid, setUpdateValid] = useState(false); | const [updateValid, setUpdateValid] = useState(false); | ||||
| const step0GuardRef = useRef(null); | const step0GuardRef = useRef(null); | ||||
| const nextBusyRef = useRef(false); | |||||
| const [base64Url, setBase64Url] = useState("") | const [base64Url, setBase64Url] = useState("") | ||||
| const [checkCode, setCheckCode] = useState("") | const [checkCode, setCheckCode] = useState("") | ||||
| const [idNo, setIdNo] = useState(""); | const [idNo, setIdNo] = useState(""); | ||||
| const [isNextBusy, setIsNextBusy] = useState(false); | |||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| const steps = [ | const steps = [ | ||||
| intl.formatMessage({id: 'personalInformation'}), | intl.formatMessage({id: 'personalInformation'}), | ||||
| @@ -103,29 +106,37 @@ const Register = () => { | |||||
| notifyActionError(intl.formatMessage({id: 'iAmSmartNoIdNoMsg'})) | notifyActionError(intl.formatMessage({id: 'iAmSmartNoIdNoMsg'})) | ||||
| return; | return; | ||||
| } | } | ||||
| if (activeStep === 0 && step0GuardRef.current) { | |||||
| const step0Ok = await Promise.resolve(step0GuardRef.current()); | |||||
| if (!step0Ok) { | |||||
| if (nextBusyRef.current) return; | |||||
| nextBusyRef.current = true; | |||||
| setIsNextBusy(true); | |||||
| try { | |||||
| if (activeStep === 0 && step0GuardRef.current) { | |||||
| const step0Ok = await Promise.resolve(step0GuardRef.current()); | |||||
| if (!step0Ok) { | |||||
| return; | |||||
| } | |||||
| } | |||||
| const captchaTest = await handleCaptcha(); | |||||
| if (!captchaTest) { | |||||
| notifyActionError(intl.formatMessage({id: 'validVerify'})) | |||||
| return; | return; | ||||
| } | } | ||||
| } | |||||
| const captchaTest = await handleCaptcha(); | |||||
| if (!captchaTest) { | |||||
| notifyActionError(intl.formatMessage({id: 'validVerify'})) | |||||
| return; | |||||
| } | |||||
| const test = await handleCheckID() | |||||
| if (test) { | |||||
| notifyActionError(intl.formatMessage({id: 'userRegistered'})) | |||||
| } else { | |||||
| const newActiveStep = | |||||
| isLastStep() && !allStepsCompleted() | |||||
| ? // It's the last step, but not all steps have been completed, | |||||
| // find the first step that has been completed | |||||
| steps.findIndex((step, i) => !(i in completed)) | |||||
| : activeStep + 1; | |||||
| setActiveStep(newActiveStep); | |||||
| scrollToTop(); | |||||
| const test = await handleCheckID() | |||||
| if (test) { | |||||
| notifyActionError(intl.formatMessage({id: 'userRegistered'})) | |||||
| } else { | |||||
| const newActiveStep = | |||||
| isLastStep() && !allStepsCompleted() | |||||
| ? // It's the last step, but not all steps have been completed, | |||||
| // find the first step that has been completed | |||||
| steps.findIndex((step, i) => !(i in completed)) | |||||
| : activeStep + 1; | |||||
| setActiveStep(newActiveStep); | |||||
| scrollToTop(); | |||||
| } | |||||
| } finally { | |||||
| nextBusyRef.current = false; | |||||
| setIsNextBusy(false); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -214,7 +225,7 @@ const Register = () => { | |||||
| ) : ( | ) : ( | ||||
| <Button | <Button | ||||
| color="inherit" | color="inherit" | ||||
| disabled={activeStep === 0} | |||||
| disabled={activeStep === 0 || isNextBusy} | |||||
| onClick={handleBack} | onClick={handleBack} | ||||
| sx={{ mr: 1 }} | sx={{ mr: 1 }} | ||||
| > | > | ||||
| @@ -227,10 +238,20 @@ const Register = () => { | |||||
| <Stack sx={{ flex: '1 1 auto' }} /> | <Stack sx={{ flex: '1 1 auto' }} /> | ||||
| {activeStep === totalSteps() - 2 ? | {activeStep === totalSteps() - 2 ? | ||||
| ( | ( | ||||
| <Button variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Typography variant="h5"> | |||||
| <FormattedMessage id="submit"/> | |||||
| </Typography> | |||||
| <Button | |||||
| variant="outlined" | |||||
| disabled={isNextBusy} | |||||
| onClick={handleNext} | |||||
| aria-busy={isNextBusy} | |||||
| sx={{ mr: 1, minWidth: 120 }} | |||||
| > | |||||
| {isNextBusy ? ( | |||||
| <CircularProgress size={22} color="inherit" aria-hidden /> | |||||
| ) : ( | |||||
| <Typography variant="h5"> | |||||
| <FormattedMessage id="submit"/> | |||||
| </Typography> | |||||
| )} | |||||
| </Button> | </Button> | ||||
| ) : (activeStep === totalSteps() - 1 ? | ) : (activeStep === totalSteps() - 1 ? | ||||
| ( | ( | ||||
| @@ -243,7 +264,7 @@ const Register = () => { | |||||
| ) : | ) : | ||||
| ( | ( | ||||
| // <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | // <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | ||||
| <Button disabled={!updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Button disabled={!updateValid || isNextBusy} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Typography variant="h5"> | <Typography variant="h5"> | ||||
| <FormattedMessage id="continue"/> | <FormattedMessage id="continue"/> | ||||
| </Typography> | </Typography> | ||||
| @@ -13,6 +13,7 @@ import { | |||||
| Stack, | Stack, | ||||
| Typography, | Typography, | ||||
| Button, StepLabel, | Button, StepLabel, | ||||
| CircularProgress, | |||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import VisibilityIcon from '@mui/icons-material/Visibility'; | import VisibilityIcon from '@mui/icons-material/Visibility'; | ||||
| import { POST_USERNAME, POST_VERIFY_CAPTCHA } from "utils/ApiPathConst"; | import { POST_USERNAME, POST_VERIFY_CAPTCHA } from "utils/ApiPathConst"; | ||||
| @@ -36,6 +37,8 @@ const Register = () => { | |||||
| const [username, setUsername] = useState(""); | const [username, setUsername] = useState(""); | ||||
| const [base64Url, setBase64Url] = useState("") | const [base64Url, setBase64Url] = useState("") | ||||
| const [checkCode, setCheckCode] = useState("") | const [checkCode, setCheckCode] = useState("") | ||||
| const [isNextBusy, setIsNextBusy] = useState(false); | |||||
| const nextBusyRef = useRef(false); | |||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| // Localized document title/meta for register flow | // Localized document title/meta for register flow | ||||
| usePageTitle("register"); | usePageTitle("register"); | ||||
| @@ -102,31 +105,39 @@ const Register = () => { | |||||
| const handleNext = async () => { | const handleNext = async () => { | ||||
| if (activeStep === 0 && step0GuardRef.current) { | |||||
| const step0Ok = await Promise.resolve(step0GuardRef.current()); | |||||
| if (!step0Ok) { | |||||
| return; | |||||
| if (nextBusyRef.current) return; | |||||
| nextBusyRef.current = true; | |||||
| setIsNextBusy(true); | |||||
| try { | |||||
| if (activeStep === 0 && step0GuardRef.current) { | |||||
| const step0Ok = await Promise.resolve(step0GuardRef.current()); | |||||
| if (!step0Ok) { | |||||
| return; | |||||
| } | |||||
| } | } | ||||
| } | |||||
| const captchaTest = await handleCaptcha(); | |||||
| if (!captchaTest) { | |||||
| notifyActionError(intl.formatMessage({id: 'validVerify'})) | |||||
| return; | |||||
| } | |||||
| const captchaTest = await handleCaptcha(); | |||||
| if (!captchaTest) { | |||||
| notifyActionError(intl.formatMessage({id: 'validVerify'})) | |||||
| return; | |||||
| } | |||||
| const test = await handleCheckUsername() | |||||
| if (test) { | |||||
| notifyActionError(intl.formatMessage({id: 'usernameTaken'})) | |||||
| } else { | |||||
| const newActiveStep = | |||||
| isLastStep() && !allStepsCompleted() | |||||
| ? // It's the last step, but not all steps have been completed, | |||||
| // find the first step that has been completed | |||||
| steps.findIndex((step, i) => !(i in completed)) | |||||
| : activeStep + 1; | |||||
| setActiveStep(newActiveStep); | |||||
| scrollToTop(); | |||||
| const test = await handleCheckUsername() | |||||
| if (test) { | |||||
| notifyActionError(intl.formatMessage({id: 'usernameTaken'})) | |||||
| } else { | |||||
| const newActiveStep = | |||||
| isLastStep() && !allStepsCompleted() | |||||
| ? // It's the last step, but not all steps have been completed, | |||||
| // find the first step that has been completed | |||||
| steps.findIndex((step, i) => !(i in completed)) | |||||
| : activeStep + 1; | |||||
| setActiveStep(newActiveStep); | |||||
| scrollToTop(); | |||||
| } | |||||
| } finally { | |||||
| nextBusyRef.current = false; | |||||
| setIsNextBusy(false); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -229,7 +240,7 @@ const Register = () => { | |||||
| ) : ( | ) : ( | ||||
| <Button | <Button | ||||
| color="inherit" | color="inherit" | ||||
| disabled={activeStep === 0} | |||||
| disabled={activeStep === 0 || isNextBusy} | |||||
| onClick={handleBack} | onClick={handleBack} | ||||
| sx={{ mr: 1 }} | sx={{ mr: 1 }} | ||||
| variant="h5" | variant="h5" | ||||
| @@ -243,10 +254,20 @@ const Register = () => { | |||||
| <Stack sx={{ flex: '1 1 auto' }} /> | <Stack sx={{ flex: '1 1 auto' }} /> | ||||
| {activeStep === totalSteps() - 2 ? | {activeStep === totalSteps() - 2 ? | ||||
| ( | ( | ||||
| <Button variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Typography variant="h5"> | |||||
| <FormattedMessage id="submit"/> | |||||
| </Typography> | |||||
| <Button | |||||
| variant="outlined" | |||||
| disabled={isNextBusy} | |||||
| onClick={handleNext} | |||||
| aria-busy={isNextBusy} | |||||
| sx={{ mr: 1, minWidth: 120 }} | |||||
| > | |||||
| {isNextBusy ? ( | |||||
| <CircularProgress size={22} color="inherit" aria-hidden /> | |||||
| ) : ( | |||||
| <Typography variant="h5"> | |||||
| <FormattedMessage id="submit"/> | |||||
| </Typography> | |||||
| )} | |||||
| </Button> | </Button> | ||||
| ) : (activeStep === totalSteps() - 1 ? | ) : (activeStep === totalSteps() - 1 ? | ||||
| ( | ( | ||||
| @@ -259,7 +280,7 @@ const Register = () => { | |||||
| ) : | ) : | ||||
| ( | ( | ||||
| // <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | // <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | ||||
| <Button disabled={!updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Button disabled={!updateValid || isNextBusy} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}> | |||||
| <Typography variant="h5"> | <Typography variant="h5"> | ||||
| <FormattedMessage id="continue"/> | <FormattedMessage id="continue"/> | ||||
| </Typography> | </Typography> | ||||