普通はしないのですが、パスワードが流出したみたいな事(笑)があったら必要な機能なので作ります。 あとメールアドレスの変更も実装するのですが、これっているの?ガラゲー時代と違ってあまり変更がないと思うだけど・・・
React Firebase入門シリーズ
1-1・ React Firebase入門 初期設定とsignup - masalibの日記
1-2・ 「react-hook-form」を入れてみた - masalibの日記
1-3・ React Firebase入門 signupの通信エラー対応 - masalibの日記
2・ React Firebase入門 ログイン処理 - masalibの日記
3・ React Firebase入門 ログイン認証とログアウト処理 - masalibの日記
4・ React Firebase入門 パスワード初期化処理 - masalibの日記
5・ React Firebase入門 メールアドレスの有効化 - masalibの日記
6・React Firebase入門 メールアドレスとパスワード変更 今ここ
処理概要
メールアドレス変更とは
現在使用しているメールアドレスから違うメールアドレスに変更する処理になります。変更をするとメールアドレス有効化のフラグ(emailVerified)がfalseになります。また変更前のアドレスに変更された旨のメールが送信されます。そのメールに記載されているキャンセルリンクを押すとメールアドレスの変更がキャンセルされます。
パスワード変更とは
userがログインしている状態でパスワードを変更する処理になります。ログインチェックは必須になります。
同時アクセスの制御について
普通はありえないのですが2台同時にログインしてメールアドレス変更をすると
先にログインした端末側では処理が失敗します。
(エラーコードはauth/requires-recent-loginです。)
パスワード変更処理
contextに処理をつくってその関数を共有させるだけです。
ソース
/src/contexts/AuthContext.js
+ function updatePassword(password) { + return currentUser.updatePassword(password) + } + function updateEmail(email) { + return currentUser.updateEmail(email) + } const value = { currentUser, signup, login, logout, resetPassword, sendEmailVerification, + updatePassword, + updateEmail }
プロフィール変更画面
メールアドレスとパスワード変更以外にもプロフィールの変更をおこなう予定です。
ソース
クリックすると展開されます(長文なので注意)
import React, { useState,useReducer, useEffect } from "react"; import { useForm } from "react-hook-form"; import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; import { Typography, Paper, Button, TextField } from '@material-ui/core'; import { useAuth } from "../contexts/AuthContext" import { Link , useHistory} from "react-router-dom" const useStyles = makeStyles((theme: Theme) => createStyles({ container: { padding: 16, margin: 'auto', maxWidth: 480 }, signupBtn: { marginTop: theme.spacing(2), flexGrow: 1, color:'primary' }, }) ); //state type type State = { username: string, password: string, passwordconfirm: string, isButtonDisabled: boolean, helperText: string, isError: boolean }; const initialState: State = { username: "", password: "", passwordconfirm: "", isButtonDisabled: true, helperText: "", isError: false }; type Action = | { type: "setUsername", payload: string } | { type: "setPassword", payload: string } | { type: "setPasswordConfirm", payload: string } | { type: "setIsButtonDisabled", payload: boolean } | { type: "signupSuccess", payload: string } | { type: "signupFailed", payload: string } | { type: "setIsError", payload: boolean }; const reducer = (state: State, action: Action): State => { switch (action.type) { case "setUsername": return { ...state, username: action.payload }; case "setPassword": return { ...state, password: action.payload }; case "setPasswordConfirm": return { ...state, passwordconfirm: action.payload }; case "setIsButtonDisabled": return { ...state, isButtonDisabled: action.payload }; case "signupSuccess": return { ...state, helperText: action.payload, isError: false }; case "signupFailed": return { ...state, helperText: action.payload, isError: true }; case "setIsError": return { ...state, isError: action.payload }; default: return state; } }; const UpdateProfile = () => { const { currentUser, updatePassword, updateEmail,sendEmailVerification} = useAuth() const classes = useStyles(); //const email = currentUser.email const [state, dispatch] = useReducer(reducer, {...initialState, username:currentUser.email}); const [error, setError] = useState("") const [successMessage, setSuccessMessage] = useState("") const { register, handleSubmit, errors ,formState} = useForm(); const history = useHistory() useEffect(() => { if (state.password.trim() !== state.passwordconfirm.trim()){ //clearErrors() dispatch({ type: "setIsButtonDisabled", payload: true }); } else if (state.username.trim()){ dispatch({ type: "setIsButtonDisabled", payload: false }); } else { //clearErrors() dispatch({ type: "setIsButtonDisabled", payload: true }); } }, [state.username, state.password, state.passwordconfirm]); async function handleUpdateProfile (data) { //react-hook-formを導入したためevent -> dataに変更 //event.preventDefault() //react-hook-formを導入したため削除 setError("") setSuccessMessage("") //sing up ボタンの無効化 dispatch({ type: "setIsButtonDisabled", payload: true }); //処理の初期化 const promises = [] //更新処理をセット if (state.username !== currentUser.email) { console.log("updateEmail") promises.push(updateEmail(state.username)) } if (state.password) { console.log("updatePassword") promises.push(updatePassword(state.password)) } Promise.all(promises) .then(() => { setSuccessMessage("プロフィールを更新しました。ダッシュボードにリダレクトします") //ボタンの有効化 dispatch({ type: "setIsButtonDisabled", payload: false }); //history.push("/") setTimeout(function(){ console.log("リダレクト処理") history.push("/dashboard") },2000); }) .catch((e) => { console.log(e) switch (e.code) { case "auth/network-request-failed": setError("通信がエラーになったのか、またはタイムアウトになりました。通信環境がいい所で再度やり直してください。"); break; case "auth/weak-password": setError("パスワードが正しくないです。"); break; case "auth/invalid-email": setError("メールアドレスが正しくないです。"); break; case "auth/requires-recent-login": setError("別の端末でログインしているか、セッションが切れたので再度、ログインしてください。(ログインページにリダイレクトします)"); setTimeout(function(){ console.log("リダレクト処理") history.push("/login") },3000); break; case "auth/user-disabled": setError("入力されたメールアドレスは無効(BAN)になっています。"); break; default: //想定外 setError("失敗しました。通信環境がいい所で再度やり直してください。"); } //ボタンの有効化 dispatch({ type: "setIsButtonDisabled", payload: false }); }) .finally(() => { dispatch({ type: "setIsButtonDisabled", payload: false }); }) }; const handleUsernameChange: React.ChangeEventHandler<HTMLInputElement> = (event) => { dispatch({ type: "setUsername", payload: event.target.value }); }; const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> = (event) => { dispatch({ type: "setPassword", payload: event.target.value }); }; const handlePasswordConfirmChange: React.ChangeEventHandler<HTMLInputElement> = (event) => { dispatch({ type: "setPasswordConfirm", payload: event.target.value }); }; async function handlesendEmailVerification() { setError("") try { await sendEmailVerification() setError("メールをおくりました。メール有効化をお願いします") } catch (e){ console.log(e) setError("有効化メールの送信に失敗しました") } } //あとで原因を調べる。わからない場合は別のツールを検討する formState.isSubmitted = false //一回submittedになるとレンダリングが遅くなり、変な動きするので強制的にfalseにする return ( <div className={classes.container} > <Typography variant="h4" align="center" component="h1" gutterBottom> プロフィールの更新 </Typography> <form noValidate autoComplete="off"> <Paper style={{ padding: 16 }}> {error && <div style={{ color: "red" }}>{error}</div>} {successMessage && <div variant="danger">{successMessage}</div>} <TextField error={state.isError} fullWidth id="username" name="username" type="email" label="Email" //placeholder="Email" margin="normal" value={state.username} onChange={handleUsernameChange} inputRef={register({pattern: /^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/ })} /> {errors.username?.type === "pattern" && <div style={{ color: "red" }}>メールアドレスの形式で入力されていません</div>} {!currentUser.emailVerified && <div>メールアドレスが有効化されていません{' '}<Button onClick={handlesendEmailVerification} variant="contained" color="primary">有効化</Button></div>} <TextField error={state.isError} fullWidth id="password" name="password" type="password" label="Password" placeholder="Password" margin="normal" onChange={handlePasswordChange} inputRef={register({ minLength: 6 })} /> {errors.password?.type === "minLength" && <div style={{ color: "red" }}>パスワードは6文字以上で入力してください</div>} <TextField error={state.isError} fullWidth id="password-confirm" name="password-confirm" type="password" label="Password-confirm" placeholder="Password-confirm" margin="normal" onChange={handlePasswordConfirmChange} inputRef={register} /> <Button variant="contained" size="large" fullWidth color="primary" className={classes.signupBtn} onClick={handleSubmit(handleUpdateProfile)} disabled={state.isButtonDisabled} > プロフィールを更新 </Button> </Paper> <Typography paragraph> ※表示名とアバター以外は公表される事はありません </Typography> <Typography paragraph> <Link to="/dashboard">dashboard</Link>に戻る </Typography> </form> </div> ); }; export default UpdateProfile;
ソース解説
このソースはSingUp処理をもとに作っています。cardではなく通常のフォームです。 SignUPはポップアップとかでも使えるようにしたかったのでcardにしています。
できていない所
バリデーションがちょっと変。一度バリデーション処理が走るとレンダリング処理が遅くなって、文字入力がおかしくなっている。たぶんuseMemoなどを使ってレンダリング処理を制御しないといけない。
メールアドレス変更のキャンセルページは、デザイン変更とかできない・・・アクションURLも設定できない。
感想
- インタフェースが別々なので更新処理を2つ投げる部分ができてよかった。
- この画面に項目を追加するんだけど・・・さらに複雑になりそう・・・