🚦

Firebaseでユーザーの退会機能を作る

2022/08/21に公開

これがないとAppleさんから、リジェクトを食らうらしい😇

FirebaseAuthenticationには、ユーザーを削除する機能があるそうです。
公式ドキュメント
https://firebase.flutter.dev/docs/auth/manage-users

こちらを書けば良いとのこと

await user?.delete();

メソッドにするとこんな感じですね。userコレクションと、messagesサブコレクションも削除するように、書いています。

void deleteUser() async {
    final user = FirebaseAuth.instance.currentUser;
    final uid = user?.uid;
    // userコレクションを削除
    final msg =
        await FirebaseFirestore.instance.collection('users').doc(uid).delete();
    // messagesサブコレクションを削除
    await FirebaseFirestore.instance
        .collection('users')
        .doc(uid)
        .collection('messages')
        .doc(uid)
        .delete();
    // ユーザーを削除
    await user?.delete();
    await FirebaseAuth.instance.signOut();
    print('ユーザーを削除しました!');
  }

認証だけだと、こんな感じになるのでしょうね。関数を作るか、ボタンの中に直接書く。

// 定数を定義する。
final user = FirebaseAuth.instance.currentUser;
// uidを取得する
final uid = user?.uid;
// ?つけないとエラーが出る!
await user?.delete();

で、他の記事で使っているコードに機能を追加して、プログラムを作ってみました。
ダイアログが使えるクラスを使ってWidgetは切り分けてます。
ダイアログの作り方は、こちらのページを参考にいたしました。
https://www.kamo-it.org/blog/flutter-dialog/

import 'package:auth_crud/main.dart';
import 'package:auth_crud/page/group_info.dart';
import 'package:auth_crud/page/home_page.dart';
import 'package:auth_crud/page/update.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';

class DeleteUserPage extends StatefulWidget {
  DeleteUserPage({Key? key}) : super(key: key);

  
  State<DeleteUserPage> createState() => _DeleteUserPageState();
}

class _DeleteUserPageState extends State<DeleteUserPage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('HOME'),
      ),
      body: Center(
        child: Column(
          children: [
            ElevatedButton(
                onPressed: () async {
                  final String? selectedText = await showDialog<String>(
                      context: context,
                      builder: (_) {
                        return SimpleDialogSample();
                      });
                  print('ユーザーを削除しました!');
                  Navigator.push(context,
                      MaterialPageRoute(builder: (context) => GroupInfoPage()));
                },
                child: Text('ユーザーを削除')),
          ],
        ),
      ),
    );
  }
}

class SimpleDialogSample extends StatefulWidget {
  SimpleDialogSample({Key? key}) : super(key: key);

  
  State<SimpleDialogSample> createState() => _SimpleDialogSampleState();
}

class _SimpleDialogSampleState extends State<SimpleDialogSample> {
  void deleteUser() async {
    final user = FirebaseAuth.instance.currentUser;
    final uid = user?.uid;
    final msg =
        await FirebaseFirestore.instance.collection('users').doc(uid).delete();
    await FirebaseFirestore.instance
        .collection('users')
        .doc(uid)
        .collection('messages')
        .doc(uid)
        .delete();
    // ユーザーを削除
    await user?.delete();
    await FirebaseAuth.instance.signOut();
    print('ユーザーを削除しました!');
  }

  
  Widget build(BuildContext context) {
    return SimpleDialog(
      title: Text('退会してもよろしいですか?'),
      children: [
        SimpleDialogOption(
          child: Text('退会する'),
          onPressed: () async {
            deleteUser();
            print('ユーザーを削除しました!');
            Navigator.push(context,
                MaterialPageRoute(builder: (context) => GroupInfoPage()));
          },
        ),
        SimpleDialogOption(
          child: Text('退会しない'),
          onPressed: () {
            Navigator.push(context,
                MaterialPageRoute(builder: (context) => HomePage()));
            print('キャンセルされました!');
          },
        )
      ],
    );
  }
}

退会したら移動する静的ページのコード

import 'package:auth_crud/main.dart';
import 'package:flutter/material.dart';

class GroupInfoPage extends StatelessWidget {
  const GroupInfoPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        title: Text('退会完了'),
      ),
      body: Center(
        child: Column(
          children: [
            Text('退会手続きを完了いたしました!'),
            SizedBox(height: 20),
            ElevatedButton(
                onPressed: () {
                  Navigator.push(context,
                      MaterialPageRoute(builder: (context) => SignInPage()));
                },
                child: Text('新規登録へ戻る')),
          ],
        ),
      ),
    );
  }
}

で戻ると、Widgetのエラーが発生した!
原因は、MaterialAppと、Scaffoldが必要でした。

