1차 보완(0.1.1+14) 내용 합치기
This commit is contained in:
@@ -133,19 +133,6 @@ abstract interface class AuthSession {
|
||||
Future<void> signOut();
|
||||
}
|
||||
|
||||
/// 「동일 이메일만 연동 가능」 판정 — 명세 `554:22797` · 안내 문구 `554:23251`.
|
||||
///
|
||||
/// **제공자가 인증한 이메일 전체**를 정규화(trim·소문자)해 비교한다.
|
||||
/// 로컬 파트만 보면 `test123@naver.com` 과 `test123@gmail.com` 처럼 남의 계정이
|
||||
/// 붙는다. 비었거나 `@` 가 없는 값은 이메일로 보지 않아 false.
|
||||
///
|
||||
/// Figma 예시(앞부분 같으면 O)는 **2026-09-16 회신 3절 7항**(「다른 도메인 이메일의 앞부분만으로
|
||||
/// 동일 소유자를 판단하지 않습니다」)이 대체했다. 이메일을 주지 않는 제공자의 연결은 해석 확인 Q-E8.
|
||||
bool sameAccountIdentity(String a, String b) {
|
||||
final x = a.trim().toLowerCase();
|
||||
return x.contains('@') && x == b.trim().toLowerCase();
|
||||
}
|
||||
|
||||
/// 연동 화면에 거는 제공자 — 별첨 B v1.4 확정 4종.
|
||||
/// 이메일은 QA 내부 경로라 목록에 없다.
|
||||
const List<AuthProviderKind> linkableProviders = [
|
||||
@@ -159,6 +146,11 @@ const List<AuthProviderKind> linkableProviders = [
|
||||
///
|
||||
/// 별첨 B v1.4 — *하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다.*
|
||||
/// Firebase 는 `User.linkWithCredential` / `User.unlink` 로 이걸 지원한다.
|
||||
///
|
||||
/// 통합검토회신 v2.3 **Q-E8**(C 안 보완) — 연결은 현재 계정의 유효한 인증 상태와 새 Provider 인증 자격을
|
||||
/// **서버가** 모두 확인해 붙인다(Firebase Auth `linkWithCredential` · 카카오는 `fnAuthKakao`). 이메일 일치는
|
||||
/// 소유 확인이 아니어서 비교하지 않는다 — 이메일을 주지 않는 카카오 · LINE 계정도 붙는다. 그 Provider 가
|
||||
/// 다른 FANUZU UID 에 붙어 있으면 거부하고(E-AUTH-08) 자동 병합하지 않는다.
|
||||
/// 저장소와 화면은 이 인터페이스만 알고 SDK 는 어댑터 뒤에 둔다 — 덕분에 설정
|
||||
/// 파일 없이도 연결·해제 규칙을 단위 테스트할 수 있다.
|
||||
abstract interface class AccountLinker {
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../core/firebase/firebase_boot.dart';
|
||||
import 'auth.dart';
|
||||
import 'firebase_account_linker.dart';
|
||||
import 'social_providers.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
|
||||
@@ -120,9 +119,7 @@ class AuthRepository extends ChangeNotifier {
|
||||
/// 사용자가 닿는 경로가 없다 — 읽는 분기가 없는 기능 플래그로 닫는 것보다 확실하다.
|
||||
/// QA 경로는 [qaEmailProvider] 하나뿐이다.
|
||||
///
|
||||
/// 카카오는 공식 SDK + 서버 커스텀 토큰([KakaoSignInProvider]), LINE 은 Firebase OIDC
|
||||
/// 제공자다. OIDC 를 콘솔에서 켜지 않았으면 Firebase 가 `operation-not-allowed`(E-AUTH-11)를
|
||||
/// 돌려준다.
|
||||
/// 카카오 · LINE 은 공식 SDK + 서버 커스텀 토큰([KakaoSignInProvider] · [LineSignInProvider])이다.
|
||||
static List<SocialAuthProvider> _defaultProviders() => [
|
||||
const GoogleSignInProvider(),
|
||||
// Android 는 Apple SDK 가 없어 Firebase 웹 흐름(`apple.com` 제공자)으로 간다 —
|
||||
@@ -138,15 +135,8 @@ class AuthRepository extends ChangeNotifier {
|
||||
// 동의 항목은 카카오 콘솔 설정을 따른다(지금은 닉네임). 이메일(account_email)은 비즈 앱
|
||||
// 전환 후 콘솔에서 켜면 서버가 동의·인증된 값만 받는다.
|
||||
KakaoSignInProvider(),
|
||||
// LINE 동의 항목: profile·email(이메일 권한 신청 승인 후).
|
||||
OidcSignInProvider(
|
||||
AuthProviderKind.line,
|
||||
providerId: FirebaseAccountLinker.providerIds[AuthProviderKind.line]!,
|
||||
scopes: const ['profile', 'email'],
|
||||
// LINE 앱 자동 로그인으로 넘어가면 콜백이 Custom Tab 밖에서 열려 로그인이
|
||||
// 깨진다(Android). LINE Login 공식 파라미터로 끈다.
|
||||
androidParameters: const {'disable_auto_login': 'true'},
|
||||
),
|
||||
// LINE 동의 항목: profile · openid · email(이메일 권한 신청 승인 후에만 온다 — 없어도 로그인된다).
|
||||
LineSignInProvider(),
|
||||
];
|
||||
|
||||
/// QA 계정(qa-test@fanuzu.co.kr) 전용 이메일 로그인 — **디버그 빌드에서만**.
|
||||
|
||||
@@ -27,8 +27,8 @@ final class FirebaseAccountLinker implements AccountLinker {
|
||||
AuthProviderKind.google: 'google.com',
|
||||
AuthProviderKind.apple: 'apple.com',
|
||||
// 카카오·LINE 은 Firebase 기본 제공자가 아니라 OIDC 커스텀 제공자로 붙는다.
|
||||
// 접두사 `oidc.` 는 Firebase 콘솔 등록 시 강제되는 형식이다. 카카오는 로그인을
|
||||
// 서버 커스텀 토큰으로 옮겼지만, 서버(fnAuthKakao)가 같은 `oidc.kakao` 로 연결을 남긴다.
|
||||
// 접두사 `oidc.` 는 Firebase 콘솔 등록 시 강제되는 형식이다. 두 제공자 모두 로그인을
|
||||
// 서버 커스텀 토큰으로 옮겼지만, 서버(fnAuthKakao · fnAuthLine)가 같은 ID 로 연결을 남긴다.
|
||||
AuthProviderKind.kakao: 'oidc.kakao',
|
||||
AuthProviderKind.line: 'oidc.line',
|
||||
AuthProviderKind.email: 'password',
|
||||
@@ -59,38 +59,28 @@ final class FirebaseAccountLinker implements AccountLinker {
|
||||
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
||||
if (impl is! CredentialProvider &&
|
||||
impl is! OidcSignInProvider &&
|
||||
impl is! KakaoSignInProvider) {
|
||||
impl is! KakaoSignInProvider &&
|
||||
impl is! LineSignInProvider) {
|
||||
return AuthNotConfigured(provider, const ['자격증명 구현']);
|
||||
}
|
||||
|
||||
try {
|
||||
if (impl is KakaoSignInProvider) return await impl.link(user);
|
||||
if (impl is LineSignInProvider) return await impl.link(user);
|
||||
if (impl is OidcSignInProvider) return await _linkOidc(user, impl);
|
||||
final source = impl as CredentialProvider;
|
||||
final obtained = await source.obtainCredential();
|
||||
// 사용자가 소셜 창을 닫았다. 실패로 세지 않는다.
|
||||
if (obtained == null) return const AuthCancelled();
|
||||
|
||||
// 명세 `554:22797` — *동일 이메일만 연동 가능*. 붙이기 **전에** 막는다.
|
||||
// 붙인 뒤 되돌리는 방식은 unlink 가 실패하면 남의 계정이 붙은 채로 남는다.
|
||||
//
|
||||
// ponytail: 제공자가 이메일을 알려 줄 때만 판정한다. Apple 은 최초 인가
|
||||
// 때만 이메일을 주므로 재연동 시 통과한다 — 서버(fn-account-link)에서
|
||||
// 다시 보려면 Functions 층에 같은 판정을 둔다.
|
||||
final mine = user.email;
|
||||
final theirs = obtained.email;
|
||||
if (mine != null &&
|
||||
theirs != null &&
|
||||
!sameAccountIdentity(mine, theirs)) {
|
||||
return AuthFailed('E-AUTH-08', appL10n.authDifferentEmail);
|
||||
}
|
||||
|
||||
// 이메일을 비교하지 않는다(Q-E8). Firebase 가 지금 로그인 토큰과 새 자격증명을 서버에서 함께
|
||||
// 검증하고, 그 Provider 가 다른 UID 에 붙어 있으면 credential-already-in-use 로 거부한다.
|
||||
final result = await user.linkWithCredential(obtained.credential);
|
||||
return AuthSucceeded(_accountOf(result.user, provider));
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
if (isWebFlowCancelled(error)) return const AuthCancelled();
|
||||
return AuthFailed(
|
||||
authCatalogCode(error),
|
||||
authCatalogCode(error, flow: AuthFlow.link),
|
||||
authMessage(error, flow: AuthFlow.link),
|
||||
);
|
||||
} on Object catch (error) {
|
||||
@@ -120,39 +110,20 @@ final class FirebaseAccountLinker implements AccountLinker {
|
||||
return AuthSucceeded(_accountOf(_firebaseAuth.currentUser, provider));
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed(
|
||||
authCatalogCode(error),
|
||||
authCatalogCode(error, flow: AuthFlow.link),
|
||||
authMessage(error, flow: AuthFlow.link),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 카카오·LINE(OIDC) 연결.
|
||||
///
|
||||
/// ponytail: OIDC 웹 흐름은 자격증명을 먼저 내주지 않고 연결까지 한 번에 끝낸다.
|
||||
/// 그래서 「동일 이메일만 연동」을 **붙인 뒤** 판정하고 어긋나면 바로 뗀다.
|
||||
/// 떼기가 실패하면 남의 이메일 계정이 붙은 채 남는다 — 막아야 하면 Functions 의
|
||||
/// blocking 함수(beforeSignIn)로 서버에서 거른다.
|
||||
/// 웹 흐름(OIDC — Android Apple) 연결 — 웹 흐름이 인증과 연결을 한 번에 끝낸다. 검증 · 충돌 거부는 Firebase 가 한다(Q-E8).
|
||||
Future<AuthOutcome> _linkOidc(fb.User user, OidcSignInProvider impl) async {
|
||||
final mine = user.email;
|
||||
final result = await untilAbandoned(
|
||||
user.linkWithProvider(impl.authProvider),
|
||||
final result = await retryStaleWebContext(
|
||||
() => untilAbandoned(user.linkWithProvider(impl.authProvider)),
|
||||
);
|
||||
// 연결된 제공자 정보에 그 계정의 이메일이 실린다(동의 항목에 이메일이 있을 때).
|
||||
final theirs = _emailOf(result.user, impl.providerId);
|
||||
if (mine != null && theirs != null && !sameAccountIdentity(mine, theirs)) {
|
||||
await user.unlink(impl.providerId);
|
||||
return AuthFailed('E-AUTH-08', appL10n.authDifferentEmail);
|
||||
}
|
||||
return AuthSucceeded(_accountOf(result.user, impl.provider));
|
||||
}
|
||||
|
||||
static String? _emailOf(fb.User? user, String providerId) {
|
||||
for (final info in user?.providerData ?? const <fb.UserInfo>[]) {
|
||||
if (info.providerId == providerId) return info.email;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
SocialAuthProvider? _implOf(AuthProviderKind provider) {
|
||||
for (final candidate in _providers) {
|
||||
if (candidate.provider == provider) return candidate;
|
||||
|
||||
@@ -215,6 +215,7 @@ class _SimpleLoginScreenState extends State<SimpleLoginScreen> {
|
||||
bool _busy = false;
|
||||
|
||||
Future<void> _signIn(SocialProvider provider) async {
|
||||
if (_busy) return;
|
||||
if (widget.signUp) {
|
||||
final agreed = await Navigator.of(context).push<Set<String>>(
|
||||
MaterialPageRoute(builder: (_) => const TermsScreen()),
|
||||
@@ -223,6 +224,8 @@ class _SimpleLoginScreenState extends State<SimpleLoginScreen> {
|
||||
// 인증 뒤 온보딩이 이 동의를 서버에 기록한다(약관 단계를 다시 묻지 않는다).
|
||||
widget.auth.pendingConsent = agreed;
|
||||
}
|
||||
// 제공자 화면에서 돌아온 뒤에도 서버 토큰 교환 · 로그인에 몇 초 걸린다(Kakao · LINE).
|
||||
// 끝날 때까지 화면 전체를 막고 진행 표시를 띄운다 — 다시 누르지 못하게.
|
||||
setState(() => _busy = true);
|
||||
final outcome = await widget.auth.signIn(provider, signUp: widget.signUp);
|
||||
if (!mounted) return;
|
||||
@@ -264,7 +267,7 @@ class _SimpleLoginScreenState extends State<SimpleLoginScreen> {
|
||||
for (final kind in _figmaOrder)
|
||||
...widget.auth.providers.where((p) => p.provider == kind),
|
||||
];
|
||||
return Scaffold(
|
||||
final scaffold = Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
@@ -347,9 +350,26 @@ class _SimpleLoginScreenState extends State<SimpleLoginScreen> {
|
||||
),
|
||||
),
|
||||
);
|
||||
return PopScope(
|
||||
canPop: !_busy,
|
||||
child: Stack(children: [scaffold, if (_busy) const BlockingProgress()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 진행 중 화면 전체를 막는 반투명 막 + 가운데 진행 표시.
|
||||
class BlockingProgress extends StatelessWidget {
|
||||
const BlockingProgress({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const Stack(
|
||||
children: [
|
||||
ModalBarrier(dismissible: false, color: Color(0x8A000000)),
|
||||
Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
const _figmaOrder = [
|
||||
AuthProviderKind.apple,
|
||||
AuthProviderKind.google,
|
||||
@@ -371,10 +391,12 @@ String providerButtonLabel(
|
||||
: t.authProviderLogin(kind.name);
|
||||
}
|
||||
|
||||
/// 로그인 결과 안내. 취소 · 중단도 안내한다 — 1차 검수 R01 「취소 · 실패 시 로그인 성공으로 처리하지 않음 ·
|
||||
/// 오류 안내와 정상 재시도 가능」. 아무 말 없이 화면이 그대로면 사용자는 앱이 멈춘 줄 안다.
|
||||
String? authNotice(AuthOutcome outcome, [AppLocalizations? l10n]) =>
|
||||
switch (outcome) {
|
||||
AuthSucceeded() => null,
|
||||
AuthCancelled() => null,
|
||||
AuthCancelled() => (l10n ?? koL10n).authSocialCancelled,
|
||||
AuthFailed(:final code, :final message) => '[$code] $message',
|
||||
AuthNotConfigured(:final provider) =>
|
||||
(l10n ?? koL10n).authNotConfiguredNotice(provider.shortName),
|
||||
|
||||
@@ -2,17 +2,24 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../../core/ui/app_dialog.dart';
|
||||
|
||||
/// 회원가입 약관 — Figma `179:24230`(미동의) · `179:24324`(전체 동의).
|
||||
///
|
||||
/// 필수 4항을 모두 체크해야 「완료」가 켜진다. 완료하면 동의한 항목 키([signupTermKeys] 중)를
|
||||
/// 필수 항목을 모두 체크해야 「완료」가 켜진다. 완료하면 동의한 항목 키([signupTermKeys] 중)를
|
||||
/// 돌려준다 — 가입 모드([SimpleLoginScreen])는 `pop` 결과로, 온보딩은 [onAgreed] 로 받는다.
|
||||
/// 서버(`fnSignupConsent`)가 같은 필수 목록을 다시 검사하고 항목별 동의 행을 남긴다.
|
||||
///
|
||||
/// 필수 항목은 Figma 의 서비스 · 위치기반 · 만 14세에 개인정보 처리방침을 더했다(10/3 통합회신
|
||||
/// 3절 4 「이용약관 · 개인정보처리방침 문구 제공」). 구성은 해석 확인 요청 E-Q1 회신 대기다.
|
||||
/// 약관 본문은 발주사 실서비스 문구를 받기 전까지 시안의 자리 표시 문구다 — 받으면 [_body] 와
|
||||
/// [signupTermsVersion] 을 함께 바꾼다(동의 기록에 버전이 남는다).
|
||||
/// 항목은 통합검토회신 v2.3 **Q-E1** · 1차 검수 회신서 **R02** 를 따른다(공통 기준 2항 — Figma 의
|
||||
/// 과거 구성보다 확정 회신이 우선).
|
||||
/// - 필수: 서비스 이용약관 · 개인정보 수집·이용 동의 · 만 14세 이상.
|
||||
/// - 선택: 마케팅 수신(기본 OFF).
|
||||
/// - **위치기반 서비스 약관은 여기서 받지 않는다** — Figma 에는 있으나 위치기능 최초 사용 직전에
|
||||
/// 따로 받는다(`features/quest/location_consent.dart` 의 `ensureLocationConsent`).
|
||||
/// - 개인정보처리방침은 **동의 항목이 아니라 고지**다 — 체크 칸 없이 하단 버튼 위에 안내와 보기 링크만 둔다.
|
||||
///
|
||||
/// 약관 본문은 발주사 실서비스 문구를 받기 전까지 시안의 자리 표시 문구다 — 받으면
|
||||
/// [termsPlaceholderBody] · l10n `terms*Body` 와 [signupTermsVersion] 을 함께 바꾼다(동의 기록에 버전이 남는다).
|
||||
class TermsScreen extends StatefulWidget {
|
||||
const TermsScreen({
|
||||
super.key,
|
||||
@@ -39,27 +46,33 @@ const signupTermsVersion = 'draft-1';
|
||||
List<String> get signupTermKeys => [for (final t in _Term.values) t.name];
|
||||
|
||||
enum _Term {
|
||||
service(required: true, body: true),
|
||||
privacy(required: true, body: true),
|
||||
location(required: true, body: true),
|
||||
age(required: true, body: false),
|
||||
marketing(required: false, body: true);
|
||||
service(required: true),
|
||||
privacy(required: true),
|
||||
age(required: true),
|
||||
marketing(required: false);
|
||||
|
||||
const _Term({required this.required, required this.body});
|
||||
const _Term({required this.required});
|
||||
|
||||
final bool required;
|
||||
final bool body;
|
||||
|
||||
String label(AppLocalizations l10n) => switch (this) {
|
||||
service => l10n.termsService,
|
||||
privacy => l10n.termsPrivacy,
|
||||
location => l10n.termsLocation,
|
||||
age => l10n.termsAge,
|
||||
marketing => l10n.termsMarketing,
|
||||
};
|
||||
|
||||
/// 항목 아래에 보이는 본문 — 항목마다 제목이 다르다. 만 14세는 본문 없이 체크 줄만 그린다.
|
||||
String? body(AppLocalizations l10n) => switch (this) {
|
||||
service => termsPlaceholderBody,
|
||||
privacy => l10n.termsPrivacyBody,
|
||||
age => null,
|
||||
marketing => l10n.termsMarketingBody,
|
||||
};
|
||||
}
|
||||
|
||||
const _body =
|
||||
/// 서비스 이용약관 본문 자리 표시 문구(시안 문구). 다른 약관 · 방침은 각자의 자리 표시 문구(l10n)를 쓴다.
|
||||
const termsPlaceholderBody =
|
||||
'FANUZU 서비스 이용약관\n\n제1조 (적용범위)\n서비스 이용약관에 대한 더미 텍스트 입니다.\n'
|
||||
'추후 작성해주시기 바랍니다. 서비스 이용약관에 대한 더미 텍스트 입니다.';
|
||||
|
||||
@@ -140,7 +153,43 @@ class _TermsScreenState extends State<TermsScreen> {
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpace.s4),
|
||||
child: _termsButtons(context),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 개인정보처리방침 — 동의 항목과 구분한 고지(Q-E1). 체크 칸이 없고 「보기」로 본문을 연다.
|
||||
// 목록 끝에 두면 iPhone 11 높이에서 하단 버튼 뒤로 가려지므로 버튼 위에 고정한다.
|
||||
Padding(
|
||||
key: const ValueKey('privacy-policy-notice'),
|
||||
padding: const EdgeInsets.only(bottom: AppSpace.s2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.termsPrivacyPolicyNotice,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.display,
|
||||
fontFamilyFallback: [AppFonts.body],
|
||||
fontSize: 12,
|
||||
height: 1.36,
|
||||
color: AppColors.onCosmicMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
key: const ValueKey('privacy-policy-link'),
|
||||
onPressed: () => showAlertDialog(
|
||||
context,
|
||||
context.l10n.termsPrivacyPolicy,
|
||||
body: context.l10n.termsPrivacyPolicyBody,
|
||||
),
|
||||
child: Text(context.l10n.commonView),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_termsButtons(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
@@ -156,10 +205,12 @@ class _TermsScreenState extends State<TermsScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final t in _Term.values) ...[
|
||||
for (final (t, body) in [
|
||||
for (final t in _Term.values) (t, t.body(context.l10n)),
|
||||
]) ...[
|
||||
const SizedBox(height: AppSpace.s3),
|
||||
_Box(
|
||||
child: t.body
|
||||
child: body != null
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -174,15 +225,17 @@ class _TermsScreenState extends State<TermsScreen> {
|
||||
onTap: () => _toggle(t),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: AppSpace.s4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: AppSpace.s4,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
child: ClipRect(
|
||||
child: Text(
|
||||
_body,
|
||||
body,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.display,
|
||||
fontFamilyFallback: [AppFonts.body],
|
||||
fontSize: 12,
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart' as gsi;
|
||||
import 'package:flutter_line_sdk/flutter_line_sdk.dart' as line;
|
||||
import 'package:kakao_flutter_sdk_user/kakao_flutter_sdk_user.dart' as kakao;
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart' as apple;
|
||||
|
||||
@@ -26,12 +27,12 @@ abstract interface class CredentialProvider {
|
||||
/// 사용자가 취소하면 null. 쓸 수 없는 상태면 [AuthNotConfigured] 를 던지는 대신
|
||||
/// 호출부가 [SocialAuthProvider.missingConfig] 를 먼저 본다.
|
||||
///
|
||||
/// [SocialCredential.email] 은 「동일 이메일만 연동 가능」(명세 `554:22797`)
|
||||
/// 판정에 쓴다. 제공자가 알려 주지 않으면 null 이다.
|
||||
/// [SocialCredential.email] 은 제공자가 알려 줄 때만 있다(null 가능). 계정 연결의 소유 확인에는
|
||||
/// 쓰지 않는다(Q-E8 — 이메일 일치는 소유 확인이 아니다).
|
||||
Future<SocialCredential?> obtainCredential();
|
||||
}
|
||||
|
||||
/// 자격증명 한 벌과, 판정에 쓸 그 계정의 이메일.
|
||||
/// 자격증명 한 벌과 그 계정의 이메일(제공자가 줄 때만).
|
||||
///
|
||||
/// [name] 은 제공자가 자격증명과 함께만 주는 표시 이름(Apple fullName — 최초 인가 때만). Firebase 가
|
||||
/// 이 값을 계정 이름으로 옮기지 않아, 로그인 직후 우리가 계정에 써 둔다(온보딩 닉네임 기본값).
|
||||
@@ -99,7 +100,7 @@ final class GoogleSignInProvider
|
||||
return AuthFailed('E-AUTH-03', error.description ?? error.code.name);
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed(
|
||||
authCatalogCode(error),
|
||||
authCatalogCode(error, flow: AuthFlow.signIn),
|
||||
authMessage(error, flow: AuthFlow.signIn),
|
||||
);
|
||||
}
|
||||
@@ -165,13 +166,12 @@ final class AppleSignInProvider
|
||||
accessToken: credential.authorizationCode,
|
||||
rawNonce: rawNonce,
|
||||
),
|
||||
// Apple 은 **최초 인가 때만** 이메일을 준다. 두 번째부터는 null 이라
|
||||
// 「동일 이메일만 연동」 판정을 할 수 없다 — 그때는 통과시킨다.
|
||||
// Apple 은 **최초 인가 때만** 이메일을 준다. 두 번째부터는 null 이다.
|
||||
email: credential.email,
|
||||
name: [credential.givenName, credential.familyName]
|
||||
.whereType<String>()
|
||||
.where((part) => part.isNotEmpty)
|
||||
.join(' '),
|
||||
name: [
|
||||
credential.givenName,
|
||||
credential.familyName,
|
||||
].whereType<String>().where((part) => part.isNotEmpty).join(' '),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,7 +192,9 @@ final class AppleSignInProvider
|
||||
obtained.credential,
|
||||
);
|
||||
final name = obtained.name;
|
||||
if (name != null && name.isNotEmpty && (result.user?.displayName ?? '').isEmpty) {
|
||||
if (name != null &&
|
||||
name.isNotEmpty &&
|
||||
(result.user?.displayName ?? '').isEmpty) {
|
||||
await result.user?.updateDisplayName(name);
|
||||
}
|
||||
return AuthSucceeded(_toAccount(result.user, provider));
|
||||
@@ -206,7 +208,7 @@ final class AppleSignInProvider
|
||||
return AuthFailed('E-AUTH-03', appL10n.authSocialFailed);
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed(
|
||||
authCatalogCode(error),
|
||||
authCatalogCode(error, flow: AuthFlow.signIn),
|
||||
authMessage(error, flow: AuthFlow.signIn),
|
||||
);
|
||||
}
|
||||
@@ -221,8 +223,9 @@ final class AppleSignInProvider
|
||||
kIsWeb || defaultTargetPlatform == TargetPlatform.android;
|
||||
}
|
||||
|
||||
/// LINE — Firebase Auth 의 OpenID Connect 제공자(`oidc.line`). 카카오도 이 경로였으나
|
||||
/// Android 앱 전환 문제로 [KakaoSignInProvider] 로 옮겼다(클래스는 제공자 무관이라 그대로 둔다).
|
||||
/// Firebase Auth 웹 흐름 제공자 — 지금은 Android 의 Apple(`apple.com`)만 쓴다. 카카오 · LINE 도 이
|
||||
/// 경로(`oidc.kakao` · `oidc.line`)였으나 앱 전환 · 「missing initial state」 문제로 공식 SDK + 서버 커스텀
|
||||
/// 토큰([KakaoSignInProvider] · [LineSignInProvider])으로 옮겼다(클래스는 제공자 무관이라 그대로 둔다).
|
||||
///
|
||||
/// 두 회사 모두 OIDC 표준(issuer `https://kauth.kakao.com` · `https://access.line.me`)을
|
||||
/// 지원해 Firebase 가 코드 교환·ID 토큰 검증까지 맡는다. 앱은 SDK 없이
|
||||
@@ -242,7 +245,6 @@ final class OidcSignInProvider implements SocialAuthProvider {
|
||||
this.provider, {
|
||||
required this.providerId,
|
||||
this.scopes = const [],
|
||||
this.androidParameters = const {},
|
||||
this.firebaseAuth,
|
||||
});
|
||||
|
||||
@@ -257,12 +259,6 @@ final class OidcSignInProvider implements SocialAuthProvider {
|
||||
/// KOE205 로 로그인 자체를 막는다.
|
||||
final List<String> scopes;
|
||||
|
||||
/// Android 에서만 인가 URL 에 붙일 파라미터. Android Custom Tab 은 다른 앱으로
|
||||
/// 넘어갔다 돌아오면 콜백이 일반 Chrome 탭에서 열려 Firebase 핸들러가
|
||||
/// 「missing initial state」로 끝난다 — 앱 전환을 막는 공식 파라미터를 여기 둔다.
|
||||
/// iOS 는 정상이라 건드리지 않는다.
|
||||
final Map<String, String> androidParameters;
|
||||
|
||||
/// 테스트 주입용. 기본은 [fb.FirebaseAuth.instance].
|
||||
final fb.FirebaseAuth? firebaseAuth;
|
||||
|
||||
@@ -278,9 +274,6 @@ final class OidcSignInProvider implements SocialAuthProvider {
|
||||
for (final scope in scopes) {
|
||||
p.addScope(scope);
|
||||
}
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||
p.setCustomParameters(androidParameters);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -289,16 +282,18 @@ final class OidcSignInProvider implements SocialAuthProvider {
|
||||
final missing = missingConfig;
|
||||
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
||||
try {
|
||||
final result = await untilAbandoned(
|
||||
(firebaseAuth ?? fb.FirebaseAuth.instance).signInWithProvider(
|
||||
authProvider,
|
||||
final result = await retryStaleWebContext(
|
||||
() => untilAbandoned(
|
||||
(firebaseAuth ?? fb.FirebaseAuth.instance).signInWithProvider(
|
||||
authProvider,
|
||||
),
|
||||
),
|
||||
);
|
||||
return AuthSucceeded(_toAccount(result.user, provider));
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
if (isWebFlowCancelled(error)) return const AuthCancelled();
|
||||
return AuthFailed(
|
||||
authCatalogCode(error),
|
||||
authCatalogCode(error, flow: AuthFlow.signIn),
|
||||
authMessage(error, flow: AuthFlow.signIn),
|
||||
);
|
||||
}
|
||||
@@ -389,7 +384,8 @@ final class KakaoSignInProvider implements SocialAuthProvider {
|
||||
return AuthSucceeded(_toAccount(result.user, provider));
|
||||
});
|
||||
|
||||
/// SNS 연동 — 로그인한 UID 에 카카오를 붙인다. 「동일 이메일만 연동」은 서버가 붙이기 전에 본다.
|
||||
/// SNS 연동 — 로그인한 UID 에 카카오를 붙인다. 현재 세션 · 카카오 토큰 검증과 다른 UID 연결 거부는
|
||||
/// 서버(`fnAuthKakao` link)가 한다. 이메일은 보지 않는다(Q-E8).
|
||||
Future<AuthOutcome> link(fb.User user) => _run(AuthFlow.link, () async {
|
||||
await _exchange({'accessToken': await _accessToken(), 'link': true});
|
||||
await user.reload();
|
||||
@@ -415,28 +411,20 @@ final class KakaoSignInProvider implements SocialAuthProvider {
|
||||
}
|
||||
// 카카오톡 왕복 뒤 인가 코드 · 토큰 단계 실패(KOE · misconfigured 등). 원인을 로그로만 남긴다 —
|
||||
// 10/6 릴리스 빌드 E-AUTH-03 은 이 분기로 들어와 아무 로그도 남지 않았다.
|
||||
debugPrint('[KakaoSignInProvider] auth failed: ${error.error} ${error.errorDescription}');
|
||||
debugPrint(
|
||||
'[KakaoSignInProvider] auth failed: ${error.error} ${error.errorDescription}',
|
||||
);
|
||||
return AuthFailed('E-AUTH-03', appL10n.authSocialFailed);
|
||||
} on kakao.KakaoException catch (error) {
|
||||
debugPrint('[KakaoSignInProvider] sdk error: $error');
|
||||
return AuthFailed('E-AUTH-03', appL10n.authSocialFailed);
|
||||
} on FirebaseFunctionsException catch (error) {
|
||||
final details = error.details;
|
||||
final code = details is Map && details['code'] is String
|
||||
? details['code'] as String
|
||||
: 'E-AUTH-03';
|
||||
return AuthFailed(code, switch (code) {
|
||||
_ when details is Map && details['reason'] == 'different-email' =>
|
||||
appL10n.authDifferentEmail,
|
||||
'E-AUTH-08' => appL10n.authCredentialInUse,
|
||||
'E-AUTH-09' => appL10n.authAlreadyLinked,
|
||||
_ =>
|
||||
flow == AuthFlow.link
|
||||
? appL10n.authLinkFailed
|
||||
: appL10n.authSocialFailed,
|
||||
});
|
||||
return _serverFailure(error, flow);
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed(authCatalogCode(error), authMessage(error, flow: flow));
|
||||
return AuthFailed(
|
||||
authCatalogCode(error, flow: flow),
|
||||
authMessage(error, flow: flow),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,10 +441,195 @@ final class KakaoSignInProvider implements SocialAuthProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// 커스텀 토큰 교환 서버(`fnAuthKakao` · `fnAuthLine`)의 거부를 카탈로그 코드 · 문구로 옮긴다.
|
||||
AuthFailed _serverFailure(FirebaseFunctionsException error, AuthFlow flow) {
|
||||
final details = error.details;
|
||||
final code = details is Map && details['code'] is String
|
||||
? details['code'] as String
|
||||
: 'E-AUTH-03';
|
||||
return AuthFailed(code, switch (code) {
|
||||
_ when details is Map && details['reason'] == 'session' =>
|
||||
appL10n.authRecentLogin,
|
||||
'E-AUTH-08' => appL10n.authCredentialInUse,
|
||||
'E-AUTH-09' => appL10n.authAlreadyLinked,
|
||||
_ =>
|
||||
flow == AuthFlow.link ? appL10n.authLinkFailed : appL10n.authSocialFailed,
|
||||
});
|
||||
}
|
||||
|
||||
/// LINE Login 채널 ID. 카카오 앱 키와 같은 `config/kakao.env` 를 `--dart-define-from-file` 로 넘긴다.
|
||||
const lineChannelId = String.fromEnvironment('FANUZU_LINE_CHANNEL_ID');
|
||||
|
||||
/// LINE 이 돌려준 토큰. ID 토큰은 `openid` 동의가 있을 때만 온다 — 서버는 있으면 ID 토큰으로 검증한다.
|
||||
typedef LineTokens = ({String? idToken, String accessToken});
|
||||
|
||||
/// LINE — 공식 SDK 로그인 + 서버 커스텀 토큰(`fnAuthLine`). [KakaoSignInProvider] 와 같은 구조다.
|
||||
///
|
||||
/// Firebase OIDC(`oidc.line`) 웹 흐름은 iOS 에서 첫 시도에 「missing initial state」로 깨진다(10/11
|
||||
/// iPhone 11 · iOS 26 — `disable_ios_auto_login` 을 붙여도 재현). SDK 가 LINE 앱(없으면 SDK 웹 화면)으로
|
||||
/// 로그인해 받은 토큰을 서버가 LINE API 로 검증하고 커스텀 토큰을 내준다.
|
||||
///
|
||||
/// 서버는 사용자 제공자 정보에 `oidc.line` 을 그대로 남긴다 — OIDC 시절 가입자는 같은 UID 로 들어오고,
|
||||
/// [FirebaseAccountLinker.linkedAccounts] 도 지금처럼 「연결됨」을 그린다.
|
||||
///
|
||||
/// 동의 항목: profile · openid · email. 이메일은 LINE 이메일 권한 승인 · 사용자 동의가 있을 때만 ID 토큰에
|
||||
/// 오고, 서버가 그때만 남긴다(C-143). 없어도 로그인 · 연결이 된다.
|
||||
final class LineSignInProvider implements SocialAuthProvider {
|
||||
LineSignInProvider({
|
||||
Future<LineTokens> Function()? login,
|
||||
Future<Map<String, dynamic>> Function(Map<String, dynamic> payload)?
|
||||
exchange,
|
||||
this.firebaseAuth,
|
||||
this.channelId = lineChannelId,
|
||||
}) : _login = login ?? _sdkLogin,
|
||||
_exchange = exchange ?? _callServer;
|
||||
|
||||
final Future<LineTokens> Function() _login;
|
||||
final Future<Map<String, dynamic>> Function(Map<String, dynamic>) _exchange;
|
||||
|
||||
/// 테스트 주입용. 기본은 [fb.FirebaseAuth.instance].
|
||||
final fb.FirebaseAuth? firebaseAuth;
|
||||
final String channelId;
|
||||
|
||||
static Future<void>? _setup;
|
||||
|
||||
/// 사용자 취소 — Android `CANCEL`(LineApiResponseCode) · iOS `3003`(LineSDKError userCancelled).
|
||||
static const _cancelCodes = {'CANCEL', '3003'};
|
||||
|
||||
@override
|
||||
SocialProvider get provider => SocialProvider.line;
|
||||
|
||||
@override
|
||||
bool get isLive => true;
|
||||
|
||||
@override
|
||||
List<String> get missingConfig => [
|
||||
if (!FirebaseBoot.isLive) ...FirebaseBoot.missingLabels,
|
||||
if (channelId.isEmpty) 'LINE 채널 ID',
|
||||
];
|
||||
|
||||
fb.FirebaseAuth get _auth => firebaseAuth ?? fb.FirebaseAuth.instance;
|
||||
|
||||
static Future<LineTokens> _sdkLogin() async {
|
||||
await (_setup ??= line.LineSDK.instance.setup(lineChannelId));
|
||||
final result = await line.LineSDK.instance.login(
|
||||
scopes: const ['profile', 'openid', 'email'],
|
||||
);
|
||||
return (
|
||||
idToken: result.accessToken.idTokenRaw,
|
||||
accessToken: result.accessToken.value,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> _callServer(
|
||||
Map<String, dynamic> payload,
|
||||
) async {
|
||||
final result = await FirebaseFunctions.instanceFor(
|
||||
region: Backend.region,
|
||||
).httpsCallable('fnAuthLine').call<Object?>(payload);
|
||||
final data = result.data;
|
||||
return data is Map ? Map<String, dynamic>.from(data) : const {};
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _tokens() async {
|
||||
final t = await _login();
|
||||
return {
|
||||
if (t.idToken != null) 'idToken': t.idToken,
|
||||
'accessToken': t.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthOutcome> signIn() => _run(AuthFlow.signIn, () async {
|
||||
final data = await _exchange(await _tokens());
|
||||
final result = await _auth.signInWithCustomToken(data['token'] as String);
|
||||
return AuthSucceeded(_toAccount(result.user, provider));
|
||||
});
|
||||
|
||||
/// SNS 연동 — 로그인한 UID 에 LINE 을 붙인다. 현재 세션 · LINE 토큰 검증과 다른 UID 연결 거부는
|
||||
/// 서버(`fnAuthLine` link)가 한다. 이메일은 보지 않는다(Q-E8).
|
||||
Future<AuthOutcome> link(fb.User user) => _run(AuthFlow.link, () async {
|
||||
await _exchange({...await _tokens(), 'link': true});
|
||||
await user.reload();
|
||||
return AuthSucceeded(_toAccount(_auth.currentUser ?? user, provider));
|
||||
});
|
||||
|
||||
Future<AuthOutcome> _run(
|
||||
AuthFlow flow,
|
||||
Future<AuthOutcome> Function() body,
|
||||
) async {
|
||||
final missing = missingConfig;
|
||||
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
|
||||
try {
|
||||
return await body();
|
||||
} on PlatformException catch (error) {
|
||||
if (_cancelCodes.contains(error.code)) return const AuthCancelled();
|
||||
debugPrint(
|
||||
'[LineSignInProvider] sdk failed: ${error.code} ${error.message}',
|
||||
);
|
||||
return AuthFailed(
|
||||
'E-AUTH-03',
|
||||
flow == AuthFlow.link
|
||||
? appL10n.authLinkFailed
|
||||
: appL10n.authSocialFailed,
|
||||
);
|
||||
} on FirebaseFunctionsException catch (error) {
|
||||
return _serverFailure(error, flow);
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed(
|
||||
authCatalogCode(error, flow: flow),
|
||||
authMessage(error, flow: flow),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
// SDK 를 한 번도 띄우지 않았으면 지울 LINE 토큰도 없다.
|
||||
if (_setup == null) return;
|
||||
try {
|
||||
await line.LineSDK.instance.logout();
|
||||
} on Object catch (error) {
|
||||
// 토큰이 이미 만료 · 폐기됐어도 로그아웃을 막을 이유가 없다.
|
||||
debugPrint('[LineSignInProvider] logout: ${error.runtimeType}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자가 인가 웹 화면을 닫았다. Android 는 `canceled`, iOS 는 `cancelled` 철자다.
|
||||
/// `user-cancelled` 는 동의 화면에서 거절한 경우다(웹 흐름).
|
||||
bool isWebFlowCancelled(fb.FirebaseAuthException error) =>
|
||||
error.code == 'web-context-canceled' ||
|
||||
error.code == 'web-context-cancelled';
|
||||
error.code == 'web-context-cancelled' ||
|
||||
error.code == 'user-cancelled';
|
||||
|
||||
const _authChannel = MethodChannel('kr.co.fanuzu.app/auth');
|
||||
|
||||
/// Android — 결과 없이 남은 Firebase 웹 인가 대기 작업을 취소로 끝낸다(MainActivity `cancelPendingWebSignIn`).
|
||||
///
|
||||
/// LINE 로그인 도중 본인 확인을 하러 LINE 앱으로 갔다 돌아오는 식으로 흐름이 끊기면, Firebase 는 대기 작업을
|
||||
/// 들고 있어 다음 시도를 `web-context-already-presented` 로 막는다(10/8 실기기 — 다시 눌러도 로그인이 안 됨).
|
||||
/// 대기 작업이 없으면 아무 일도 하지 않는다. iOS · 웹은 SDK 가 스스로 정리한다.
|
||||
Future<void> clearStaleWebSignIn() async {
|
||||
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) return;
|
||||
try {
|
||||
await _authChannel.invokeMethod<void>('cancelPendingWebSignIn');
|
||||
} on Object catch (error) {
|
||||
debugPrint('[Auth] clear stale web sign-in skipped: $error');
|
||||
}
|
||||
}
|
||||
|
||||
/// 웹 인가가 앞선 흐름에 막혔으면(`web-context-already-presented`) 남은 대기를 정리하고 **한 번만** 다시 연다.
|
||||
/// 1차 검수 R01 「취소 · 실패 후 정상 재시도 가능」.
|
||||
Future<T> retryStaleWebContext<T>(Future<T> Function() attempt) async {
|
||||
try {
|
||||
return await attempt();
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
if (error.code != 'web-context-already-presented') rethrow;
|
||||
await clearStaleWebSignIn();
|
||||
return attempt();
|
||||
}
|
||||
}
|
||||
|
||||
/// Android 에서 웹 인가 흐름이 결과 없이 버려졌으면 취소(`web-context-canceled`)로 끝낸다.
|
||||
///
|
||||
@@ -481,6 +654,8 @@ Future<T> untilAbandoned<T>(
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
timer = Timer(grace, () {
|
||||
if (done.isCompleted) return;
|
||||
// Firebase 쪽 대기 작업도 끝낸다 — 남기면 다음 시도가 web-context-already-presented 로 막힌다.
|
||||
unawaited(clearStaleWebSignIn());
|
||||
done.completeError(
|
||||
fb.FirebaseAuthException(code: 'web-context-canceled'),
|
||||
);
|
||||
@@ -569,8 +744,8 @@ final class EmailPasswordProvider implements EmailAuthProvider {
|
||||
return const AuthCancelled();
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed(
|
||||
authCatalogCode(error),
|
||||
authMessage(error, flow: AuthFlow.signIn),
|
||||
authCatalogCode(error, flow: AuthFlow.email),
|
||||
authMessage(error, flow: AuthFlow.email),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -583,8 +758,8 @@ final class EmailPasswordProvider implements EmailAuthProvider {
|
||||
return AuthSucceeded(_toAccount(result.user, provider));
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed(
|
||||
authCatalogCode(error),
|
||||
authMessage(error, flow: AuthFlow.signIn),
|
||||
authCatalogCode(error, flow: AuthFlow.email),
|
||||
authMessage(error, flow: AuthFlow.email),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -599,7 +774,14 @@ final class EmailPasswordProvider implements EmailAuthProvider {
|
||||
///
|
||||
/// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호
|
||||
/// 오류」를 갈라 주면 어느 이메일이 가입돼 있는지 훑을 수 있다.
|
||||
String authCatalogCode(fb.FirebaseAuthException error) => switch (error.code) {
|
||||
///
|
||||
/// 알 수 없는 코드는 경로별로 갈린다 — 이메일 폼([AuthFlow.email])은 E-AUTH-01, 소셜 로그인 · 연결은
|
||||
/// 소셜 실패 E-AUTH-03 이다. 예전에는 하나로 E-AUTH-01 이라 LINE 웹 로그인이 끊기면(`web-*`)
|
||||
/// 「이메일 또는 비밀번호를 확인해 주세요」가 떴다(10/8 실기기 · 1차 검수 R01).
|
||||
String authCatalogCode(
|
||||
fb.FirebaseAuthException error, {
|
||||
required AuthFlow flow,
|
||||
}) => switch (error.code) {
|
||||
'email-already-in-use' => 'E-AUTH-04',
|
||||
'weak-password' => 'E-AUTH-05',
|
||||
'invalid-email' => 'E-AUTH-06',
|
||||
@@ -613,8 +795,13 @@ String authCatalogCode(fb.FirebaseAuthException error) => switch (error.code) {
|
||||
'no-such-provider' => 'E-AUTH-10',
|
||||
// Firebase 콘솔에서 제공자를 켜지 않았을 때. 사유가 사용자가 아니라 설정에 있다.
|
||||
'operation-not-allowed' => 'E-AUTH-11',
|
||||
'requires-recent-login' => 'E-AUTH-12',
|
||||
_ => 'E-AUTH-01',
|
||||
// 현재 계정의 세션이 만료 · 폐기됐다(Q-E8 「세션 만료 · 인증 취소」). 다시 로그인해야 한다.
|
||||
'requires-recent-login' ||
|
||||
'user-token-expired' ||
|
||||
'invalid-user-token' ||
|
||||
'user-mismatch' ||
|
||||
'user-disabled' => 'E-AUTH-12',
|
||||
_ => flow == AuthFlow.email ? 'E-AUTH-01' : 'E-AUTH-03',
|
||||
};
|
||||
|
||||
/// 오류 문구가 갈리는 경로.
|
||||
@@ -623,9 +810,12 @@ String authCatalogCode(fb.FirebaseAuthException error) => switch (error.code) {
|
||||
/// 밝힌다. 기본값을 두지 않는 것은 의도한 것이다 — 새 호출부가 경로를 밝히지
|
||||
/// 않고 지나가면 다시 한쪽 문구가 양쪽에 나간다.
|
||||
enum AuthFlow {
|
||||
/// 로그인 화면에서 계정으로 들어가려는 경로(`signInWithCredential`).
|
||||
/// 소셜 로그인 경로(`signInWithCredential` · `signInWithProvider`).
|
||||
signIn,
|
||||
|
||||
/// 이메일 · 비밀번호 폼(QA 내부 경로). 알 수 없는 실패를 「이메일 또는 비밀번호」로 안내하는 유일한 경로다.
|
||||
email,
|
||||
|
||||
/// 이미 로그인한 UID 에 소셜 제공자를 붙이거나 떼는 경로
|
||||
/// (`linkWithCredential` · `unlink`) — 별첨 B v1.4.
|
||||
link,
|
||||
@@ -645,7 +835,8 @@ String authMessage(fb.FirebaseAuthException error, {required AuthFlow flow}) =>
|
||||
'invalid-email' => appL10n.authInvalidEmail,
|
||||
'too-many-requests' => appL10n.commonTryLater,
|
||||
'account-exists-with-different-credential' => switch (flow) {
|
||||
AuthFlow.signIn => _otherProviderSignInGuide(error.email),
|
||||
AuthFlow.signIn ||
|
||||
AuthFlow.email => _otherProviderSignInGuide(error.email),
|
||||
AuthFlow.link => appL10n.authCredentialInUse,
|
||||
},
|
||||
// 붙이려는 자격증명이 남의 UID 에 이미 달려 있다. 연결 경로에서만 난다.
|
||||
@@ -653,8 +844,20 @@ String authMessage(fb.FirebaseAuthException error, {required AuthFlow flow}) =>
|
||||
'provider-already-linked' => appL10n.authAlreadyLinked,
|
||||
'no-such-provider' => appL10n.authNotLinked,
|
||||
'operation-not-allowed' => appL10n.authMethodUnavailable,
|
||||
'requires-recent-login' => appL10n.authRecentLogin,
|
||||
_ => appL10n.authCheckCredentials,
|
||||
'requires-recent-login' ||
|
||||
'user-token-expired' ||
|
||||
'invalid-user-token' ||
|
||||
'user-mismatch' ||
|
||||
'user-disabled' => appL10n.authRecentLogin,
|
||||
// 웹 인가 흐름이 끊기거나 망이 끊겼다 — 소셜 경로의 사실대로 알리고 다시 시도하게 한다(R01).
|
||||
'web-context-already-presented' => appL10n.authSocialCancelled,
|
||||
'web-network-request-failed' ||
|
||||
'network-request-failed' => appL10n.authSocialNetwork,
|
||||
_ => switch (flow) {
|
||||
AuthFlow.email => appL10n.authCheckCredentials,
|
||||
AuthFlow.signIn => appL10n.authSocialFailed,
|
||||
AuthFlow.link => appL10n.authLinkFailed,
|
||||
},
|
||||
};
|
||||
|
||||
/// 「먼저 쓰던 수단으로 로그인한 뒤 SNS 연동에서 붙이라」는 안내.
|
||||
|
||||
@@ -14,9 +14,9 @@ import '../../core/ui/bottom_inset.dart';
|
||||
/// 별첨 B v1.4 — *하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다.*
|
||||
/// X(트위터)는 확정 4종에 없어 목록에서 빠졌다([linkableProviders]).
|
||||
///
|
||||
/// 명세 `554:22797` — *플랫폼별 연동 상태 반영 · **동일 이메일만 연동 가능***
|
||||
/// 판정은 [sameAccountIdentity] 가 하고, 실제 연결은 [AccountLinker] 구현체
|
||||
/// (Firebase `linkWithCredential`)가 한다.
|
||||
/// 명세 `554:22797` — *플랫폼별 연동 상태 반영*. 명세의 「동일 이메일만 연동 가능」은 통합검토회신
|
||||
/// v2.3 **Q-E8** 이 대체했다(이메일 일치는 소유 확인이 아니다). 연결 검증은 [AccountLinker] 구현체
|
||||
/// (Firebase `linkWithCredential` · 카카오 `fnAuthKakao`)가 서버에서 한다.
|
||||
class AccountLinkScreen extends StatefulWidget {
|
||||
const AccountLinkScreen({
|
||||
super.key,
|
||||
@@ -68,8 +68,6 @@ class _AccountLinkScreenState extends State<AccountLinkScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpace.s4),
|
||||
Text(context.l10n.linkSameEmailOnly, style: t.labelSmall),
|
||||
const SizedBox(height: AppSpace.s5),
|
||||
FilledButton(
|
||||
onPressed: _busy ? null : _toggle,
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'dart:typed_data';
|
||||
|
||||
import '../../core/data/backend.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../quest/location_consent.dart';
|
||||
|
||||
/// POP 종류 — 정책 원장 **B-01**.
|
||||
///
|
||||
@@ -775,6 +776,9 @@ void checkProfilePhoto(PickedPhoto photo) {
|
||||
}
|
||||
|
||||
abstract interface class MyRepository {
|
||||
/// 위치기반 서비스 약관 동의 상태 — 설정의 동의 · 철회 토글(Q-E1 · R02).
|
||||
LocationConsent get locationConsent;
|
||||
|
||||
Future<MyProfile> profile();
|
||||
|
||||
/// 명세 — *디폴트 최신순으로 20개 노출*.
|
||||
@@ -836,6 +840,9 @@ abstract interface class MyRepository {
|
||||
class SampleMyRepository implements MyRepository {
|
||||
SampleMyRepository();
|
||||
|
||||
@override
|
||||
LocationConsent get locationConsent => SampleLocationConsent.shared;
|
||||
|
||||
@override
|
||||
Future<void> withdraw() async {}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:firebase_storage/firebase_storage.dart';
|
||||
import '../../core/data/backend.dart';
|
||||
import '../home/home_repository_firestore.dart' show kstDayKey;
|
||||
import 'my_data.dart';
|
||||
import '../quest/location_consent.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
|
||||
/// MY 의 실 서버 구현 — 화면 대조 원장 F6.
|
||||
@@ -42,6 +43,9 @@ import '../../core/i18n/app_locale.dart';
|
||||
class FirestoreMyRepository extends SampleMyRepository {
|
||||
FirestoreMyRepository(this._backend);
|
||||
|
||||
@override
|
||||
late final LocationConsent locationConsent = FirestoreLocationConsent(_backend);
|
||||
|
||||
final Backend _backend;
|
||||
|
||||
@override
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../core/ui/app_icons.dart';
|
||||
import '../auth/auth.dart';
|
||||
import '../auth/auth_repository.dart';
|
||||
import '../home/home_screen.dart' show formatPop;
|
||||
import '../quest/location_consent.dart';
|
||||
import 'my_data.dart';
|
||||
import 'notice_screens.dart';
|
||||
import 'payment_screens.dart';
|
||||
@@ -24,7 +25,7 @@ import '../../core/ui/bottom_inset.dart';
|
||||
/// 설정 — 화면 대조 원장 F6 · Figma `354:222008`.
|
||||
///
|
||||
/// 명세 `554:22634` — 계정 정보(닉네임 · 가입 이메일)
|
||||
/// 명세 `554:22797` — SNS 연동. *동일 이메일만 연동 가능*. 행의 「연결」에서 바로 제공자
|
||||
/// 명세 `554:22797` — SNS 연동. 행의 「연결」에서 바로 제공자
|
||||
/// 시트를 띄운다(시안 F6-05~07 · 대표 결정 — 별도 연동 화면을 거치지 않는다).
|
||||
/// 명세 `554:22831` — 푸시 알림 · 차단 · 언어
|
||||
/// 명세 `554:22857` — 공지사항 · 1:1 문의접수 · 앱 버전
|
||||
@@ -65,6 +66,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Map<AuthProviderKind, String?> _linked = const {};
|
||||
({String current, bool latest})? _version;
|
||||
|
||||
/// 위치기반 서비스 약관 동의 상태(Q-E1 · R02). 못 읽었으면 null — 토글을 그리지 않는다.
|
||||
bool? _locationConsent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -74,8 +78,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Future<void> _load() async {
|
||||
final profile = await widget.repository.profile();
|
||||
final version = await widget.repository.appVersion();
|
||||
bool? location;
|
||||
try {
|
||||
location = await widget.repository.locationConsent.granted();
|
||||
} catch (_) {
|
||||
location = null;
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_locationConsent = location;
|
||||
_profile = profile;
|
||||
_linked = widget.linker.linkedAccounts;
|
||||
_version = version;
|
||||
@@ -166,6 +177,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
down: true,
|
||||
onTap: _pickLanguage,
|
||||
),
|
||||
if (_locationConsent case final on?)
|
||||
_SwitchRow(
|
||||
label: l10n.settingsLocationConsent,
|
||||
value: on,
|
||||
onChanged: _setLocationConsent,
|
||||
),
|
||||
],
|
||||
),
|
||||
MyGroup(
|
||||
@@ -254,7 +271,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
/// 설정 행에서 바로 연결 · 해제한다(시안 F6-05~07 — 설정 위에 OS 제공자 시트가 뜬다).
|
||||
///
|
||||
/// 연결은 제공자 시트로 곧장 가고, 해제는 확인 팝업(시안 `554:23257`)을 거친다.
|
||||
/// 이메일이 다르면(E-AUTH-08 · [sameAccountIdentity]) 시안 `554:23103` 안내 팝업.
|
||||
/// 그 소셜 계정이 다른 FANUZU 계정에 이미 붙어 있으면(E-AUTH-08) 시안 `554:23103` 안내 팝업.
|
||||
/// 명세의 「동일 이메일만 연동 가능」은 통합검토회신 v2.3 **Q-E8** 이 대체했다 — 이메일은 소유 확인이 아니다.
|
||||
Future<void> _toggleLink(AuthProviderKind provider) async {
|
||||
final connected = _linked.containsKey(provider);
|
||||
if (connected) {
|
||||
@@ -274,8 +292,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// 로컬 값을 뒤집으면 서버가 거절한 연결이 붙은 것처럼 보인다.
|
||||
setState(() => _linked = widget.linker.linkedAccounts);
|
||||
switch (outcome) {
|
||||
case AuthFailed(code: 'E-AUTH-08'):
|
||||
await _notice(context.l10n.settingsSameEmailOnly);
|
||||
case AuthFailed(code: 'E-AUTH-08', :final message):
|
||||
await _notice(message);
|
||||
case AuthFailed(:final code, :final message):
|
||||
_toast('[$code] $message');
|
||||
case AuthNotConfigured():
|
||||
@@ -335,6 +353,24 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await LocaleScope.maybeOf(context)?.select(picked);
|
||||
}
|
||||
|
||||
/// 위치기반 서비스 약관 동의 · 철회(Q-E1 · R02). 켜면 동의 창을 거쳐 기록하고, 끄면 철회 행을 남긴다.
|
||||
/// Figma 설정 시안에 없는 줄이다 — 동의를 철회할 수단이 있어야 해 언어 아래에 토글 하나를 둔다.
|
||||
Future<void> _setLocationConsent(bool on) async {
|
||||
final consent = widget.repository.locationConsent;
|
||||
var now = on;
|
||||
if (on) {
|
||||
now = await ensureLocationConsent(context, consent);
|
||||
} else {
|
||||
try {
|
||||
await consent.record(false);
|
||||
} catch (_) {
|
||||
now = true;
|
||||
if (mounted) _toast(context.l10n.commonRequestFailed);
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() => _locationConsent = now);
|
||||
}
|
||||
|
||||
/// 서비스 탈퇴 — 명세 `554:24885` 「연동 상태값이 있을 경우 비정상 탈퇴 처리 · 게시물 보존 ·
|
||||
/// 탈퇴 처리 후 앱 종료」. 연동이 남아 있으면 비정상 탈퇴 안내(`554:24908` · `374:14699`)만 띄운다.
|
||||
/// 서버도 같은 조건을 다시 본다(E-AUTH-14).
|
||||
@@ -400,6 +436,41 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 켜고 끄는 줄 — 푸시 알림 설정 화면의 토글과 같은 모양.
|
||||
class _SwitchRow extends StatelessWidget {
|
||||
const _SwitchRow({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
height: 24,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(label, style: myText(16, FontWeight.w600, height: 1)),
|
||||
),
|
||||
Switch(
|
||||
value: value,
|
||||
activeThumbColor: AppColors.onCosmic,
|
||||
activeTrackColor: AppColors.cosmicAccent,
|
||||
inactiveThumbColor: const Color(0xFF3A3A3A),
|
||||
inactiveTrackColor: const Color(0xFF2C2C2C),
|
||||
trackOutlineColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 「닉네임 · BTS_Hong」 — 둘 다 16/600, 값은 #bbbbbb.
|
||||
class _ValueRow extends StatelessWidget {
|
||||
const _ValueRow({required this.label, required this.value});
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/// 위치기반 서비스 약관 — 위치기능 **최초 사용 직전**에 받는 동의.
|
||||
///
|
||||
/// 통합검토회신 v2.3 **Q-E1** · 1차 검수 회신서 **R02** — 가입 때 위치기반 약관을
|
||||
/// 일괄 필수로 받지 않는다. 위치 약관 동의와 OS 위치권한은 위치기능을 처음 쓰기 전에 **각각** 처리하고,
|
||||
/// 거부해도 일반 기능은 막지 않는다. 그래서 이 동의는 위치 Quest 인증 · 장소 선택 지도의 현재 위치처럼
|
||||
/// 기기 위치를 쓰는 지점에서만 묻고, OS 권한은 그 뒤 [devicePlaceFix] 가 따로 요청한다.
|
||||
///
|
||||
/// 기록은 서버 `fnConsentRecord`(`users/{uid}/consents` append-only 행 + `consentState.location`)가 하고,
|
||||
/// 위치 Quest 인증(`fnQuestVerifyPlace`)도 같은 상태를 다시 본다. 철회는 설정 화면의 토글이다.
|
||||
///
|
||||
/// Figma 에는 이 시점의 동의 화면이 없다(`179:24230` 은 가입 약관). 앱 공통 확인 창([showConfirmDialog])
|
||||
/// 틀에 약관 칸을 넣어 쓴다.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/data/backend.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../../core/ui/app_dialog.dart';
|
||||
|
||||
/// 동의 기록에 남는 위치기반 약관 버전. 🟡 임시 — 발주사 확정 문안(R02) 수령 전 자리 표시 문구다.
|
||||
const locationTermsVersion = 'draft-1';
|
||||
|
||||
/// 위치기반 서비스 동의 상태 · 기록 경계.
|
||||
abstract interface class LocationConsent {
|
||||
Future<bool> granted();
|
||||
|
||||
/// 동의(true) · 철회(false)를 기록한다. 철회도 새 행으로 남는다(서버 append-only).
|
||||
Future<void> record(bool granted);
|
||||
}
|
||||
|
||||
class FirestoreLocationConsent implements LocationConsent {
|
||||
FirestoreLocationConsent(this._backend);
|
||||
|
||||
final Backend _backend;
|
||||
|
||||
/// 서버 `hasLocationConsent` 와 같은 판정 — `consentState.location` 이 없으면 예전 가입 화면
|
||||
/// (위치 약관이 가입 필수였던 0.2.0 이전)의 동의 행으로 본다.
|
||||
@override
|
||||
Future<bool> granted() async {
|
||||
final uid = _backend.requireUid;
|
||||
final user =
|
||||
(await _backend.db.doc('users/$uid').get()).data() ??
|
||||
const <String, dynamic>{};
|
||||
final state = user['consentState'];
|
||||
if (state is Map && state['location'] is Map) {
|
||||
return (state['location'] as Map)['granted'] == true;
|
||||
}
|
||||
final version = user['termsVersion'];
|
||||
if (version is! String || version.isEmpty) return false;
|
||||
return (await _backend.db
|
||||
.doc('users/$uid/consents/location_${version}_grant')
|
||||
.get())
|
||||
.exists;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> record(bool granted) => _backend.call('fnConsentRecord', {
|
||||
'docType': 'location',
|
||||
'docVersion': locationTermsVersion,
|
||||
'granted': granted,
|
||||
});
|
||||
}
|
||||
|
||||
/// 목 모드 · 테스트용 — 앱 안에서 하나를 같이 쓴다(퀘스트 · 설정 화면이 같은 상태를 본다).
|
||||
class SampleLocationConsent implements LocationConsent {
|
||||
SampleLocationConsent([this.value = false]);
|
||||
|
||||
static final shared = SampleLocationConsent();
|
||||
|
||||
bool value;
|
||||
final records = <bool>[];
|
||||
|
||||
@override
|
||||
Future<bool> granted() async => value;
|
||||
|
||||
@override
|
||||
Future<void> record(bool granted) async {
|
||||
records.add(granted);
|
||||
value = granted;
|
||||
}
|
||||
}
|
||||
|
||||
/// 위치기능을 쓰기 직전에 부른다. 이미 동의했으면 묻지 않고 true, 처음이면 동의 창을 띄워
|
||||
/// 동의를 기록한 뒤 true. 「나중에」 · 기록 실패면 false — 호출부는 그 위치기능만 건너뛴다.
|
||||
Future<bool> ensureLocationConsent(
|
||||
BuildContext context,
|
||||
LocationConsent consent,
|
||||
) async {
|
||||
try {
|
||||
if (await consent.granted()) return true;
|
||||
} catch (_) {
|
||||
// 상태를 못 읽었으면 묻는다 — 동의는 다시 기록해도 행이 하나 더 남을 뿐이다.
|
||||
}
|
||||
if (!context.mounted) return false;
|
||||
final agreed = await showLocationConsentDialog(context);
|
||||
if (!agreed) return false;
|
||||
try {
|
||||
await consent.record(true);
|
||||
return true;
|
||||
} catch (_) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.commonRequestFailed),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 동의 창 — 제목 · 사용 목적 · 약관 본문 칸 · 「나중에 / 동의」. 동의면 true.
|
||||
Future<bool> showLocationConsentDialog(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return showConfirmDialog(
|
||||
context,
|
||||
l10n.termsLocation,
|
||||
body: l10n.locationConsentBody,
|
||||
content: Container(
|
||||
key: const ValueKey('location-terms-body'),
|
||||
constraints: const BoxConstraints(maxHeight: 120),
|
||||
padding: const EdgeInsets.all(AppSpace.s3),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFF545454)),
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
l10n.termsLocationBody,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.display,
|
||||
fontFamilyFallback: [AppFonts.body],
|
||||
fontSize: 12,
|
||||
height: 1.36,
|
||||
color: AppColors.onCosmic,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
cancelLabel: l10n.locationConsentLater,
|
||||
confirmLabel: l10n.locationConsentAgree,
|
||||
);
|
||||
}
|
||||
@@ -26,8 +26,13 @@ class PlacePickerScreen extends StatefulWidget {
|
||||
super.key,
|
||||
this.mapBuilder = _googleMap,
|
||||
this.lastKnown = _deviceLastKnown,
|
||||
this.useDeviceLocation = true,
|
||||
});
|
||||
|
||||
/// 기기 위치(마지막 위치 · 현재 위치)를 쓸지. 위치기반 서비스 약관에 동의하지 않았으면 false 다 —
|
||||
/// 지도에서 직접 고르는 것은 그대로 된다(Q-E1 · R02 「거부해도 일반 기능 이용 가능」).
|
||||
final bool useDeviceLocation;
|
||||
|
||||
/// 지도 위젯 — 위젯 테스트는 플랫폼 뷰가 없어 가짜로 바꾼다.
|
||||
final PickerMapBuilder mapBuilder;
|
||||
|
||||
@@ -68,6 +73,10 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!widget.useDeviceLocation) {
|
||||
_start = PlacePickerScreen.fallbackCenter;
|
||||
return;
|
||||
}
|
||||
_startFromLastKnown();
|
||||
_locate();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../core/data/server_clock.dart';
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../home/home_screen.dart' show formatPop;
|
||||
import 'place_picker_screen.dart';
|
||||
import 'location_consent.dart';
|
||||
import 'quest_data.dart';
|
||||
import 'widgets/quest_card_tile.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
@@ -441,9 +442,13 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
|
||||
final _extraVerify = <String>{};
|
||||
|
||||
Future<void> _pickPlace() async {
|
||||
// 지도의 현재 위치는 위치기능이다 — 위치기반 약관에 먼저 동의받는다(Q-E1 · R02). 거부해도 지도에서
|
||||
// 직접 고를 수 있다.
|
||||
final located = await ensureLocationConsent(context, widget.repository.locationConsent);
|
||||
if (!mounted) return;
|
||||
final picked = await Navigator.of(context, rootNavigator: true).push<QuestPlace>(
|
||||
// 명세 `523:18207` — 선택 완료 시 *하단 move out 페이지 종료 후 이전 화면 이동*.
|
||||
SheetRoute(builder: (_) => const PlacePickerScreen()),
|
||||
SheetRoute(builder: (_) => PlacePickerScreen(useDeviceLocation: located)),
|
||||
);
|
||||
if (picked == null || !mounted) return;
|
||||
setState(() => _place = picked);
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../core/data/backend.dart' show BackendException;
|
||||
import '../../core/monetization/rewarded_ad.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import 'place_picker_screen.dart' show PlacePickerScreen;
|
||||
import 'location_consent.dart';
|
||||
import 'quest_evidence.dart' show ShakeJudge, ShakeWindows;
|
||||
|
||||
/// 진행 상태 — 명세가 **다섯 가지**를 못박았다.
|
||||
@@ -684,6 +685,9 @@ class QuestDraft {
|
||||
}
|
||||
|
||||
abstract interface class QuestRepository {
|
||||
/// 위치기반 서비스 약관 동의(Q-E1 · R02) — 위치 Quest 인증 · 장소 선택의 현재 위치 직전에 확인한다.
|
||||
LocationConsent get locationConsent;
|
||||
|
||||
/// 명세 — *퀘스트 생성 최신순 소팅(최초 10개 노출, 스크롤 시 10개씩 로드)*.
|
||||
///
|
||||
/// [limit] 장보다 적게 돌려주면 더 읽을 문서가 없다는 뜻이다 — 화면은 이것으로 끝을 판정한다.
|
||||
@@ -798,6 +802,9 @@ abstract interface class QuestRepository {
|
||||
class SampleQuestRepository implements QuestRepository {
|
||||
const SampleQuestRepository();
|
||||
|
||||
@override
|
||||
LocationConsent get locationConsent => SampleLocationConsent.shared;
|
||||
|
||||
/// 원장 C-01 매핑 중 사용자 생성 초기값(C-02 A안)으로 열린 활동만.
|
||||
static QuestCatalog get _catalog => QuestCatalog(
|
||||
categories: [
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../planet/moderation.dart';
|
||||
import 'music_shake_play_screen.dart';
|
||||
import 'quest_ad_screens.dart';
|
||||
import 'quest_create_screen.dart' show QuestLinkCard;
|
||||
import 'location_consent.dart';
|
||||
import 'quest_data.dart';
|
||||
import 'quest_evidence.dart';
|
||||
import 'qr_scan_screen.dart';
|
||||
@@ -1025,6 +1026,13 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
// 위치 Quest 의 추가 인증(C-03) — 고른 수단의 증거를 먼저 모은다. 취소하면 아무것도 보내지 않는다.
|
||||
var proof = const PlaceProof();
|
||||
if (detail.card.type == QuestType.place && detail.card.endsAt == null) {
|
||||
// 위치기반 서비스 약관 — 위치기능 최초 사용 직전(Q-E1 · R02). 거부하면 이 인증만 하지 않는다.
|
||||
// OS 위치권한은 그 뒤 좌표를 읽을 때 따로 묻는다.
|
||||
if (!await ensureLocationConsent(context, widget.repository.locationConsent)) {
|
||||
if (mounted) _toast(context.l10n.locationConsentNeeded);
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
final collected = await _placeProof(detail.verifyMethods);
|
||||
if (collected == null || !mounted) return;
|
||||
proof = collected;
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:firebase_storage/firebase_storage.dart' show SettableMetadata;
|
||||
import '../../core/data/backend.dart';
|
||||
import '../../core/data/server_clock.dart';
|
||||
import '../../core/monetization/rewarded_ad.dart';
|
||||
import 'location_consent.dart';
|
||||
import 'quest_data.dart';
|
||||
import 'quest_evidence.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
@@ -40,6 +41,10 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
_baseline = baseline ?? WalkBaseline();
|
||||
|
||||
final Backend _backend;
|
||||
|
||||
@override
|
||||
late final LocationConsent locationConsent = FirestoreLocationConsent(_backend);
|
||||
|
||||
final StepCountProbe _stepCount;
|
||||
final TodayStepsProbe _todaySteps;
|
||||
final PlaceProbe _placeFix;
|
||||
|
||||
+16
-6
@@ -27,7 +27,7 @@
|
||||
"termsTitle": "Terms of use",
|
||||
"termsAgreeAll": "Agree to all",
|
||||
"termsService": "Terms of Service (required)",
|
||||
"termsLocation": "Location-based Service Terms (required)",
|
||||
"termsLocation": "Location-based Service Terms",
|
||||
"termsAge": "I am 14 or older. (required)",
|
||||
"termsMarketing": "Receive marketing and event notifications (optional)",
|
||||
"signupDoneTitle": "You're all signed up",
|
||||
@@ -76,7 +76,6 @@
|
||||
"settingsUnlinkConfirm": "Unlink this account?",
|
||||
"settingsUnlinkBody": "Only this social login method is removed. Your account data is not deleted.",
|
||||
"settingsUnlink": "Unlink",
|
||||
"settingsSameEmailOnly": "Only social accounts with the same\nemail as your account can be linked.",
|
||||
"settingsLinkNotReady": "{name} linking isn't available on this device yet.",
|
||||
"settingsConnect": "Link",
|
||||
"settingsDisconnect": "Unlink",
|
||||
@@ -98,7 +97,6 @@
|
||||
"linkTitle": "{name} link",
|
||||
"linkNotLinked": "Not linked",
|
||||
"linkLinked": "Linked",
|
||||
"linkSameEmailOnly": "Only social accounts with the same email as your account can be linked.",
|
||||
"linkUnlink": "Unlink",
|
||||
"linkLink": "Link",
|
||||
"noticeBadge": "Notice",
|
||||
@@ -157,7 +155,6 @@
|
||||
"authProviderName": "{provider, select, email{Email} kakao{Kakao} line{LINE} apple{Apple} other{Google}}",
|
||||
"authLastProvider": "You can't unlink your last sign-in method.",
|
||||
"authLoginRequired": "Please sign in.",
|
||||
"authDifferentEmail": "This account doesn't match your sign-up email.",
|
||||
"authLinkFailed": "Couldn't link the account.",
|
||||
"authNotLinked": "This account isn't linked.",
|
||||
"authEmailUnavailable": "Email sign-in is unavailable.",
|
||||
@@ -854,7 +851,7 @@
|
||||
"popReasonQuestFeeRefund": "Quest fee refund",
|
||||
"questGoalSteps": "Step goal",
|
||||
"eclipseTapToJoin": "Can't shake? Tap to join",
|
||||
"termsPrivacy": "Privacy Policy and Personal Data Collection (required)",
|
||||
"termsPrivacy": "Consent to Collection and Use of Personal Data (required)",
|
||||
"onbSkip": "Skip",
|
||||
"onbIntro1Title": "Grow your fandom's planet",
|
||||
"onbIntro1Body": "Collect POP and send it with ZAP\nto grow your fandom planet together.",
|
||||
@@ -908,5 +905,18 @@
|
||||
"liveEclipseDefending": "[LIVE] {count} fans defending together now",
|
||||
"liveHeroTime": "[LIVE] HERO TIME on planet {fandom}",
|
||||
"liveHeroTimeBy": "[LIVE] HERO TIME on planet {fandom} · by {name}",
|
||||
"questReflected": "Quest completion counted toward {fandom}'s growth"
|
||||
"questReflected": "Quest completion counted toward {fandom}'s growth",
|
||||
"termsPrivacyPolicy": "Privacy Policy",
|
||||
"termsPrivacyPolicyNotice": "See the Privacy Policy for how your personal data is processed.",
|
||||
"termsPrivacyBody": "FANUZU Consent to Collection and Use of Personal Data\n\nThis is temporary placeholder text until the final text is provided.\n\nData collected\nTo be filled in when the final text is provided.\n\nPurpose of use\nTo be filled in when the final text is provided.\n\nRetention period\nTo be filled in when the final text is provided.\n\nRight to refuse consent and consequences of refusal\nTo be filled in when the final text is provided.",
|
||||
"termsMarketingBody": "FANUZU Consent to Receive Marketing Information\n\nThis is temporary placeholder text until the final text is provided.\n\nWhat you will receive\nTo be filled in when the final text is provided.\n\nHow you will receive it\nTo be filled in when the final text is provided.\n\nRetention period\nTo be filled in when the final text is provided.\n\nHow to withdraw consent\nTo be filled in when the final text is provided.",
|
||||
"termsPrivacyPolicyBody": "FANUZU Privacy Policy\n\nThis is temporary placeholder text until the final text is provided.\n\n1. Personal data collected\nTo be filled in when the final text is provided.\n\n2. Purpose of use\nTo be filled in when the final text is provided.\n\n3. Retention period\nTo be filled in when the final text is provided.\n\n4. Processing outsourcing and overseas transfer\nTo be filled in when the final text is provided.\n\n5. Destruction of personal data\nTo be filled in when the final text is provided.\n\n6. Your rights and how to exercise them\nTo be filled in when the final text is provided.\n\n7. Privacy officer and contact\nTo be filled in when the final text is provided.\n\n8. Effective date\nTo be filled in when the final text is provided.",
|
||||
"termsLocationBody": "FANUZU Location-based Service Terms\n\nThis is temporary placeholder text until the final text is provided.\n\nPurpose of use\nTo be filled in when the final text is provided.\n\nLocation data collected and used\nTo be filled in when the final text is provided.\n\nRetention period\nTo be filled in when the final text is provided.\n\nLocation-based service provider information\nTo be filled in when the final text is provided.\n\nEffective date\nTo be filled in when the final text is provided.",
|
||||
"locationConsentBody": "Your device location is used for location-verified quests and to show where you are on the map. You can keep using other features without agreeing.",
|
||||
"locationConsentAgree": "Agree",
|
||||
"locationConsentLater": "Not now",
|
||||
"locationConsentNeeded": "Agree to the Location-based Service Terms to verify your location.",
|
||||
"settingsLocationConsent": "Location-based service consent",
|
||||
"authSocialCancelled": "Sign-in wasn't completed. Please try again.",
|
||||
"authSocialNetwork": "Check your network connection and try again."
|
||||
}
|
||||
|
||||
+16
-6
@@ -27,7 +27,7 @@
|
||||
"termsTitle": "Términos de uso",
|
||||
"termsAgreeAll": "Aceptar todo",
|
||||
"termsService": "Términos del servicio (obligatorio)",
|
||||
"termsLocation": "Términos de servicios de ubicación (obligatorio)",
|
||||
"termsLocation": "Términos del servicio basado en ubicación",
|
||||
"termsAge": "Tengo 14 años o más. (obligatorio)",
|
||||
"termsMarketing": "Recibir información de marketing y eventos (opcional)",
|
||||
"signupDoneTitle": "Registro completado",
|
||||
@@ -76,7 +76,6 @@
|
||||
"settingsUnlinkConfirm": "¿Desvincular esta cuenta?",
|
||||
"settingsUnlinkBody": "Solo se quita este método de inicio social; los datos de tu cuenta no se eliminan.",
|
||||
"settingsUnlink": "Desvincular",
|
||||
"settingsSameEmailOnly": "Solo se pueden vincular cuentas sociales\ncon el mismo correo.",
|
||||
"settingsLinkNotReady": "La vinculación con {name} aún no está disponible en este dispositivo.",
|
||||
"settingsConnect": "Vincular",
|
||||
"settingsDisconnect": "Desvincular",
|
||||
@@ -98,7 +97,6 @@
|
||||
"linkTitle": "Vincular {name}",
|
||||
"linkNotLinked": "No vinculada",
|
||||
"linkLinked": "Vinculada",
|
||||
"linkSameEmailOnly": "Solo se pueden vincular cuentas sociales con el mismo correo.",
|
||||
"linkUnlink": "Desvincular",
|
||||
"linkLink": "Vincular",
|
||||
"noticeBadge": "Aviso",
|
||||
@@ -157,7 +155,6 @@
|
||||
"authProviderName": "{provider, select, email{Correo} kakao{Kakao} line{LINE} apple{Apple} other{Google}}",
|
||||
"authLastProvider": "No puedes desvincular tu último método de inicio de sesión.",
|
||||
"authLoginRequired": "Debes iniciar sesión.",
|
||||
"authDifferentEmail": "Esta cuenta no coincide con tu correo de registro.",
|
||||
"authLinkFailed": "No se pudo vincular la cuenta.",
|
||||
"authNotLinked": "Esta cuenta no está vinculada.",
|
||||
"authEmailUnavailable": "El inicio de sesión con correo no está disponible.",
|
||||
@@ -854,7 +851,7 @@
|
||||
"popReasonQuestFeeRefund": "Reembolso de comisión de misión",
|
||||
"questGoalSteps": "Meta de pasos",
|
||||
"eclipseTapToJoin": "¿No puedes agitar? Toca para participar",
|
||||
"termsPrivacy": "Política de privacidad y recopilación de datos personales (obligatorio)",
|
||||
"termsPrivacy": "Consentimiento para la recopilación y uso de datos personales (obligatorio)",
|
||||
"onbSkip": "Omitir",
|
||||
"onbIntro1Title": "Haz crecer el planeta de tu fandom",
|
||||
"onbIntro1Body": "Reúne POP y envíalo con ZAP\npara que el planeta de tu fandom crezca.",
|
||||
@@ -908,5 +905,18 @@
|
||||
"liveEclipseDefending": "[LIVE] {count} fans defendiendo juntos ahora",
|
||||
"liveHeroTime": "[LIVE] HERO TIME en el planeta {fandom}",
|
||||
"liveHeroTimeBy": "[LIVE] HERO TIME en el planeta {fandom} · by {name}",
|
||||
"questReflected": "Tu misión completada se reflejó en el crecimiento de {fandom}"
|
||||
"questReflected": "Tu misión completada se reflejó en el crecimiento de {fandom}",
|
||||
"termsPrivacyPolicy": "Política de privacidad",
|
||||
"termsPrivacyPolicyNotice": "Consulta la Política de privacidad para saber cómo se tratan tus datos personales.",
|
||||
"termsPrivacyBody": "FANUZU Consentimiento para la recopilación y el uso de datos personales\n\nTexto provisional hasta recibir el texto definitivo.\n\nDatos recopilados\nSe completará al recibir el texto definitivo.\n\nFinalidad del uso\nSe completará al recibir el texto definitivo.\n\nPlazo de conservación\nSe completará al recibir el texto definitivo.\n\nDerecho a negarse y consecuencias de la negativa\nSe completará al recibir el texto definitivo.",
|
||||
"termsMarketingBody": "FANUZU Consentimiento para recibir información de marketing\n\nTexto provisional hasta recibir el texto definitivo.\n\nContenido que recibirás\nSe completará al recibir el texto definitivo.\n\nMedio de envío\nSe completará al recibir el texto definitivo.\n\nPlazo de conservación\nSe completará al recibir el texto definitivo.\n\nCómo retirar el consentimiento\nSe completará al recibir el texto definitivo.",
|
||||
"termsPrivacyPolicyBody": "FANUZU Política de privacidad\n\nTexto provisional hasta recibir el texto definitivo.\n\n1. Datos personales recopilados\nSe completará al recibir el texto definitivo.\n\n2. Finalidad del uso\nSe completará al recibir el texto definitivo.\n\n3. Plazo de conservación\nSe completará al recibir el texto definitivo.\n\n4. Encargo del tratamiento y transferencia internacional\nSe completará al recibir el texto definitivo.\n\n5. Eliminación de datos personales\nSe completará al recibir el texto definitivo.\n\n6. Tus derechos y cómo ejercerlos\nSe completará al recibir el texto definitivo.\n\n7. Responsable de privacidad y contacto\nSe completará al recibir el texto definitivo.\n\n8. Fecha de entrada en vigor\nSe completará al recibir el texto definitivo.",
|
||||
"termsLocationBody": "FANUZU Términos del servicio basado en la ubicación\n\nTexto provisional hasta recibir el texto definitivo.\n\nFinalidad del uso\nSe completará al recibir el texto definitivo.\n\nDatos de ubicación recopilados y utilizados\nSe completará al recibir el texto definitivo.\n\nPlazo de conservación\nSe completará al recibir el texto definitivo.\n\nInformación del proveedor del servicio basado en la ubicación\nSe completará al recibir el texto definitivo.\n\nFecha de entrada en vigor\nSe completará al recibir el texto definitivo.",
|
||||
"locationConsentBody": "Usamos la ubicación del dispositivo para las misiones con verificación de ubicación y para mostrar dónde estás en el mapa. Puedes seguir usando las demás funciones sin aceptar.",
|
||||
"locationConsentAgree": "Aceptar",
|
||||
"locationConsentLater": "Ahora no",
|
||||
"locationConsentNeeded": "Acepta los términos del servicio basado en ubicación para verificar tu ubicación.",
|
||||
"settingsLocationConsent": "Consentimiento del servicio basado en ubicación",
|
||||
"authSocialCancelled": "No se completó el inicio de sesión. Inténtalo de nuevo.",
|
||||
"authSocialNetwork": "Revisa tu conexión de red e inténtalo de nuevo."
|
||||
}
|
||||
|
||||
+16
-6
@@ -27,7 +27,7 @@
|
||||
"termsTitle": "利用規約",
|
||||
"termsAgreeAll": "すべて同意",
|
||||
"termsService": "サービス利用規約(必須)",
|
||||
"termsLocation": "位置情報サービス利用規約(必須)",
|
||||
"termsLocation": "位置情報サービス利用規約",
|
||||
"termsAge": "14歳以上です。(必須)",
|
||||
"termsMarketing": "マーケティング情報・イベント通知の受信に同意(任意)",
|
||||
"signupDoneTitle": "新規登録が完了しました",
|
||||
@@ -76,7 +76,6 @@
|
||||
"settingsUnlinkConfirm": "連携中のアカウントを解除しますか?",
|
||||
"settingsUnlinkBody": "連携したSNSのログイン手段のみ削除され、アカウント情報は削除されません。",
|
||||
"settingsUnlink": "連携解除",
|
||||
"settingsSameEmailOnly": "アカウントのメールと同じ\nソーシャルアカウントのみ連携できます。",
|
||||
"settingsLinkNotReady": "この端末では{name}連携をまだご利用いただけません。",
|
||||
"settingsConnect": "連携",
|
||||
"settingsDisconnect": "解除",
|
||||
@@ -98,7 +97,6 @@
|
||||
"linkTitle": "{name}連携",
|
||||
"linkNotLinked": "未連携",
|
||||
"linkLinked": "連携済み",
|
||||
"linkSameEmailOnly": "アカウントのメールと同じソーシャルアカウントのみ連携できます。",
|
||||
"linkUnlink": "連携解除",
|
||||
"linkLink": "連携する",
|
||||
"noticeBadge": "お知らせ",
|
||||
@@ -157,7 +155,6 @@
|
||||
"authProviderName": "{provider, select, email{メール} kakao{Kakao} line{LINE} apple{Apple} other{Google}}",
|
||||
"authLastProvider": "最後のログイン手段は連携解除できません。",
|
||||
"authLoginRequired": "ログインが必要です。",
|
||||
"authDifferentEmail": "登録したメールアドレスと異なるアカウントです。",
|
||||
"authLinkFailed": "アカウントの連携に失敗しました。",
|
||||
"authNotLinked": "連携されていないアカウントです。",
|
||||
"authEmailUnavailable": "メールログインは利用できません。",
|
||||
@@ -854,7 +851,7 @@
|
||||
"popReasonQuestFeeRefund": "クエスト手数料の返還",
|
||||
"questGoalSteps": "目標歩数",
|
||||
"eclipseTapToJoin": "振るのが難しければタップで参加",
|
||||
"termsPrivacy": "個人情報の収集・利用およびプライバシーポリシーへの同意(必須)",
|
||||
"termsPrivacy": "個人情報の収集・利用への同意(必須)",
|
||||
"onbSkip": "スキップ",
|
||||
"onbIntro1Title": "私たちのファンダムの惑星を育てよう",
|
||||
"onbIntro1Body": "POPを集めてZAPで送ると\nファンダムの惑星が一緒に成長します。",
|
||||
@@ -908,5 +905,18 @@
|
||||
"liveEclipseDefending": "[LIVE] いま{count}人のファンが一緒に防衛中",
|
||||
"liveHeroTime": "[LIVE] {fandom}惑星 HERO TIME 進行中",
|
||||
"liveHeroTimeBy": "[LIVE] {fandom}惑星 HERO TIME · by {name}",
|
||||
"questReflected": "クエスト達成が{fandom}の成長に反映されました"
|
||||
"questReflected": "クエスト達成が{fandom}の成長に反映されました",
|
||||
"termsPrivacyPolicy": "プライバシーポリシー",
|
||||
"termsPrivacyPolicyNotice": "個人情報の取り扱いはプライバシーポリシーでご確認いただけます。",
|
||||
"termsPrivacyBody": "FANUZU 個人情報の収集・利用への同意\n\n正式な文言を受け取るまでの仮の文言です。\n\n収集項目\n正式な文言を受け取り次第記載します。\n\n利用目的\n正式な文言を受け取り次第記載します。\n\n保有・利用期間\n正式な文言を受け取り次第記載します。\n\n同意を拒否する権利と拒否した場合の不利益\n正式な文言を受け取り次第記載します。",
|
||||
"termsMarketingBody": "FANUZU マーケティング情報受信への同意\n\n正式な文言を受け取るまでの仮の文言です。\n\n受信内容\n正式な文言を受け取り次第記載します。\n\n受信方法\n正式な文言を受け取り次第記載します。\n\n保有・利用期間\n正式な文言を受け取り次第記載します。\n\n同意の撤回方法\n正式な文言を受け取り次第記載します。",
|
||||
"termsPrivacyPolicyBody": "FANUZU プライバシーポリシー\n\n正式な文言を受け取るまでの仮の文言です。\n\n1. 収集する個人情報の項目\n正式な文言を受け取り次第記載します。\n\n2. 個人情報の利用目的\n正式な文言を受け取り次第記載します。\n\n3. 個人情報の保有・利用期間\n正式な文言を受け取り次第記載します。\n\n4. 個人情報の処理委託および国外移転\n正式な文言を受け取り次第記載します。\n\n5. 個人情報の破棄\n正式な文言を受け取り次第記載します。\n\n6. 利用者の権利と行使方法\n正式な文言を受け取り次第記載します。\n\n7. 個人情報保護責任者および問い合わせ先\n正式な文言を受け取り次第記載します。\n\n8. 施行日\n正式な文言を受け取り次第記載します。",
|
||||
"termsLocationBody": "FANUZU 位置情報サービス利用規約\n\n正式な文言を受け取るまでの仮の文言です。\n\n利用目的\n正式な文言を受け取り次第記載します。\n\n収集・利用する位置情報\n正式な文言を受け取り次第記載します。\n\n保有・利用期間\n正式な文言を受け取り次第記載します。\n\n位置情報サービス事業者情報\n正式な文言を受け取り次第記載します。\n\n施行日\n正式な文言を受け取り次第記載します。",
|
||||
"locationConsentBody": "位置認証クエストと地図上の現在地表示に端末の位置情報を使用します。同意しなくても他の機能はそのままご利用いただけます。",
|
||||
"locationConsentAgree": "同意する",
|
||||
"locationConsentLater": "後で",
|
||||
"locationConsentNeeded": "位置認証には位置情報サービス利用規約への同意が必要です。",
|
||||
"settingsLocationConsent": "位置情報サービスの利用同意",
|
||||
"authSocialCancelled": "ログインが完了しませんでした。もう一度お試しください。",
|
||||
"authSocialNetwork": "ネットワーク接続を確認してから、もう一度お試しください。"
|
||||
}
|
||||
|
||||
+16
-6
@@ -69,7 +69,7 @@
|
||||
"termsTitle": "이용 약관",
|
||||
"termsAgreeAll": "전체 동의",
|
||||
"termsService": "서비스 이용약관 (필수)",
|
||||
"termsLocation": "위치기반 서비스 이용약관 (필수)",
|
||||
"termsLocation": "위치기반 서비스 이용약관",
|
||||
"termsAge": "만 14세 이상입니다. (필수)",
|
||||
"termsMarketing": "마케팅 정보 및 이벤트 알림 수신 동의 (선택)",
|
||||
"signupDoneTitle": "회원가입이 완료되었습니다",
|
||||
@@ -134,7 +134,6 @@
|
||||
"settingsUnlinkConfirm": "연결된 계정을 끊으시겠습니까?",
|
||||
"settingsUnlinkBody": "연결한 SNS 계정 로그인 수단만 제거되며, 계정 정보는 삭제되지 않습니다.",
|
||||
"settingsUnlink": "연결 해제",
|
||||
"settingsSameEmailOnly": "계정 이메일과 동일한 소셜 계정만\n연결 할 수 있습니다.",
|
||||
"settingsLinkNotReady": "이 기기에서는 {name} 연동을 아직 이용할 수 없습니다.",
|
||||
"@settingsLinkNotReady": {
|
||||
"placeholders": {
|
||||
@@ -170,7 +169,6 @@
|
||||
},
|
||||
"linkNotLinked": "연동되지 않음",
|
||||
"linkLinked": "연동됨",
|
||||
"linkSameEmailOnly": "계정 이메일과 동일한 소셜 계정만 연결 할 수 있습니다.",
|
||||
"linkUnlink": "연동 해제",
|
||||
"linkLink": "연동하기",
|
||||
"noticeBadge": "공지",
|
||||
@@ -302,7 +300,6 @@
|
||||
},
|
||||
"authLastProvider": "마지막 로그인 수단은 연결을 해제할 수 없습니다.",
|
||||
"authLoginRequired": "로그인이 필요합니다.",
|
||||
"authDifferentEmail": "가입하신 이메일과 다른 계정입니다.",
|
||||
"authLinkFailed": "계정 연결에 실패했습니다.",
|
||||
"authNotLinked": "연결되어 있지 않은 계정입니다.",
|
||||
"authEmailUnavailable": "이메일 로그인을 쓸 수 없습니다.",
|
||||
@@ -1804,7 +1801,7 @@
|
||||
"popReasonQuestFeeRefund": "퀘스트 수수료 반환",
|
||||
"questGoalSteps": "목표 걸음 수",
|
||||
"eclipseTapToJoin": "흔들기 어려우면 눌러서 참여",
|
||||
"termsPrivacy": "개인정보 수집·이용 및 처리방침 동의 (필수)",
|
||||
"termsPrivacy": "개인정보 수집·이용 동의 (필수)",
|
||||
"onbSkip": "건너뛰기",
|
||||
"onbIntro1Title": "우리 팬덤의 행성을 키워요",
|
||||
"onbIntro1Body": "POP 을 모아 ZAP 으로 보내면\n팬덤 행성이 함께 성장합니다.",
|
||||
@@ -1876,5 +1873,18 @@
|
||||
"liveHeroTimeBy": "[LIVE] {fandom} 행성 HERO TIME · by {name}",
|
||||
"@liveHeroTimeBy": { "placeholders": { "fandom": { "type": "String" }, "name": { "type": "String" } } },
|
||||
"questReflected": "퀘스트 완료가 {fandom} 성장에 반영되었습니다",
|
||||
"@questReflected": { "placeholders": { "fandom": { "type": "String" } } }
|
||||
"@questReflected": { "placeholders": { "fandom": { "type": "String" } } },
|
||||
"termsPrivacyPolicy": "개인정보처리방침",
|
||||
"termsPrivacyPolicyNotice": "개인정보 처리 내용은 개인정보처리방침에서 확인할 수 있습니다.",
|
||||
"termsPrivacyBody": "FANUZU 개인정보 수집·이용 동의\n\n정식 문안을 받기 전 임시 문구입니다.\n\n수집 항목\n정식 문안을 받으면 기재합니다.\n\n이용 목적\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n동의를 거부할 권리와 거부 시 불이익\n정식 문안을 받으면 기재합니다.",
|
||||
"termsMarketingBody": "FANUZU 마케팅 정보 수신 동의\n\n정식 문안을 받기 전 임시 문구입니다.\n\n수신 내용\n정식 문안을 받으면 기재합니다.\n\n수신 방법\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n동의 철회 방법\n정식 문안을 받으면 기재합니다.",
|
||||
"termsPrivacyPolicyBody": "FANUZU 개인정보처리방침\n\n정식 문안을 받기 전 임시 문구입니다.\n\n1. 수집하는 개인정보 항목\n정식 문안을 받으면 기재합니다.\n\n2. 개인정보의 이용 목적\n정식 문안을 받으면 기재합니다.\n\n3. 개인정보의 보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n4. 개인정보 처리 위탁 및 국외 이전\n정식 문안을 받으면 기재합니다.\n\n5. 개인정보의 파기\n정식 문안을 받으면 기재합니다.\n\n6. 이용자의 권리와 행사 방법\n정식 문안을 받으면 기재합니다.\n\n7. 개인정보 보호책임자 및 문의처\n정식 문안을 받으면 기재합니다.\n\n8. 시행일\n정식 문안을 받으면 기재합니다.",
|
||||
"termsLocationBody": "FANUZU 위치기반서비스 이용약관\n\n정식 문안을 받기 전 임시 문구입니다.\n\n이용 목적\n정식 문안을 받으면 기재합니다.\n\n수집·이용하는 위치정보\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n위치기반서비스 사업자 정보\n정식 문안을 받으면 기재합니다.\n\n시행일\n정식 문안을 받으면 기재합니다.",
|
||||
"locationConsentBody": "위치 인증 퀘스트와 지도의 현재 위치 표시에 기기 위치를 사용합니다. 동의하지 않아도 다른 기능은 그대로 이용할 수 있습니다.",
|
||||
"locationConsentAgree": "동의",
|
||||
"locationConsentLater": "나중에",
|
||||
"locationConsentNeeded": "위치기반 서비스 이용약관에 동의해야 위치 인증을 할 수 있어요.",
|
||||
"settingsLocationConsent": "위치기반 서비스 이용 동의",
|
||||
"authSocialCancelled": "로그인이 완료되지 않았습니다. 다시 시도해 주세요.",
|
||||
"authSocialNetwork": "네트워크 연결을 확인한 뒤 다시 시도해 주세요."
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @termsLocation.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'위치기반 서비스 이용약관 (필수)'**
|
||||
/// **'위치기반 서비스 이용약관'**
|
||||
String get termsLocation;
|
||||
|
||||
/// No description provided for @termsAge.
|
||||
@@ -560,12 +560,6 @@ abstract class AppLocalizations {
|
||||
/// **'연결 해제'**
|
||||
String get settingsUnlink;
|
||||
|
||||
/// No description provided for @settingsSameEmailOnly.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'계정 이메일과 동일한 소셜 계정만\n연결 할 수 있습니다.'**
|
||||
String get settingsSameEmailOnly;
|
||||
|
||||
/// No description provided for @settingsLinkNotReady.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
@@ -692,12 +686,6 @@ abstract class AppLocalizations {
|
||||
/// **'연동됨'**
|
||||
String get linkLinked;
|
||||
|
||||
/// No description provided for @linkSameEmailOnly.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'계정 이메일과 동일한 소셜 계정만 연결 할 수 있습니다.'**
|
||||
String get linkSameEmailOnly;
|
||||
|
||||
/// No description provided for @linkUnlink.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
@@ -1046,12 +1034,6 @@ abstract class AppLocalizations {
|
||||
/// **'로그인이 필요합니다.'**
|
||||
String get authLoginRequired;
|
||||
|
||||
/// No description provided for @authDifferentEmail.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'가입하신 이메일과 다른 계정입니다.'**
|
||||
String get authDifferentEmail;
|
||||
|
||||
/// No description provided for @authLinkFailed.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
@@ -5243,7 +5225,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @termsPrivacy.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'개인정보 수집·이용 및 처리방침 동의 (필수)'**
|
||||
/// **'개인정보 수집·이용 동의 (필수)'**
|
||||
String get termsPrivacy;
|
||||
|
||||
/// No description provided for @onbSkip.
|
||||
@@ -5569,6 +5551,84 @@ abstract class AppLocalizations {
|
||||
/// In ko, this message translates to:
|
||||
/// **'퀘스트 완료가 {fandom} 성장에 반영되었습니다'**
|
||||
String questReflected(String fandom);
|
||||
|
||||
/// No description provided for @termsPrivacyPolicy.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'개인정보처리방침'**
|
||||
String get termsPrivacyPolicy;
|
||||
|
||||
/// No description provided for @termsPrivacyPolicyNotice.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'개인정보 처리 내용은 개인정보처리방침에서 확인할 수 있습니다.'**
|
||||
String get termsPrivacyPolicyNotice;
|
||||
|
||||
/// No description provided for @termsPrivacyBody.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'FANUZU 개인정보 수집·이용 동의\n\n정식 문안을 받기 전 임시 문구입니다.\n\n수집 항목\n정식 문안을 받으면 기재합니다.\n\n이용 목적\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n동의를 거부할 권리와 거부 시 불이익\n정식 문안을 받으면 기재합니다.'**
|
||||
String get termsPrivacyBody;
|
||||
|
||||
/// No description provided for @termsMarketingBody.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'FANUZU 마케팅 정보 수신 동의\n\n정식 문안을 받기 전 임시 문구입니다.\n\n수신 내용\n정식 문안을 받으면 기재합니다.\n\n수신 방법\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n동의 철회 방법\n정식 문안을 받으면 기재합니다.'**
|
||||
String get termsMarketingBody;
|
||||
|
||||
/// No description provided for @termsPrivacyPolicyBody.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'FANUZU 개인정보처리방침\n\n정식 문안을 받기 전 임시 문구입니다.\n\n1. 수집하는 개인정보 항목\n정식 문안을 받으면 기재합니다.\n\n2. 개인정보의 이용 목적\n정식 문안을 받으면 기재합니다.\n\n3. 개인정보의 보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n4. 개인정보 처리 위탁 및 국외 이전\n정식 문안을 받으면 기재합니다.\n\n5. 개인정보의 파기\n정식 문안을 받으면 기재합니다.\n\n6. 이용자의 권리와 행사 방법\n정식 문안을 받으면 기재합니다.\n\n7. 개인정보 보호책임자 및 문의처\n정식 문안을 받으면 기재합니다.\n\n8. 시행일\n정식 문안을 받으면 기재합니다.'**
|
||||
String get termsPrivacyPolicyBody;
|
||||
|
||||
/// No description provided for @termsLocationBody.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'FANUZU 위치기반서비스 이용약관\n\n정식 문안을 받기 전 임시 문구입니다.\n\n이용 목적\n정식 문안을 받으면 기재합니다.\n\n수집·이용하는 위치정보\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n위치기반서비스 사업자 정보\n정식 문안을 받으면 기재합니다.\n\n시행일\n정식 문안을 받으면 기재합니다.'**
|
||||
String get termsLocationBody;
|
||||
|
||||
/// No description provided for @locationConsentBody.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'위치 인증 퀘스트와 지도의 현재 위치 표시에 기기 위치를 사용합니다. 동의하지 않아도 다른 기능은 그대로 이용할 수 있습니다.'**
|
||||
String get locationConsentBody;
|
||||
|
||||
/// No description provided for @locationConsentAgree.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'동의'**
|
||||
String get locationConsentAgree;
|
||||
|
||||
/// No description provided for @locationConsentLater.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'나중에'**
|
||||
String get locationConsentLater;
|
||||
|
||||
/// No description provided for @locationConsentNeeded.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'위치기반 서비스 이용약관에 동의해야 위치 인증을 할 수 있어요.'**
|
||||
String get locationConsentNeeded;
|
||||
|
||||
/// No description provided for @settingsLocationConsent.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'위치기반 서비스 이용 동의'**
|
||||
String get settingsLocationConsent;
|
||||
|
||||
/// No description provided for @authSocialCancelled.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'로그인이 완료되지 않았습니다. 다시 시도해 주세요.'**
|
||||
String get authSocialCancelled;
|
||||
|
||||
/// No description provided for @authSocialNetwork.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'네트워크 연결을 확인한 뒤 다시 시도해 주세요.'**
|
||||
String get authSocialNetwork;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -117,7 +117,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get termsService => 'Terms of Service (required)';
|
||||
|
||||
@override
|
||||
String get termsLocation => 'Location-based Service Terms (required)';
|
||||
String get termsLocation => 'Location-based Service Terms';
|
||||
|
||||
@override
|
||||
String get termsAge => 'I am 14 or older. (required)';
|
||||
@@ -271,10 +271,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settingsUnlink => 'Unlink';
|
||||
|
||||
@override
|
||||
String get settingsSameEmailOnly =>
|
||||
'Only social accounts with the same\nemail as your account can be linked.';
|
||||
|
||||
@override
|
||||
String settingsLinkNotReady(String name) {
|
||||
return '$name linking isn\'t available on this device yet.';
|
||||
@@ -345,10 +341,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get linkLinked => 'Linked';
|
||||
|
||||
@override
|
||||
String get linkSameEmailOnly =>
|
||||
'Only social accounts with the same email as your account can be linked.';
|
||||
|
||||
@override
|
||||
String get linkUnlink => 'Unlink';
|
||||
|
||||
@@ -566,10 +558,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get authLoginRequired => 'Please sign in.';
|
||||
|
||||
@override
|
||||
String get authDifferentEmail =>
|
||||
'This account doesn\'t match your sign-up email.';
|
||||
|
||||
@override
|
||||
String get authLinkFailed => 'Couldn\'t link the account.';
|
||||
|
||||
@@ -2985,7 +2973,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get termsPrivacy =>
|
||||
'Privacy Policy and Personal Data Collection (required)';
|
||||
'Consent to Collection and Use of Personal Data (required)';
|
||||
|
||||
@override
|
||||
String get onbSkip => 'Skip';
|
||||
@@ -3169,4 +3157,52 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String questReflected(String fandom) {
|
||||
return 'Quest completion counted toward $fandom\'s growth';
|
||||
}
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicy => 'Privacy Policy';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyNotice =>
|
||||
'See the Privacy Policy for how your personal data is processed.';
|
||||
|
||||
@override
|
||||
String get termsPrivacyBody =>
|
||||
'FANUZU Consent to Collection and Use of Personal Data\n\nThis is temporary placeholder text until the final text is provided.\n\nData collected\nTo be filled in when the final text is provided.\n\nPurpose of use\nTo be filled in when the final text is provided.\n\nRetention period\nTo be filled in when the final text is provided.\n\nRight to refuse consent and consequences of refusal\nTo be filled in when the final text is provided.';
|
||||
|
||||
@override
|
||||
String get termsMarketingBody =>
|
||||
'FANUZU Consent to Receive Marketing Information\n\nThis is temporary placeholder text until the final text is provided.\n\nWhat you will receive\nTo be filled in when the final text is provided.\n\nHow you will receive it\nTo be filled in when the final text is provided.\n\nRetention period\nTo be filled in when the final text is provided.\n\nHow to withdraw consent\nTo be filled in when the final text is provided.';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyBody =>
|
||||
'FANUZU Privacy Policy\n\nThis is temporary placeholder text until the final text is provided.\n\n1. Personal data collected\nTo be filled in when the final text is provided.\n\n2. Purpose of use\nTo be filled in when the final text is provided.\n\n3. Retention period\nTo be filled in when the final text is provided.\n\n4. Processing outsourcing and overseas transfer\nTo be filled in when the final text is provided.\n\n5. Destruction of personal data\nTo be filled in when the final text is provided.\n\n6. Your rights and how to exercise them\nTo be filled in when the final text is provided.\n\n7. Privacy officer and contact\nTo be filled in when the final text is provided.\n\n8. Effective date\nTo be filled in when the final text is provided.';
|
||||
|
||||
@override
|
||||
String get termsLocationBody =>
|
||||
'FANUZU Location-based Service Terms\n\nThis is temporary placeholder text until the final text is provided.\n\nPurpose of use\nTo be filled in when the final text is provided.\n\nLocation data collected and used\nTo be filled in when the final text is provided.\n\nRetention period\nTo be filled in when the final text is provided.\n\nLocation-based service provider information\nTo be filled in when the final text is provided.\n\nEffective date\nTo be filled in when the final text is provided.';
|
||||
|
||||
@override
|
||||
String get locationConsentBody =>
|
||||
'Your device location is used for location-verified quests and to show where you are on the map. You can keep using other features without agreeing.';
|
||||
|
||||
@override
|
||||
String get locationConsentAgree => 'Agree';
|
||||
|
||||
@override
|
||||
String get locationConsentLater => 'Not now';
|
||||
|
||||
@override
|
||||
String get locationConsentNeeded =>
|
||||
'Agree to the Location-based Service Terms to verify your location.';
|
||||
|
||||
@override
|
||||
String get settingsLocationConsent => 'Location-based service consent';
|
||||
|
||||
@override
|
||||
String get authSocialCancelled =>
|
||||
'Sign-in wasn\'t completed. Please try again.';
|
||||
|
||||
@override
|
||||
String get authSocialNetwork =>
|
||||
'Check your network connection and try again.';
|
||||
}
|
||||
|
||||
@@ -118,8 +118,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get termsService => 'Términos del servicio (obligatorio)';
|
||||
|
||||
@override
|
||||
String get termsLocation =>
|
||||
'Términos de servicios de ubicación (obligatorio)';
|
||||
String get termsLocation => 'Términos del servicio basado en ubicación';
|
||||
|
||||
@override
|
||||
String get termsAge => 'Tengo 14 años o más. (obligatorio)';
|
||||
@@ -273,10 +272,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settingsUnlink => 'Desvincular';
|
||||
|
||||
@override
|
||||
String get settingsSameEmailOnly =>
|
||||
'Solo se pueden vincular cuentas sociales\ncon el mismo correo.';
|
||||
|
||||
@override
|
||||
String settingsLinkNotReady(String name) {
|
||||
return 'La vinculación con $name aún no está disponible en este dispositivo.';
|
||||
@@ -348,10 +343,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get linkLinked => 'Vinculada';
|
||||
|
||||
@override
|
||||
String get linkSameEmailOnly =>
|
||||
'Solo se pueden vincular cuentas sociales con el mismo correo.';
|
||||
|
||||
@override
|
||||
String get linkUnlink => 'Desvincular';
|
||||
|
||||
@@ -571,10 +562,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get authLoginRequired => 'Debes iniciar sesión.';
|
||||
|
||||
@override
|
||||
String get authDifferentEmail =>
|
||||
'Esta cuenta no coincide con tu correo de registro.';
|
||||
|
||||
@override
|
||||
String get authLinkFailed => 'No se pudo vincular la cuenta.';
|
||||
|
||||
@@ -3000,7 +2987,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get termsPrivacy =>
|
||||
'Política de privacidad y recopilación de datos personales (obligatorio)';
|
||||
'Consentimiento para la recopilación y uso de datos personales (obligatorio)';
|
||||
|
||||
@override
|
||||
String get onbSkip => 'Omitir';
|
||||
@@ -3185,4 +3172,53 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String questReflected(String fandom) {
|
||||
return 'Tu misión completada se reflejó en el crecimiento de $fandom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicy => 'Política de privacidad';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyNotice =>
|
||||
'Consulta la Política de privacidad para saber cómo se tratan tus datos personales.';
|
||||
|
||||
@override
|
||||
String get termsPrivacyBody =>
|
||||
'FANUZU Consentimiento para la recopilación y el uso de datos personales\n\nTexto provisional hasta recibir el texto definitivo.\n\nDatos recopilados\nSe completará al recibir el texto definitivo.\n\nFinalidad del uso\nSe completará al recibir el texto definitivo.\n\nPlazo de conservación\nSe completará al recibir el texto definitivo.\n\nDerecho a negarse y consecuencias de la negativa\nSe completará al recibir el texto definitivo.';
|
||||
|
||||
@override
|
||||
String get termsMarketingBody =>
|
||||
'FANUZU Consentimiento para recibir información de marketing\n\nTexto provisional hasta recibir el texto definitivo.\n\nContenido que recibirás\nSe completará al recibir el texto definitivo.\n\nMedio de envío\nSe completará al recibir el texto definitivo.\n\nPlazo de conservación\nSe completará al recibir el texto definitivo.\n\nCómo retirar el consentimiento\nSe completará al recibir el texto definitivo.';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyBody =>
|
||||
'FANUZU Política de privacidad\n\nTexto provisional hasta recibir el texto definitivo.\n\n1. Datos personales recopilados\nSe completará al recibir el texto definitivo.\n\n2. Finalidad del uso\nSe completará al recibir el texto definitivo.\n\n3. Plazo de conservación\nSe completará al recibir el texto definitivo.\n\n4. Encargo del tratamiento y transferencia internacional\nSe completará al recibir el texto definitivo.\n\n5. Eliminación de datos personales\nSe completará al recibir el texto definitivo.\n\n6. Tus derechos y cómo ejercerlos\nSe completará al recibir el texto definitivo.\n\n7. Responsable de privacidad y contacto\nSe completará al recibir el texto definitivo.\n\n8. Fecha de entrada en vigor\nSe completará al recibir el texto definitivo.';
|
||||
|
||||
@override
|
||||
String get termsLocationBody =>
|
||||
'FANUZU Términos del servicio basado en la ubicación\n\nTexto provisional hasta recibir el texto definitivo.\n\nFinalidad del uso\nSe completará al recibir el texto definitivo.\n\nDatos de ubicación recopilados y utilizados\nSe completará al recibir el texto definitivo.\n\nPlazo de conservación\nSe completará al recibir el texto definitivo.\n\nInformación del proveedor del servicio basado en la ubicación\nSe completará al recibir el texto definitivo.\n\nFecha de entrada en vigor\nSe completará al recibir el texto definitivo.';
|
||||
|
||||
@override
|
||||
String get locationConsentBody =>
|
||||
'Usamos la ubicación del dispositivo para las misiones con verificación de ubicación y para mostrar dónde estás en el mapa. Puedes seguir usando las demás funciones sin aceptar.';
|
||||
|
||||
@override
|
||||
String get locationConsentAgree => 'Aceptar';
|
||||
|
||||
@override
|
||||
String get locationConsentLater => 'Ahora no';
|
||||
|
||||
@override
|
||||
String get locationConsentNeeded =>
|
||||
'Acepta los términos del servicio basado en ubicación para verificar tu ubicación.';
|
||||
|
||||
@override
|
||||
String get settingsLocationConsent =>
|
||||
'Consentimiento del servicio basado en ubicación';
|
||||
|
||||
@override
|
||||
String get authSocialCancelled =>
|
||||
'No se completó el inicio de sesión. Inténtalo de nuevo.';
|
||||
|
||||
@override
|
||||
String get authSocialNetwork =>
|
||||
'Revisa tu conexión de red e inténtalo de nuevo.';
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get termsService => 'サービス利用規約(必須)';
|
||||
|
||||
@override
|
||||
String get termsLocation => '位置情報サービス利用規約(必須)';
|
||||
String get termsLocation => '位置情報サービス利用規約';
|
||||
|
||||
@override
|
||||
String get termsAge => '14歳以上です。(必須)';
|
||||
@@ -266,9 +266,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settingsUnlink => '連携解除';
|
||||
|
||||
@override
|
||||
String get settingsSameEmailOnly => 'アカウントのメールと同じ\nソーシャルアカウントのみ連携できます。';
|
||||
|
||||
@override
|
||||
String settingsLinkNotReady(String name) {
|
||||
return 'この端末では$name連携をまだご利用いただけません。';
|
||||
@@ -336,9 +333,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get linkLinked => '連携済み';
|
||||
|
||||
@override
|
||||
String get linkSameEmailOnly => 'アカウントのメールと同じソーシャルアカウントのみ連携できます。';
|
||||
|
||||
@override
|
||||
String get linkUnlink => '連携解除';
|
||||
|
||||
@@ -547,9 +541,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get authLoginRequired => 'ログインが必要です。';
|
||||
|
||||
@override
|
||||
String get authDifferentEmail => '登録したメールアドレスと異なるアカウントです。';
|
||||
|
||||
@override
|
||||
String get authLinkFailed => 'アカウントの連携に失敗しました。';
|
||||
|
||||
@@ -2887,7 +2878,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get eclipseTapToJoin => '振るのが難しければタップで参加';
|
||||
|
||||
@override
|
||||
String get termsPrivacy => '個人情報の収集・利用およびプライバシーポリシーへの同意(必須)';
|
||||
String get termsPrivacy => '個人情報の収集・利用への同意(必須)';
|
||||
|
||||
@override
|
||||
String get onbSkip => 'スキップ';
|
||||
@@ -3062,4 +3053,48 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String questReflected(String fandom) {
|
||||
return 'クエスト達成が$fandomの成長に反映されました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicy => 'プライバシーポリシー';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyNotice => '個人情報の取り扱いはプライバシーポリシーでご確認いただけます。';
|
||||
|
||||
@override
|
||||
String get termsPrivacyBody =>
|
||||
'FANUZU 個人情報の収集・利用への同意\n\n正式な文言を受け取るまでの仮の文言です。\n\n収集項目\n正式な文言を受け取り次第記載します。\n\n利用目的\n正式な文言を受け取り次第記載します。\n\n保有・利用期間\n正式な文言を受け取り次第記載します。\n\n同意を拒否する権利と拒否した場合の不利益\n正式な文言を受け取り次第記載します。';
|
||||
|
||||
@override
|
||||
String get termsMarketingBody =>
|
||||
'FANUZU マーケティング情報受信への同意\n\n正式な文言を受け取るまでの仮の文言です。\n\n受信内容\n正式な文言を受け取り次第記載します。\n\n受信方法\n正式な文言を受け取り次第記載します。\n\n保有・利用期間\n正式な文言を受け取り次第記載します。\n\n同意の撤回方法\n正式な文言を受け取り次第記載します。';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyBody =>
|
||||
'FANUZU プライバシーポリシー\n\n正式な文言を受け取るまでの仮の文言です。\n\n1. 収集する個人情報の項目\n正式な文言を受け取り次第記載します。\n\n2. 個人情報の利用目的\n正式な文言を受け取り次第記載します。\n\n3. 個人情報の保有・利用期間\n正式な文言を受け取り次第記載します。\n\n4. 個人情報の処理委託および国外移転\n正式な文言を受け取り次第記載します。\n\n5. 個人情報の破棄\n正式な文言を受け取り次第記載します。\n\n6. 利用者の権利と行使方法\n正式な文言を受け取り次第記載します。\n\n7. 個人情報保護責任者および問い合わせ先\n正式な文言を受け取り次第記載します。\n\n8. 施行日\n正式な文言を受け取り次第記載します。';
|
||||
|
||||
@override
|
||||
String get termsLocationBody =>
|
||||
'FANUZU 位置情報サービス利用規約\n\n正式な文言を受け取るまでの仮の文言です。\n\n利用目的\n正式な文言を受け取り次第記載します。\n\n収集・利用する位置情報\n正式な文言を受け取り次第記載します。\n\n保有・利用期間\n正式な文言を受け取り次第記載します。\n\n位置情報サービス事業者情報\n正式な文言を受け取り次第記載します。\n\n施行日\n正式な文言を受け取り次第記載します。';
|
||||
|
||||
@override
|
||||
String get locationConsentBody =>
|
||||
'位置認証クエストと地図上の現在地表示に端末の位置情報を使用します。同意しなくても他の機能はそのままご利用いただけます。';
|
||||
|
||||
@override
|
||||
String get locationConsentAgree => '同意する';
|
||||
|
||||
@override
|
||||
String get locationConsentLater => '後で';
|
||||
|
||||
@override
|
||||
String get locationConsentNeeded => '位置認証には位置情報サービス利用規約への同意が必要です。';
|
||||
|
||||
@override
|
||||
String get settingsLocationConsent => '位置情報サービスの利用同意';
|
||||
|
||||
@override
|
||||
String get authSocialCancelled => 'ログインが完了しませんでした。もう一度お試しください。';
|
||||
|
||||
@override
|
||||
String get authSocialNetwork => 'ネットワーク接続を確認してから、もう一度お試しください。';
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get termsService => '서비스 이용약관 (필수)';
|
||||
|
||||
@override
|
||||
String get termsLocation => '위치기반 서비스 이용약관 (필수)';
|
||||
String get termsLocation => '위치기반 서비스 이용약관';
|
||||
|
||||
@override
|
||||
String get termsAge => '만 14세 이상입니다. (필수)';
|
||||
@@ -266,9 +266,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get settingsUnlink => '연결 해제';
|
||||
|
||||
@override
|
||||
String get settingsSameEmailOnly => '계정 이메일과 동일한 소셜 계정만\n연결 할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String settingsLinkNotReady(String name) {
|
||||
return '이 기기에서는 $name 연동을 아직 이용할 수 없습니다.';
|
||||
@@ -336,9 +333,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get linkLinked => '연동됨';
|
||||
|
||||
@override
|
||||
String get linkSameEmailOnly => '계정 이메일과 동일한 소셜 계정만 연결 할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get linkUnlink => '연동 해제';
|
||||
|
||||
@@ -547,9 +541,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get authLoginRequired => '로그인이 필요합니다.';
|
||||
|
||||
@override
|
||||
String get authDifferentEmail => '가입하신 이메일과 다른 계정입니다.';
|
||||
|
||||
@override
|
||||
String get authLinkFailed => '계정 연결에 실패했습니다.';
|
||||
|
||||
@@ -2890,7 +2881,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get eclipseTapToJoin => '흔들기 어려우면 눌러서 참여';
|
||||
|
||||
@override
|
||||
String get termsPrivacy => '개인정보 수집·이용 및 처리방침 동의 (필수)';
|
||||
String get termsPrivacy => '개인정보 수집·이용 동의 (필수)';
|
||||
|
||||
@override
|
||||
String get onbSkip => '건너뛰기';
|
||||
@@ -3066,4 +3057,48 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String questReflected(String fandom) {
|
||||
return '퀘스트 완료가 $fandom 성장에 반영되었습니다';
|
||||
}
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicy => '개인정보처리방침';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyNotice => '개인정보 처리 내용은 개인정보처리방침에서 확인할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get termsPrivacyBody =>
|
||||
'FANUZU 개인정보 수집·이용 동의\n\n정식 문안을 받기 전 임시 문구입니다.\n\n수집 항목\n정식 문안을 받으면 기재합니다.\n\n이용 목적\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n동의를 거부할 권리와 거부 시 불이익\n정식 문안을 받으면 기재합니다.';
|
||||
|
||||
@override
|
||||
String get termsMarketingBody =>
|
||||
'FANUZU 마케팅 정보 수신 동의\n\n정식 문안을 받기 전 임시 문구입니다.\n\n수신 내용\n정식 문안을 받으면 기재합니다.\n\n수신 방법\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n동의 철회 방법\n정식 문안을 받으면 기재합니다.';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyBody =>
|
||||
'FANUZU 개인정보처리방침\n\n정식 문안을 받기 전 임시 문구입니다.\n\n1. 수집하는 개인정보 항목\n정식 문안을 받으면 기재합니다.\n\n2. 개인정보의 이용 목적\n정식 문안을 받으면 기재합니다.\n\n3. 개인정보의 보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n4. 개인정보 처리 위탁 및 국외 이전\n정식 문안을 받으면 기재합니다.\n\n5. 개인정보의 파기\n정식 문안을 받으면 기재합니다.\n\n6. 이용자의 권리와 행사 방법\n정식 문안을 받으면 기재합니다.\n\n7. 개인정보 보호책임자 및 문의처\n정식 문안을 받으면 기재합니다.\n\n8. 시행일\n정식 문안을 받으면 기재합니다.';
|
||||
|
||||
@override
|
||||
String get termsLocationBody =>
|
||||
'FANUZU 위치기반서비스 이용약관\n\n정식 문안을 받기 전 임시 문구입니다.\n\n이용 목적\n정식 문안을 받으면 기재합니다.\n\n수집·이용하는 위치정보\n정식 문안을 받으면 기재합니다.\n\n보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n위치기반서비스 사업자 정보\n정식 문안을 받으면 기재합니다.\n\n시행일\n정식 문안을 받으면 기재합니다.';
|
||||
|
||||
@override
|
||||
String get locationConsentBody =>
|
||||
'위치 인증 퀘스트와 지도의 현재 위치 표시에 기기 위치를 사용합니다. 동의하지 않아도 다른 기능은 그대로 이용할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get locationConsentAgree => '동의';
|
||||
|
||||
@override
|
||||
String get locationConsentLater => '나중에';
|
||||
|
||||
@override
|
||||
String get locationConsentNeeded => '위치기반 서비스 이용약관에 동의해야 위치 인증을 할 수 있어요.';
|
||||
|
||||
@override
|
||||
String get settingsLocationConsent => '위치기반 서비스 이용 동의';
|
||||
|
||||
@override
|
||||
String get authSocialCancelled => '로그인이 완료되지 않았습니다. 다시 시도해 주세요.';
|
||||
|
||||
@override
|
||||
String get authSocialNetwork => '네트워크 연결을 확인한 뒤 다시 시도해 주세요.';
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get termsService => '服务条款(必选)';
|
||||
|
||||
@override
|
||||
String get termsLocation => '位置服务条款(必选)';
|
||||
String get termsLocation => '位置服务条款';
|
||||
|
||||
@override
|
||||
String get termsAge => '本人已满14周岁。(必选)';
|
||||
@@ -266,9 +266,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settingsUnlink => '解除关联';
|
||||
|
||||
@override
|
||||
String get settingsSameEmailOnly => '只能关联与账号邮箱\n相同的社交账号。';
|
||||
|
||||
@override
|
||||
String settingsLinkNotReady(String name) {
|
||||
return '此设备暂时无法使用$name关联。';
|
||||
@@ -336,9 +333,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get linkLinked => '已关联';
|
||||
|
||||
@override
|
||||
String get linkSameEmailOnly => '只能关联与账号邮箱相同的社交账号。';
|
||||
|
||||
@override
|
||||
String get linkUnlink => '解除关联';
|
||||
|
||||
@@ -547,9 +541,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get authLoginRequired => '请先登录。';
|
||||
|
||||
@override
|
||||
String get authDifferentEmail => '该账号与注册邮箱不一致。';
|
||||
|
||||
@override
|
||||
String get authLinkFailed => '账号关联失败。';
|
||||
|
||||
@@ -2882,7 +2873,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get eclipseTapToJoin => '不方便摇动?点击参与';
|
||||
|
||||
@override
|
||||
String get termsPrivacy => '个人信息收集·使用及隐私政策(必选)';
|
||||
String get termsPrivacy => '同意收集·使用个人信息(必选)';
|
||||
|
||||
@override
|
||||
String get onbSkip => '跳过';
|
||||
@@ -3057,4 +3048,47 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String questReflected(String fandom) {
|
||||
return '任务完成已计入$fandom的成长';
|
||||
}
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicy => '隐私政策';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyNotice => '个人信息的处理方式请参阅隐私政策。';
|
||||
|
||||
@override
|
||||
String get termsPrivacyBody =>
|
||||
'FANUZU 个人信息收集与使用同意\n\n这是收到正式文本前的临时文字。\n\n收集项目\n收到正式文本后填写。\n\n使用目的\n收到正式文本后填写。\n\n保存与使用期限\n收到正式文本后填写。\n\n拒绝同意的权利及拒绝后的影响\n收到正式文本后填写。';
|
||||
|
||||
@override
|
||||
String get termsMarketingBody =>
|
||||
'FANUZU 营销信息接收同意\n\n这是收到正式文本前的临时文字。\n\n接收内容\n收到正式文本后填写。\n\n接收方式\n收到正式文本后填写。\n\n保存与使用期限\n收到正式文本后填写。\n\n撤回同意的方法\n收到正式文本后填写。';
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyBody =>
|
||||
'FANUZU 隐私政策\n\n这是收到正式文本前的临时文字。\n\n1. 收集的个人信息项目\n收到正式文本后填写。\n\n2. 个人信息的使用目的\n收到正式文本后填写。\n\n3. 个人信息的保存与使用期限\n收到正式文本后填写。\n\n4. 个人信息处理委托及境外转移\n收到正式文本后填写。\n\n5. 个人信息的销毁\n收到正式文本后填写。\n\n6. 用户的权利及行使方法\n收到正式文本后填写。\n\n7. 个人信息保护负责人及联系方式\n收到正式文本后填写。\n\n8. 生效日期\n收到正式文本后填写。';
|
||||
|
||||
@override
|
||||
String get termsLocationBody =>
|
||||
'FANUZU 位置服务使用条款\n\n这是收到正式文本前的临时文字。\n\n使用目的\n收到正式文本后填写。\n\n收集和使用的位置信息\n收到正式文本后填写。\n\n保存与使用期限\n收到正式文本后填写。\n\n位置服务提供者信息\n收到正式文本后填写。\n\n生效日期\n收到正式文本后填写。';
|
||||
|
||||
@override
|
||||
String get locationConsentBody => '位置认证任务和地图上的当前位置显示将使用设备位置。不同意也可以继续使用其他功能。';
|
||||
|
||||
@override
|
||||
String get locationConsentAgree => '同意';
|
||||
|
||||
@override
|
||||
String get locationConsentLater => '以后再说';
|
||||
|
||||
@override
|
||||
String get locationConsentNeeded => '需同意位置服务条款才能进行位置认证。';
|
||||
|
||||
@override
|
||||
String get settingsLocationConsent => '位置服务使用同意';
|
||||
|
||||
@override
|
||||
String get authSocialCancelled => '登录未完成,请重试。';
|
||||
|
||||
@override
|
||||
String get authSocialNetwork => '请检查网络连接后重试。';
|
||||
}
|
||||
|
||||
+16
-6
@@ -27,7 +27,7 @@
|
||||
"termsTitle": "使用条款",
|
||||
"termsAgreeAll": "全部同意",
|
||||
"termsService": "服务条款(必选)",
|
||||
"termsLocation": "位置服务条款(必选)",
|
||||
"termsLocation": "位置服务条款",
|
||||
"termsAge": "本人已满14周岁。(必选)",
|
||||
"termsMarketing": "同意接收营销信息与活动通知(可选)",
|
||||
"signupDoneTitle": "注册完成",
|
||||
@@ -76,7 +76,6 @@
|
||||
"settingsUnlinkConfirm": "确定要解除关联账号吗?",
|
||||
"settingsUnlinkBody": "仅移除该社交登录方式,账号信息不会被删除。",
|
||||
"settingsUnlink": "解除关联",
|
||||
"settingsSameEmailOnly": "只能关联与账号邮箱\n相同的社交账号。",
|
||||
"settingsLinkNotReady": "此设备暂时无法使用{name}关联。",
|
||||
"settingsConnect": "关联",
|
||||
"settingsDisconnect": "解除",
|
||||
@@ -98,7 +97,6 @@
|
||||
"linkTitle": "{name}关联",
|
||||
"linkNotLinked": "未关联",
|
||||
"linkLinked": "已关联",
|
||||
"linkSameEmailOnly": "只能关联与账号邮箱相同的社交账号。",
|
||||
"linkUnlink": "解除关联",
|
||||
"linkLink": "关联",
|
||||
"noticeBadge": "公告",
|
||||
@@ -157,7 +155,6 @@
|
||||
"authProviderName": "{provider, select, email{邮箱} kakao{Kakao} line{LINE} apple{Apple} other{Google}}",
|
||||
"authLastProvider": "无法解除最后一个登录方式的关联。",
|
||||
"authLoginRequired": "请先登录。",
|
||||
"authDifferentEmail": "该账号与注册邮箱不一致。",
|
||||
"authLinkFailed": "账号关联失败。",
|
||||
"authNotLinked": "该账号未关联。",
|
||||
"authEmailUnavailable": "无法使用邮箱登录。",
|
||||
@@ -854,7 +851,7 @@
|
||||
"popReasonQuestFeeRefund": "任务手续费退还",
|
||||
"questGoalSteps": "目标步数",
|
||||
"eclipseTapToJoin": "不方便摇动?点击参与",
|
||||
"termsPrivacy": "个人信息收集·使用及隐私政策(必选)",
|
||||
"termsPrivacy": "同意收集·使用个人信息(必选)",
|
||||
"onbSkip": "跳过",
|
||||
"onbIntro1Title": "一起培养我们粉丝团的星球",
|
||||
"onbIntro1Body": "收集POP并通过ZAP发送,\n粉丝团星球就会一起成长。",
|
||||
@@ -908,5 +905,18 @@
|
||||
"liveEclipseDefending": "[LIVE] 现在有{count}位粉丝一起防御中",
|
||||
"liveHeroTime": "[LIVE] {fandom}行星 HERO TIME 进行中",
|
||||
"liveHeroTimeBy": "[LIVE] {fandom}行星 HERO TIME · by {name}",
|
||||
"questReflected": "任务完成已计入{fandom}的成长"
|
||||
"questReflected": "任务完成已计入{fandom}的成长",
|
||||
"termsPrivacyPolicy": "隐私政策",
|
||||
"termsPrivacyPolicyNotice": "个人信息的处理方式请参阅隐私政策。",
|
||||
"termsPrivacyBody": "FANUZU 个人信息收集与使用同意\n\n这是收到正式文本前的临时文字。\n\n收集项目\n收到正式文本后填写。\n\n使用目的\n收到正式文本后填写。\n\n保存与使用期限\n收到正式文本后填写。\n\n拒绝同意的权利及拒绝后的影响\n收到正式文本后填写。",
|
||||
"termsMarketingBody": "FANUZU 营销信息接收同意\n\n这是收到正式文本前的临时文字。\n\n接收内容\n收到正式文本后填写。\n\n接收方式\n收到正式文本后填写。\n\n保存与使用期限\n收到正式文本后填写。\n\n撤回同意的方法\n收到正式文本后填写。",
|
||||
"termsPrivacyPolicyBody": "FANUZU 隐私政策\n\n这是收到正式文本前的临时文字。\n\n1. 收集的个人信息项目\n收到正式文本后填写。\n\n2. 个人信息的使用目的\n收到正式文本后填写。\n\n3. 个人信息的保存与使用期限\n收到正式文本后填写。\n\n4. 个人信息处理委托及境外转移\n收到正式文本后填写。\n\n5. 个人信息的销毁\n收到正式文本后填写。\n\n6. 用户的权利及行使方法\n收到正式文本后填写。\n\n7. 个人信息保护负责人及联系方式\n收到正式文本后填写。\n\n8. 生效日期\n收到正式文本后填写。",
|
||||
"termsLocationBody": "FANUZU 位置服务使用条款\n\n这是收到正式文本前的临时文字。\n\n使用目的\n收到正式文本后填写。\n\n收集和使用的位置信息\n收到正式文本后填写。\n\n保存与使用期限\n收到正式文本后填写。\n\n位置服务提供者信息\n收到正式文本后填写。\n\n生效日期\n收到正式文本后填写。",
|
||||
"locationConsentBody": "位置认证任务和地图上的当前位置显示将使用设备位置。不同意也可以继续使用其他功能。",
|
||||
"locationConsentAgree": "同意",
|
||||
"locationConsentLater": "以后再说",
|
||||
"locationConsentNeeded": "需同意位置服务条款才能进行位置认证。",
|
||||
"settingsLocationConsent": "位置服务使用同意",
|
||||
"authSocialCancelled": "登录未完成,请重试。",
|
||||
"authSocialNetwork": "请检查网络连接后重试。"
|
||||
}
|
||||
|
||||
@@ -76,9 +76,6 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String get authCredentialInUse => _remote['authCredentialInUse'] ?? _base.authCredentialInUse;
|
||||
|
||||
@override
|
||||
String get authDifferentEmail => _remote['authDifferentEmail'] ?? _base.authDifferentEmail;
|
||||
|
||||
@override
|
||||
String get authEmailInUse => _remote['authEmailInUse'] ?? _base.authEmailInUse;
|
||||
|
||||
@@ -151,9 +148,15 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String get authSignupHeadline => _remote['authSignupHeadline'] ?? _base.authSignupHeadline;
|
||||
|
||||
@override
|
||||
String get authSocialCancelled => _remote['authSocialCancelled'] ?? _base.authSocialCancelled;
|
||||
|
||||
@override
|
||||
String get authSocialFailed => _remote['authSocialFailed'] ?? _base.authSocialFailed;
|
||||
|
||||
@override
|
||||
String get authSocialNetwork => _remote['authSocialNetwork'] ?? _base.authSocialNetwork;
|
||||
|
||||
@override
|
||||
String get authThisAccount => _remote['authThisAccount'] ?? _base.authThisAccount;
|
||||
|
||||
@@ -1207,9 +1210,6 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String get linkNotLinked => _remote['linkNotLinked'] ?? _base.linkNotLinked;
|
||||
|
||||
@override
|
||||
String get linkSameEmailOnly => _remote['linkSameEmailOnly'] ?? _base.linkSameEmailOnly;
|
||||
|
||||
@override
|
||||
String linkTitle(String name) => _fill('linkTitle', {'name': name}) ?? _base.linkTitle(name);
|
||||
|
||||
@@ -1237,6 +1237,18 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String liveHotTime(String name) => _fill('liveHotTime', {'name': name}) ?? _base.liveHotTime(name);
|
||||
|
||||
@override
|
||||
String get locationConsentAgree => _remote['locationConsentAgree'] ?? _base.locationConsentAgree;
|
||||
|
||||
@override
|
||||
String get locationConsentBody => _remote['locationConsentBody'] ?? _base.locationConsentBody;
|
||||
|
||||
@override
|
||||
String get locationConsentLater => _remote['locationConsentLater'] ?? _base.locationConsentLater;
|
||||
|
||||
@override
|
||||
String get locationConsentNeeded => _remote['locationConsentNeeded'] ?? _base.locationConsentNeeded;
|
||||
|
||||
@override
|
||||
String get loungeFailed => _remote['loungeFailed'] ?? _base.loungeFailed;
|
||||
|
||||
@@ -2410,6 +2422,9 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String settingsLinkNotReady(String name) => _fill('settingsLinkNotReady', {'name': name}) ?? _base.settingsLinkNotReady(name);
|
||||
|
||||
@override
|
||||
String get settingsLocationConsent => _remote['settingsLocationConsent'] ?? _base.settingsLocationConsent;
|
||||
|
||||
@override
|
||||
String get settingsLogout => _remote['settingsLogout'] ?? _base.settingsLogout;
|
||||
|
||||
@@ -2431,9 +2446,6 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String get settingsPush => _remote['settingsPush'] ?? _base.settingsPush;
|
||||
|
||||
@override
|
||||
String get settingsSameEmailOnly => _remote['settingsSameEmailOnly'] ?? _base.settingsSameEmailOnly;
|
||||
|
||||
@override
|
||||
String get settingsService => _remote['settingsService'] ?? _base.settingsService;
|
||||
|
||||
@@ -2599,12 +2611,30 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String get termsLocation => _remote['termsLocation'] ?? _base.termsLocation;
|
||||
|
||||
@override
|
||||
String get termsLocationBody => _remote['termsLocationBody'] ?? _base.termsLocationBody;
|
||||
|
||||
@override
|
||||
String get termsMarketing => _remote['termsMarketing'] ?? _base.termsMarketing;
|
||||
|
||||
@override
|
||||
String get termsMarketingBody => _remote['termsMarketingBody'] ?? _base.termsMarketingBody;
|
||||
|
||||
@override
|
||||
String get termsPrivacy => _remote['termsPrivacy'] ?? _base.termsPrivacy;
|
||||
|
||||
@override
|
||||
String get termsPrivacyBody => _remote['termsPrivacyBody'] ?? _base.termsPrivacyBody;
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicy => _remote['termsPrivacyPolicy'] ?? _base.termsPrivacyPolicy;
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyBody => _remote['termsPrivacyPolicyBody'] ?? _base.termsPrivacyPolicyBody;
|
||||
|
||||
@override
|
||||
String get termsPrivacyPolicyNotice => _remote['termsPrivacyPolicyNotice'] ?? _base.termsPrivacyPolicyNotice;
|
||||
|
||||
@override
|
||||
String get termsService => _remote['termsService'] ?? _base.termsService;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user