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
|
// Version: 1.0.0
|
||||||
import 'dart:async';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'dart:convert';
|
import 'package:google_sign_in/google_sign_in.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:googleapis_auth/google_auth.dart';
|
import 'package:googleapis_auth/google_auth.dart';
|
||||||
import 'package:googleapis/gmail/v1.dart';
|
import 'package:googleapis/gmail/v1.dart';
|
||||||
|
|
||||||
/// Gmail API を介した同期用メールリレー
|
/// Gmail API を介した同期用メールリレー(複数アカウント対応)
|
||||||
///
|
///
|
||||||
/// P2P 通信不要なノード識別システムを提供します。
|
/// P2P 通信不要なノード識別システムを提供します。
|
||||||
/// BCC の Gmail アドレスをノードの一意キーとして使用。
|
/// BCC の Gmail アドレスをノードの一意キーとして使用。
|
||||||
class GmailWrapper {
|
class GmailWrapper {
|
||||||
final String _gmailAddress; // BCC 用 gmail アドレス(ノードキー)
|
final String _gmailAddress; // BCC 用 gmail アドレス(ノードキー)
|
||||||
final GAuthClient? _authClient; // OAuth 認証クライアント
|
GAuthClient? _authClient; // OAuth 認証クライアント
|
||||||
final GmailService? _gmail;
|
final GmailService? _gmail; // Gmail API サービス
|
||||||
|
final GoogleSignIn _signIn; // GoogleSignIn インスタンス
|
||||||
|
|
||||||
/// 新しいインスタンスを作成
|
/// 新しいインスタンスを作成
|
||||||
factory GmailWrapper({
|
factory GmailWrapper({
|
||||||
required String gmailAddress,
|
required String gmailAddress,
|
||||||
bool useOAuth = true,
|
bool useOAuth = true,
|
||||||
}) async {
|
}) {
|
||||||
if (useOAuth) {
|
if (useOAuth) {
|
||||||
try {
|
print('[Gmail] OAuth 認証モード。GoogleSignIn でアカウント選択を行います');
|
||||||
final client = GAuthClient.fromFile('credentials.json');
|
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(
|
return GmailWrapper._internal(
|
||||||
gmailAddress: gmailAddress,
|
gmailAddress: gmailAddress,
|
||||||
authClient: client,
|
signIn: signIn,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
|
||||||
// credentials.json がない場合、後で認証フローを実行できるように null を保持
|
|
||||||
print('[Gmail] credentials.json 未見。認証画面から開始してください');
|
|
||||||
return GmailWrapper._internal(
|
|
||||||
gmailAddress: gmailAddress,
|
|
||||||
authClient: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedError('OAuth 方式でのみサポートされています');
|
throw UnsupportedError('OAuth 方式でのみサポートされています');
|
||||||
}
|
}
|
||||||
|
|
@ -41,15 +40,25 @@ class GmailWrapper {
|
||||||
|
|
||||||
GmailWrapper._internal({
|
GmailWrapper._internal({
|
||||||
required this._gmailAddress,
|
required this._gmailAddress,
|
||||||
GAuthClient? authClient,
|
required GoogleSignIn signIn,
|
||||||
}) : _authClient = authClient,
|
}) : _signIn = signIn,
|
||||||
_gmail = authClient != null ? GmailService(authClient) : null;
|
_authClient = null,
|
||||||
|
_gmail = null;
|
||||||
|
|
||||||
/// ノード ID(BCC アドレス)を取得
|
/// ノード ID(BCC アドレス)を取得
|
||||||
String get gmailAddress => _gmailAddress;
|
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({
|
Future<void> sendMessage({
|
||||||
|
|
@ -60,9 +69,6 @@ class GmailWrapper {
|
||||||
if (_gmail == null) return;
|
if (_gmail == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// シンプルなテキストの送信(HTML のため簡易 escaping)
|
|
||||||
final body = base64Encode(utf8.encode(message));
|
|
||||||
|
|
||||||
final msg = EmailMessage(
|
final msg = EmailMessage(
|
||||||
subject: '[SalesAssist1] チャット:$fromNode',
|
subject: '[SalesAssist1] チャット:$fromNode',
|
||||||
to: toNode,
|
to: toNode,
|
||||||
|
|
@ -89,15 +95,82 @@ class GmailWrapper {
|
||||||
return []; // ここに母艦が管理するノードリストを読み込むロジック
|
return []; // ここに母艦が管理するノードリストを読み込むロジック
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 認証フローを起動(初回実行時)
|
/// OAuth 認証フローを実行(初回またはアカウント切り替え時)
|
||||||
Future<GAuthClient?> authenticate() async {
|
Future<GoogleSignInAccount?> authenticate() async {
|
||||||
try {
|
try {
|
||||||
final client = GAuthClient.fromFile('credentials.json');
|
final account = await _signIn.signIn();
|
||||||
print('[Gmail] 認証済みクライアント使用');
|
if (account != null) {
|
||||||
return client;
|
print('[Gmail] 認証成功:${account.email}');
|
||||||
|
return account;
|
||||||
|
} else {
|
||||||
|
throw Exception('ユーザーが認証をキャンセルしました');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[Gmail] 認証フロー起動:$e');
|
print('[Gmail] 認証失敗:$e');
|
||||||
// GoogleSignIn で認証画面をポップアップさせる処理(後実装)
|
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;
|
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