他の記事のコードを修正

main.dartにログインのページを作ったので、こんな感じになってます。

import 'package:auth_crud/firebase_options.dart';
import 'package:auth_crud/page/home_page.dart';
import 'package:auth_crud/page/sign_up.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
        theme: ThemeData(primaryColor: Colors.blue),
        title: "auth_crud",
        home: SignInPage());
  }
}

class SignInPage extends StatefulWidget {
  const SignInPage({Key? key}) : super(key: key);

  
  State<SignInPage> createState() => _SignInPageState();
}

class _SignInPageState extends State<SignInPage> {
  String? email;
  String? password;

  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FlutterZero'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          children: [
            TextField(
              controller: _emailController,
              onChanged: (email) {
                this.email = email;
              },
              decoration: InputDecoration(hintText: 'Email'),
            ),
            TextField(
              controller: _passwordController,
              onChanged: (password) {
                this.password = password;
              },
              obscureText: true,
              decoration: InputDecoration(hintText: 'Password'),
            ),
            ElevatedButton(
              child: Text('Sign In'),
              onPressed: () async {
                try {
                  await FirebaseAuth.instance.signInWithEmailAndPassword(
                    email: _emailController.text.trim(),
                    password: _passwordController.text.trim(),
                  );
                  final user = FirebaseAuth.instance.currentUser!;

                  final snackBar = SnackBar(
                    content: Text(user.email!),
                  );

                  ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  Navigator.push(context,
                      MaterialPageRoute(builder: (context) => HomePage()));
                } catch (e) {
                  print(e);
                }
              },
            ),
            TextButton(
                onPressed: () {
                  Navigator.push(context,
                      MaterialPageRoute(builder: (context) => SignUpPage()));
                },
                child: Text('新規登録'))
          ],
        ),
      ),
    );
  }
}

成功するとこんな感じで、ログ出ました。

Restarted application in 595ms.
flutter: キャンセルされました!
2
flutter: ユーザーを削除しました!
Application finished.
Exited

補足情報

ログインして時間が経つとユーザーを削除する機能が使えなくなってしまうので、その場合どのように対処したかというと、またログインをさせて再認証するとことによって、deleteメソッドを使えるようにしました。
Cloud Functionsを使う方法もあるのですが、お金をかけたくないので、リリースしたアプリにはこちらの再認証させる方法を使いました。

追加情報

最近、Cloud Functionsの使い方がわかってきたので、ロジックを書いておきます。
こちらの記事を見て、退会機能の作り方が理解できました。
https://zenn.dev/tama8021/articles/0920_flutter_withdrawal

  • やること
    • 退会コレクションを作る.
    • 退会コレクションに、uidとTimestampを保存する.
    • ドキュメントidとuidが一致してればユーザーは削除される.

ソースコード

Cloud Functionsでプロジェクトを作る
https://firebase.google.com/docs/functions/get-started?hl=ja&authuser=0

Flutterのログイン機能を持っているアプリにコードを追加。退会ページに設置してください。
userのコードは、こんな感じになると思います。?つけないと怒れるかも。
delete_usersが作成されると、メールアドレスとパスワードログインで登録した、ユーザー情報が削除されます。

final user = FirebaseAuth.instance.currentUser;
ElevatedButton(
            onPressed: () async {
              final data = {
                "uid": user.uid,
                "deletedAt": Timestamp.now(),
              };
              await FirebaseFirestore.instance
                  .collection('delete_users')
                  .add(data)
                  .then((value) async => {
                        await FirebaseAuth.instance.signOut(),
                        Navigator.of(context).pushReplacement(
                            MaterialPageRoute(builder: (context) {
                          return LoginPage();
                        }))
                      })
                  .catchError((e) => print("Failed to add user: $e"));
            },
            child: Text('退会する'),
          )

Cloud Functionsのコード

TypeScriptで作成してます。関数は使う前にDeployしておいてください!

index.ts
// Cloud Functions for Firebase SDKでCloud Functionsを作成し、トリガーを設定します。
import * as functions from "firebase-functions";
// FirestoreにアクセスするためのFirebase Admin SDKです。
import * as admin from "firebase-admin";
admin.initializeApp();

// 削除するユーザーを登録するコレクションが作成されたら、FirebaseAuthenticatonのユーザーデータを削除する.
// リージョンは、asia-northeast1を設定。自分の設定したリージョンに合わせる。
exports.deleteUser = functions.region("asia-northeast1").firestore.document("deleted_users/{docId}").onCreate(async (snap, context) => {
  const deleteDocument = snap.data();
  const uid = deleteDocument.uid;

  await admin.auth().deleteUser(uid);
});

Discussion