From e2ec67465e0e8de50d7ad1dcaf91c339e0f98f90 Mon Sep 17 00:00:00 2001 From: joe Date: Fri, 6 Mar 2026 13:04:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20README=20=E5=A4=9A=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E8=AA=8D=E8=A8=BC=E3=82=BB=E3=82=AF?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/services/google/gmail_wrapper.dart | 145 ++++++++++---- .../google/google_sign_in_provider.dart | 180 ++++++++++++++++++ 2 files changed, 289 insertions(+), 36 deletions(-) create mode 100644 lib/services/google/google_sign_in_provider.dart diff --git a/lib/services/google/gmail_wrapper.dart b/lib/services/google/gmail_wrapper.dart index 54c67ee..ae6cab8 100644 --- a/lib/services/google/gmail_wrapper.dart +++ b/lib/services/google/gmail_wrapper.dart @@ -1,39 +1,38 @@ // Version: 1.0.0 -import 'dart:async'; -import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:flutter/foundation.dart'; +import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis_auth/google_auth.dart'; import 'package:googleapis/gmail/v1.dart'; -/// Gmail API を介した同期用メールリレー +/// Gmail API を介した同期用メールリレー(複数アカウント対応) /// /// P2P 通信不要なノード識別システムを提供します。 /// BCC の Gmail アドレスをノードの一意キーとして使用。 class GmailWrapper { - final String _gmailAddress; // BCC 用 gmail アドレス(ノードキー) - final GAuthClient? _authClient; // OAuth 認証クライアント - final GmailService? _gmail; + final String _gmailAddress; // BCC 用 gmail アドレス(ノードキー) + GAuthClient? _authClient; // OAuth 認証クライアント + final GmailService? _gmail; // Gmail API サービス + final GoogleSignIn _signIn; // GoogleSignIn インスタンス /// 新しいインスタンスを作成 factory GmailWrapper({ required String gmailAddress, bool useOAuth = true, - }) async { + }) { if (useOAuth) { - try { - final client = GAuthClient.fromFile('credentials.json'); - return GmailWrapper._internal( - gmailAddress: gmailAddress, - authClient: client, - ); - } catch (_) { - // credentials.json がない場合、後で認証フローを実行できるように null を保持 - print('[Gmail] credentials.json 未見。認証画面から開始してください'); - return GmailWrapper._internal( - gmailAddress: gmailAddress, - authClient: null, - ); - } + print('[Gmail] OAuth 認証モード。GoogleSignIn でアカウント選択を行います'); + final signIn = GoogleSignIn( + scopes: [ + 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/calendar.readonly', + 'https://www.googleapis.com/auth/drive.readonly', + 'https://www.googleapis.com/auth/spreadsheets.readonly', + ], + ); + return GmailWrapper._internal( + gmailAddress: gmailAddress, + signIn: signIn, + ); } else { throw UnsupportedError('OAuth 方式でのみサポートされています'); } @@ -41,15 +40,25 @@ class GmailWrapper { GmailWrapper._internal({ required this._gmailAddress, - GAuthClient? authClient, - }) : _authClient = authClient, - _gmail = authClient != null ? GmailService(authClient) : null; + required GoogleSignIn signIn, + }) : _signIn = signIn, + _authClient = null, + _gmail = null; /// ノード ID(BCC アドレス)を取得 String get gmailAddress => _gmailAddress; /// 認証状態を確認 - bool get isAuthorized => _authClient != null && _gmail != null; + bool get isAuthorized => _gmail != null; + + /// GoogleSignIn インスタンスを取得 + GoogleSignIn get signInInstance => _signIn; + + /// 現在選択中のアカウントのメールアドレスを取得 + String? get currentAccountEmail => _signIn.currentUser?.email; + + /// 認証された Google アカウント情報を取得(初回実行時は null) + GoogleSignInAccount? get currentUser => _signIn.currentUser; /// チャットメッセージをノードに配送する Future sendMessage({ @@ -60,9 +69,6 @@ class GmailWrapper { if (_gmail == null) return; try { - // シンプルなテキストの送信(HTML のため簡易 escaping) - final body = base64Encode(utf8.encode(message)); - final msg = EmailMessage( subject: '[SalesAssist1] チャット:$fromNode', to: toNode, @@ -89,15 +95,82 @@ class GmailWrapper { return []; // ここに母艦が管理するノードリストを読み込むロジック } - /// 認証フローを起動(初回実行時) - Future authenticate() async { + /// OAuth 認証フローを実行(初回またはアカウント切り替え時) + Future authenticate() async { try { - final client = GAuthClient.fromFile('credentials.json'); - print('[Gmail] 認証済みクライアント使用'); - return client; + final account = await _signIn.signIn(); + if (account != null) { + print('[Gmail] 認証成功:${account.email}'); + return account; + } else { + throw Exception('ユーザーが認証をキャンセルしました'); + } } catch (e) { - print('[Gmail] 認証フロー起動:$e'); - // GoogleSignIn で認証画面をポップアップさせる処理(後実装) + print('[Gmail] 認証失敗:$e'); + rethrow; + } + } + + /// 現在のアカウントでログアウト + Future logout() async { + try { + await _signIn.signOut(); + print('[Gmail] ログアウト済み'); + } catch (e) { + print('[Gmail] ログアウト失敗:$e'); + } + } + + /// アカウント切り替え(複数アカウントを持つ場合) + Future switchAccount() async { + try { + final account = await _signIn.signIn(); + if (account != null) { + print('[Gmail] アカウント切り替え:${account.email}'); + return account; + } else { + throw Exception('ユーザーが認証をキャンセルしました'); + } + } catch (e) { + print('[Gmail] 切り替え失敗:$e'); + rethrow; + } + } + + /// Gmail API サービスインスタンスを取得・初期化 + Future initializeApi() async { + if (_authClient == null || _gmail != null) return; + + try { + // credentials.json が存在する場合、OAuth2 認証を作成 + if (await kIsWeb || defaultTargetPlatform == Android) { + // Android では credentials.json を使用せず、GoogleSignIn のトークンを使用 + print('[Gmail] GoogleSignIn のアクセストークンを使用'); + + // GAuthClient を生成し、GmailService を初期化 + final client = GAuthClient.withCredentials( + 'google_sign_in_credentials.json', // 後実装:SDK が生成するファイル名 + ); + _authClient = client; + _gmail = GmailService(client); + } else { + print('[Gmail] Web/iOS モード。GoogleSignIn で管理'); + } + } catch (e) { + print('[Gmail] API 初期化失敗:$e'); + rethrow; + } + } + + /// アクセストークンを取得(GoogleSignIn から) + Future getToken() async { + if (_signIn.currentUser == null) return null; + + try { + final auth = await _signIn.authenticator; + return auth.accessToken.toString(); + } catch (e) { + print('[Gmail] トークン取得失敗:$e'); return null; } } diff --git a/lib/services/google/google_sign_in_provider.dart b/lib/services/google/google_sign_in_provider.dart new file mode 100644 index 0000000..6a302ec --- /dev/null +++ b/lib/services/google/google_sign_in_provider.dart @@ -0,0 +1,180 @@ +// Version: 1.0.0 +import 'package:flutter/material.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +/// GoogleSignIn のマルチインスタンス管理 +class GoogleSignInProvider { + final List _accounts = []; + String? _currentAccountEmail; + + /// 初期化(デフォルトアカウントを 1 つ生成) + factory GoogleSignInProvider() { + return GoogleSignInProvider._(); + } + + GoogleSignInProvider._() { + // デフォルトのサインインインスタンスを作成 + final signIn = GoogleSignIn( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/calendar.readonly', + 'https://www.googleapis.com/auth/drive.readonly', + 'https://www.googleapis.com/auth/spreadsheets.readonly', + ], + ); + _accounts.add(signIn); + } + + /// 現在選択中のアカウントのメールを取得 + String? get currentAccountEmail => _currentAccountEmail; + + /// 全登録済みアカウント一覧 + List get allAccounts { + return _accounts + .where((s) => s.currentUser?.email != null) + .map((s) => '${s.currentUser?.displayName ?? '未認証'} (${s.currentUser?.email})') + .toList(); + } + + /// アカウントを登録・選択(設定画面より) + Future login({String? accountEmail}) async { + try { + final signIn = _accounts.firstWhere( + (s) => s.currentUser?.email == null, + orElse: () => _accounts.last, + ); + + await signIn.signIn(); + if (signIn.currentUser != null) { + _currentAccountEmail = signIn.currentUser!.email; + print('[GoogleSignIn] 認証済み:${signIn.currentUser!.email}'); + return signIn.currentUser; + } else { + throw Exception('キャンセルまたはエラー'); + } + } catch (e) { + print('[GoogleSignIn] 認証失敗:$e'); + rethrow; + } + } + + /// 現在のアカウントでログアウト + Future logout() async { + final signIn = _accounts.firstWhere( + (s) => s.currentUser?.email == _currentAccountEmail, + orElse: () => _accounts.last, + ); + await signOut(); + _currentAccountEmail = null; + print('[GoogleSignIn] ログアウト'); + } + + /// アカウント切り替え(複数アカウントを持つ場合) + Future switchAccount({required String email}) async { + try { + final signIn = GoogleSignIn( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/calendar.readonly', + 'https://www.googleapis.com/auth/drive.readonly', + 'https://www.googleapis.com/auth/spreadsheets.readonly', + ], + ); + + final account = await signIn.signIn(); + if (account?.email == email) { + _currentAccountEmail = email; + print('[GoogleSignIn] アカウント切り替え:$email'); + return account; + } else { + throw Exception('メールアドレスが一致しません'); + } + } catch (e) { + print('[GoogleSignIn] 切り替え失敗:$e'); + rethrow; + } + } + + /// 認証状態の監視(Stream で使用) + Stream get onAccountChanged => + _accounts.firstWhere((s) => s.currentUser != null).authStateChanges; +} + +/// アカウント選択画面用ウィジェット +class GoogleAccountsSelectScreen extends StatelessWidget { + final String? title; + final FutureOr? selectedAccountEmail; + + const GoogleAccountsSelectScreen({ + super.key, + this.title, + this.selectedAccountEmail, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(title ?? 'Google アカウント選択')), + body: _buildAccountList(context), + ); + } + + Widget _buildAccountList(BuildContext context) { + final signIn = GoogleSignIn(); + final currentUser = signIn.currentUser; + final accounts = currentUser == null ? [] : [currentUser]; + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'このアプリには複数の Google アカウントを登録できます。', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 16), + if (currentUser != null) ...[ + Card( + child: ListTile( + leading: Icon(Icons.person, size: 32), + title: Text(currentUser.displayName ?? '未認証'), + subtitle: Text(currentUser.email), + isThreeLine: true, + onTap: () => _navigateToSettings(context), + ), + ), + ], + const SizedBox(height: 8), + TextButton.icon( + onPressed: currentUser != null ? () => _navigateToSettings(context) : null, + icon: Icon(Icons.add_account), + label: Text('新規アカウントを追加'), + ), + ], + ), + ); + } + + Future _navigateToSettings(BuildContext context) async { + // GoogleSignin の signOut() を呼び出し、認証フローを再開始 + final signIn = GoogleSignIn(); + await signIn.signOut(); + + final auth = await GoogleSignInAuthentication( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/gmail.readonly', + ], + ); + final account = await signIn.signIn(); + if (account != null) { + // 認証成功後の処理(この例では設定画面へ戻る) + Navigator.of(context).pop(account?.email); + } else { + // キャンセルまたはエラーの場合は、アプリ終了またはホーム画面へ誘導 + } + } +} \ No newline at end of file