index.jsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. // hooks
  2. import { useForm, Controller } from 'react-hook-form';
  3. import React, { useCallback, useState } from 'react';
  4. import styles from './styles.module.scss';
  5. // utils
  6. import classNames from 'classnames';
  7. import PhoneInput from "@components/UInput/PhoneInput";
  8. import { useDispatch, useSelector } from "react-redux";
  9. import {
  10. getSendCodeStatus,
  11. getSendCodeTS,
  12. } from "@features/mainSlice";
  13. import { useEffect } from 'react';
  14. import { REQ_STATUS } from "@api/config";
  15. import { reqSendCode } from "@features/api";
  16. import { getCountryPhoneError, validateCountryPhone } from "@utils/helpers";
  17. import { CircularProgress } from "@mui/material";
  18. import { toast } from 'react-toastify';
  19. import MainButton from '@components/Buttons/MainButton';
  20. import { ReactComponent as AlertIcon } from '@assets/singlesvg/icon_alert.svg';
  21. import { getPhoneCode } from '@constants/region';
  22. const i18n = global.i18n;
  23. const objInfo = {
  24. cdTimer: null,
  25. inSubmit: false
  26. }
  27. const CODE_MIN = 4;
  28. const CODE_MAX = 4;
  29. const DEFAULT_COUNTRY = String(getPhoneCode()).replace(/^\+/, "") || "44";
  30. const PhoneSMS = ({ children, callback, buttonText, handleStatus, onCountryChange, options = {}, defValues = {}, className = '' }) => {
  31. const [country, setCountry] = useState(DEFAULT_COUNTRY);
  32. const [phoneValid, setPhoneValid] = useState(Boolean(defValues.phone));
  33. const [phoneMessage] = useState("");
  34. const [sendCount, setSendCount] = useState(0);
  35. const dispatch = useDispatch();
  36. const { handleSubmit, control, watch } = useForm({
  37. defaultValues: {
  38. // country 由当前区域配置决定。
  39. account: '',
  40. password: '',
  41. email: "",
  42. repassword: "",
  43. lang: "",
  44. code: "",
  45. phone: defValues.phone || "",
  46. newpassword: "",
  47. renewpassword: ""
  48. }
  49. });
  50. const otpTimeStamp = useSelector(getSendCodeTS);
  51. const [otpCD, setOtpCD] = useState(-1);
  52. const otpStatus = useSelector(getSendCodeStatus);
  53. const onSendCode = (e) => {
  54. if (otpCD > 0) return;
  55. if (otpStatus === REQ_STATUS.LOADING) return;
  56. let phone = control._formValues.phone;
  57. let validate = validatePhone(phone);
  58. if (!validate) {
  59. let msg = getCountryPhoneError(country);
  60. toast.warn(i18n.t(msg), { toastId: "phone_sms" });
  61. // toast.warn(i18n.t("phoneSms.tips_phone"), { toastId: "phone_sms" });
  62. control.setError("phone", true);
  63. return;
  64. }
  65. let params = { phone: phone, country: country };
  66. dispatch(reqSendCode(params));
  67. }
  68. const getLeftCD = useCallback(() => {
  69. let now = Date.now();
  70. let delta = now - otpTimeStamp;
  71. delta = Math.floor(delta / 1000);
  72. delta = 60 - delta;
  73. if (delta < 0) return -1;
  74. else return delta;
  75. }, [otpTimeStamp]);
  76. const checkPhone = (value) => {
  77. const ok = validatePhone(value);
  78. if (!ok) {
  79. setPhoneValid(false);
  80. } else {
  81. setPhoneValid(true);
  82. }
  83. return ok;
  84. }
  85. const clearTimer = () => {
  86. if (objInfo.cdTimer != null) {
  87. clearInterval(objInfo.cdTimer);
  88. objInfo.cdTimer = null;
  89. }
  90. }
  91. //effect_______
  92. useEffect(() => {
  93. return () => {
  94. global.authLockPath = null;
  95. clearTimer();
  96. objInfo.inSubmit = false;
  97. }
  98. }, [])
  99. useEffect(() => {
  100. let left = getLeftCD();
  101. setOtpCD(left);
  102. clearTimer();
  103. if (left > 0) {
  104. objInfo.cdTimer = setInterval(() => {
  105. left = getLeftCD();
  106. setOtpCD(left);
  107. if (left < 0) {
  108. setSendCount(prev => prev + 1);
  109. clearTimer();
  110. }
  111. }, 1000)
  112. }
  113. }, [getLeftCD])
  114. const validatePhone = (value, countryCode = country) => {
  115. if (String(defValues.phone) === String(value) && value.length > 0) return true;
  116. return validateCountryPhone(countryCode, value || "");
  117. }
  118. const codeValue = watch("code");
  119. const phoneValue = watch("phone");
  120. const checkCode = () => {
  121. return (!codeValue || codeValue.length < CODE_MIN || codeValue.length > CODE_MAX)
  122. }
  123. const checkFormOnClaim = (tips = true) => {
  124. if (!checkPhone(phoneValue)) {
  125. control.setError("phone", true);
  126. if (tips) toast.warn(i18n.t("phoneSms.tips_phone"), { toastId: "phone_sms" });
  127. return false;
  128. }
  129. if (!codeValue || codeValue.length < CODE_MIN || codeValue.length > CODE_MAX) {
  130. control.setError("code", true);
  131. if (tips) toast.warn(i18n.t("phoneSms.tips_info"), { toastId: "phone_sms" });
  132. return false;
  133. }
  134. return true;
  135. }
  136. const onSubmitOTPLogin = (data) => {
  137. if (!checkFormOnClaim()) return;
  138. //data.phone data.code
  139. data.country = country;
  140. callback && callback(data);
  141. }
  142. return (
  143. <form key={"otp"} autoComplete="off" className={`${styles.l_form} ${className && className}`} onSubmit={handleSubmit((data) => {
  144. onSubmitOTPLogin(data);
  145. })}>
  146. <Controller control={control}
  147. name="phone"
  148. rules={{ required: true }}
  149. render={({ field: { ref, onChange, value }, fieldState: { error } }) => {
  150. let message = phoneMessage;
  151. return (<PhoneInput
  152. key={"phone"}
  153. className={classNames(styles.input_box, { 'input-control--error': error })}
  154. value={value}
  155. message={message}
  156. onChange={
  157. e => {
  158. onChange(e.target.value);
  159. setPhoneValid(validatePhone(e.target.value));
  160. }
  161. }
  162. onChangeCountry={(country) => {
  163. setCountry(country);
  164. setPhoneValid(validatePhone(control._formValues.phone, country));
  165. if (onCountryChange != null) onCountryChange(country);
  166. }}
  167. placeholder={i18n.t('login.phone_ph')}
  168. innerRef={ref}
  169. {...options}
  170. />)
  171. }}
  172. />
  173. <Controller control={control}
  174. name="code"
  175. rules={{ required: true, minLength: CODE_MIN, maxLength: CODE_MAX }}
  176. render={({ field: { ref, onChange, value }, fieldState: { error } }) => {
  177. return (
  178. <div className={`d-flex flex-column g-6`}>
  179. <div
  180. className={`${styles.sms_box} `}>
  181. <input
  182. type="text"
  183. inputMode="numeric"
  184. autoComplete="off"
  185. className={styles.input}
  186. ref={ref}
  187. maxLength={CODE_MAX}
  188. onChange={e => {
  189. e.target.value = e.target.value.replace(/\D+/g, '');
  190. onChange(e.target.value)
  191. }}
  192. placeholder={i18n.t('login.otp_ph')}
  193. />
  194. <button type='button'
  195. disabled={!phoneValid || otpCD > 0 || otpStatus === REQ_STATUS.LOADING}
  196. className={styles.otp_btn}
  197. onClick={e => {
  198. onSendCode(e);
  199. }}>
  200. {
  201. otpStatus === REQ_STATUS.LOADING ? (
  202. <CircularProgress color='inherit' size={22} thickness={5} />) : (
  203. <>
  204. {
  205. otpCD > 0
  206. ? i18n.t('login.btn_otp_cd', { times: otpCD })
  207. : (
  208. sendCount > 0 ? i18n.t("login.btn_otp_re") : i18n.t("login.btn_otp")
  209. )
  210. }
  211. </>
  212. )
  213. }
  214. </button>
  215. </div>
  216. </div>
  217. )
  218. }}
  219. />
  220. <div className={styles.tips}>
  221. <AlertIcon />
  222. <span>{i18n.t('login.otp_tips')}</span>
  223. </div>
  224. <MainButton
  225. disabled={!phoneValid || checkCode()}
  226. className={styles.btn_claim}
  227. loading={handleStatus === REQ_STATUS.LOADING}
  228. onClick={checkFormOnClaim}>
  229. <span>{buttonText}</span>
  230. {children}
  231. </MainButton>
  232. </form>
  233. )
  234. }
  235. export default PhoneSMS;