feat: README 多アカウント認証セクション追加
This commit is contained in:
parent
1a89559648
commit
e2ec67465e
2 changed files with 289 additions and 36 deletions
|
|
@ -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<void> 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<GAuthClient?> authenticate() async {
|
||||
/// OAuth 認証フローを実行(初回またはアカウント切り替え時)
|
||||
Future<GoogleSignInAccount?> 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<void> logout() async {
|
||||
try {
|
||||
await _signIn.signOut();
|
||||
print('[Gmail] ログアウト済み');
|
||||
} catch (e) {
|
||||
print('[Gmail] ログアウト失敗:$e');
|
||||
}
|
||||
}
|
||||
|
||||
/// アカウント切り替え(複数アカウントを持つ場合)
|
||||
Future<GoogleSignInAccount?> 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<void> 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<String?> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
180
lib/services/google/google_sign_in_provider.dart
Normal file
180
lib/services/google/google_sign_in_provider.dart
Normal file
|
|
@ -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<GoogleSignIn> _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<String> get allAccounts {
|
||||
return _accounts
|
||||
.where((s) => s.currentUser?.email != null)
|
||||
.map((s) => '${s.currentUser?.displayName ?? '未認証'} (${s.currentUser?.email})')
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// アカウントを登録・選択(設定画面より)
|
||||
Future<GoogleSignInAccount?> 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<void> logout() async {
|
||||
final signIn = _accounts.firstWhere(
|
||||
(s) => s.currentUser?.email == _currentAccountEmail,
|
||||
orElse: () => _accounts.last,
|
||||
);
|
||||
await signOut();
|
||||
_currentAccountEmail = null;
|
||||
print('[GoogleSignIn] ログアウト');
|
||||
}
|
||||
|
||||
/// アカウント切り替え(複数アカウントを持つ場合)
|
||||
Future<GoogleSignInAccount?> 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<GoogleSignInAccount?> get onAccountChanged =>
|
||||
_accounts.firstWhere((s) => s.currentUser != null).authStateChanges;
|
||||
}
|
||||
|
||||
/// アカウント選択画面用ウィジェット
|
||||
class GoogleAccountsSelectScreen extends StatelessWidget {
|
||||
final String? title;
|
||||
final FutureOr<String>? 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<void> _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 {
|
||||
// キャンセルまたはエラーの場合は、アプリ終了またはホーム画面へ誘導
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue