INSIDE · STATION · 참여형 번역 · 알림 · MY 화면
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../auth/auth.dart';
|
||||
|
||||
/// SNS 연동 — 화면 대조 원장 F6.
|
||||
///
|
||||
/// Figma `357:222740`(Apple) · `357:222899`(Google) · `357:222671`(Kakao).
|
||||
/// **화면들이 제공자만 다르고 같다** — 따로 만들면 연동 규칙이 여러 벌이 되고,
|
||||
/// 규칙이 바뀔 때 한 곳만 고치는 사고가 난다.
|
||||
///
|
||||
/// 별첨 B v1.4 — *하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다.*
|
||||
/// X(트위터)는 확정 4종에 없어 목록에서 빠졌다([linkableProviders]).
|
||||
///
|
||||
/// 명세 `554:22797` — *플랫폼별 연동 상태 반영 · **동일 이메일만 연동 가능***
|
||||
/// 판정은 [sameAccountIdentity] 가 하고, 실제 연결은 [AccountLinker] 구현체
|
||||
/// (Firebase `linkWithCredential`)가 한다.
|
||||
class AccountLinkScreen extends StatefulWidget {
|
||||
const AccountLinkScreen({
|
||||
super.key,
|
||||
required this.provider,
|
||||
required this.linker,
|
||||
this.linkedEmail,
|
||||
});
|
||||
|
||||
final AuthProviderKind provider;
|
||||
|
||||
/// 연결 경계. Firebase 어댑터이거나 [SampleAccountLinker] 다.
|
||||
final AccountLinker linker;
|
||||
|
||||
/// 이미 연동된 이메일. 없으면 미연동.
|
||||
final String? linkedEmail;
|
||||
|
||||
@override
|
||||
State<AccountLinkScreen> createState() => _AccountLinkScreenState();
|
||||
}
|
||||
|
||||
class _AccountLinkScreenState extends State<AccountLinkScreen> {
|
||||
late String? _linked = widget.linkedEmail;
|
||||
bool _busy = false;
|
||||
|
||||
bool get _connected => _linked != null;
|
||||
|
||||
String get _name => widget.provider.shortName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Theme.of(context).textTheme;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(context.l10n.linkTitle(_name))),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(AppSpace.s4),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpace.s4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cosmicSurfaceContainer,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(_name, style: t.titleMedium),
|
||||
const SizedBox(height: AppSpace.s2),
|
||||
Text(_linked ?? context.l10n.linkNotLinked, style: t.bodySmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpace.s4),
|
||||
Text(context.l10n.linkSameEmailOnly, style: t.labelSmall),
|
||||
const SizedBox(height: AppSpace.s5),
|
||||
FilledButton(
|
||||
onPressed: _busy ? null : _toggle,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(AppSizing.touchMinApp),
|
||||
backgroundColor: _connected
|
||||
? AppColors.cosmicSurfaceRaised
|
||||
: AppColors.cosmicAccent,
|
||||
foregroundColor: _connected
|
||||
? AppColors.onCosmic
|
||||
: AppColors.onCosmicAccent,
|
||||
),
|
||||
child: Text(
|
||||
_connected ? context.l10n.linkUnlink : context.l10n.linkLink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpace.s3),
|
||||
// 명세 `554:23411` — 연동 해제가 계정 삭제로 오인되지 않게 한다.
|
||||
Text(context.l10n.settingsUnlinkBody, style: t.labelSmall),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggle() async {
|
||||
final wasConnected = _connected;
|
||||
setState(() => _busy = true);
|
||||
final outcome = wasConnected
|
||||
? await widget.linker.unlink(widget.provider)
|
||||
: await widget.linker.link(widget.provider);
|
||||
if (!mounted) return;
|
||||
|
||||
// 화면 상태를 추측하지 않고 연결 경계에 다시 물어본다 — 성공했다고 믿고
|
||||
// 로컬 값을 뒤집으면 서버가 거절한 연결이 붙은 것처럼 보인다.
|
||||
final accounts = widget.linker.linkedAccounts;
|
||||
setState(() {
|
||||
_busy = false;
|
||||
_linked = accounts.containsKey(widget.provider)
|
||||
// 붙었는데 제공자가 이메일을 안 준 경우(Apple 재연동)도 연결 상태다.
|
||||
? accounts[widget.provider] ?? context.l10n.linkLinked
|
||||
: null;
|
||||
});
|
||||
|
||||
switch (outcome) {
|
||||
case AuthFailed(:final code, :final message):
|
||||
_toast('[$code] $message');
|
||||
case AuthNotConfigured(:final missing):
|
||||
_toast(context.l10n.settingsLinkNotReady(_name, missing.join(' · ')));
|
||||
case AuthSucceeded() || AuthCancelled():
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _toast(String message) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user