로그아웃 SDK 대기 제거 · 온보딩 국가 시트 · 프로필 머리줄
This commit is contained in:
@@ -4,6 +4,10 @@ import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.os.Bundle
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
|
||||
// Health Connect 권한 요청(health 플러그인)이 `registerForActivityResult` 를 쓰므로 FragmentActivity 여야 한다.
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
@@ -29,4 +33,25 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 국가 표시명 — 앱 언어로 OS 가 아는 이름(`Locale.getDisplayCountry`)을 OS 정렬(`Collator`) 순서로 준다.
|
||||
* 국가 이름을 앱이 번역하지 않으려는 통로다(Dart `loadCountryNames`). 결과는 `[[코드, 이름], …]`.
|
||||
*/
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "kr.co.fanuzu.app/locale")
|
||||
.setMethodCallHandler { call, result ->
|
||||
if (call.method != "countryNames") {
|
||||
result.notImplemented()
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
val locale = Locale.forLanguageTag(call.argument<String>("locale") ?: "ko")
|
||||
val collator = Collator.getInstance(locale)
|
||||
val pairs = Locale.getISOCountries()
|
||||
.map { listOf(it, Locale("", it).getDisplayCountry(locale)) }
|
||||
.sortedWith(compareBy(collator) { it[1] })
|
||||
result.success(pairs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,20 @@ import UIKit
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
// 국가 표시명 — 앱 언어로 OS 가 아는 이름을 OS 정렬 순서로 준다(Dart `loadCountryNames`). `[[코드, 이름], …]`.
|
||||
guard let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "FanuzuLocale") else { return }
|
||||
FlutterMethodChannel(name: "kr.co.fanuzu.app/locale", binaryMessenger: registrar.messenger())
|
||||
.setMethodCallHandler { call, result in
|
||||
guard call.method == "countryNames" else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
let tag = (call.arguments as? [String: Any])?["locale"] as? String ?? "ko"
|
||||
let locale = Locale(identifier: tag)
|
||||
let pairs: [[String]] = Locale.isoRegionCodes
|
||||
.compactMap { code in locale.localizedString(forRegionCode: code).map { [code, $0] } }
|
||||
.sorted { $0[1].compare($1[1], locale: locale) == .orderedAscending }
|
||||
result(pairs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -205,14 +206,42 @@ class RemoteL10nDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
AppLocalizations.delegate.isSupported(locale);
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) => SynchronousFuture(
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
unawaited(loadCountryNames(locale));
|
||||
return SynchronousFuture(
|
||||
appL10n = RemoteAppLocalizations(lookupAppLocalizations(locale), remote),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReload(RemoteL10nDelegate old) => !identical(old.remote, remote);
|
||||
}
|
||||
|
||||
/// 기기 OS 가 아는 국가 표시명(현재 앱 언어) — 코드 → 이름, **OS 정렬 순서**(Android `Collator` ·
|
||||
/// iOS `localizedCompare`)대로 들어 있다. 국가 이름을 우리가 번역하지 않으려고 플랫폼 로케일 이름을 쓴다
|
||||
/// (Android `Locale.getDisplayCountry` · iOS `Locale.localizedString(forRegionCode:)`).
|
||||
/// 앱 언어가 바뀔 때마다 [RemoteL10nDelegate] 가 다시 읽는다. 채널이 없는 곳(위젯 테스트)은 빈 맵.
|
||||
Map<String, String> platformCountryNames = const {};
|
||||
|
||||
const _localeChannel = MethodChannel('kr.co.fanuzu.app/locale');
|
||||
|
||||
Future<void> loadCountryNames(Locale locale) async {
|
||||
try {
|
||||
final pairs = await _localeChannel.invokeListMethod<List<Object?>>(
|
||||
'countryNames',
|
||||
{'locale': localeTag(locale)},
|
||||
);
|
||||
if (pairs == null) return;
|
||||
platformCountryNames = {
|
||||
for (final p in pairs)
|
||||
if (p.length == 2 && p[0] is String && p[1] is String)
|
||||
p[0]! as String: p[1]! as String,
|
||||
};
|
||||
} on Object catch (e) {
|
||||
debugPrint('[Locale] country names unavailable: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 기준 언어(ko) 문자열. `BuildContext` 가 없는 곳의 기본값으로 쓴다.
|
||||
final AppLocalizations koL10n = lookupAppLocalizations(fallbackLocale);
|
||||
|
||||
|
||||
@@ -241,13 +241,19 @@ class AuthRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// SDK 로그아웃(Google · Kakao 등)은 기다리지 않는다. 로그인 상태를 정하는 것은 Firebase 세션이고,
|
||||
// SDK 쪽은 다음 로그인의 자동 선택을 지우는 정리일 뿐이다. Android Google 은 Credential Manager
|
||||
// `clearCredentialStateAsync` 가 응답 없이 멈춰 3초 시간 제한을 다 쓰고, 그 TimeoutException 이
|
||||
// 처리되지 않은 채 로그에 남았다(10/6 실기기). 실패는 로그로만 남긴다.
|
||||
if (provider != null) {
|
||||
unawaited(
|
||||
provider.signOut().catchError((Object e) {
|
||||
debugPrint('[Auth] provider sign-out skipped: $e');
|
||||
}),
|
||||
);
|
||||
}
|
||||
try {
|
||||
await Future.wait([
|
||||
// SDK 로그아웃(Google · Kakao 등)이 응답 없이 멈춰도 로컬 로그아웃은 한다.
|
||||
if (provider != null)
|
||||
provider.signOut().timeout(const Duration(seconds: 3)),
|
||||
if (_session != null) _session.signOut(),
|
||||
], eagerError: false);
|
||||
await _session?.signOut();
|
||||
} finally {
|
||||
// 원격 SDK 정리가 실패해도 로컬 라우터는 로그인 상태에 고착되지 않는다.
|
||||
_replaceAccount(null);
|
||||
|
||||
@@ -381,6 +381,8 @@ class _ProfileStepState extends State<_ProfileStep> {
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: const Color(0xFF101010),
|
||||
// 사본 `178:23489` 상단 손잡이.
|
||||
showDragHandle: true,
|
||||
builder: (_) => _CountrySheet(current: _country),
|
||||
);
|
||||
if (picked != null && mounted) setState(() => _country = picked);
|
||||
@@ -390,9 +392,12 @@ class _ProfileStepState extends State<_ProfileStep> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final current = _locale ?? LocaleScope.maybeOf(context)?.locale;
|
||||
// Figma `511:86825` — 머리줄 「<」 좌측 · 타이틀 중앙(사본 `178:23489` 「회원가입」), 본문 큰 제목 없음.
|
||||
// 입력란은 사본처럼 테두리 상자(`511:86809`).
|
||||
return _StepScaffold(
|
||||
title: l10n.onbProfileTitle,
|
||||
barTitle: l10n.authSignup,
|
||||
onBack: widget.onBack,
|
||||
boxedInputs: true,
|
||||
action: _PrimaryButton(
|
||||
label: l10n.commonNext,
|
||||
onPressed: _valid && !_busy ? _save : null,
|
||||
@@ -475,10 +480,12 @@ class _CountrySheetState extends State<_CountrySheet> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 4, 0),
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 4, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.onbCountry, style: _title)),
|
||||
Expanded(
|
||||
child: Text(context.l10n.onbCountrySheetTitle, style: _title),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.commonClose,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
@@ -490,7 +497,7 @@ class _CountrySheetState extends State<_CountrySheet> {
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
for (final c in isoCountries)
|
||||
for (final c in sortedCountries())
|
||||
ListTile(
|
||||
onTap: () => setState(() => _picked = c),
|
||||
leading: Text(
|
||||
@@ -739,14 +746,18 @@ class _PermissionStepState extends State<_PermissionStep> {
|
||||
/// 단계 화면 틀 — 사본 가입 화면과 같은 코스믹 다크 · 하단 고정 버튼.
|
||||
class _StepScaffold extends StatelessWidget {
|
||||
const _StepScaffold({
|
||||
required this.title,
|
||||
this.title,
|
||||
required this.action,
|
||||
required this.children,
|
||||
this.subtitle,
|
||||
this.onBack,
|
||||
this.barTitle,
|
||||
this.boxedInputs = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? title;
|
||||
final String? barTitle;
|
||||
final bool boxedInputs;
|
||||
final String? subtitle;
|
||||
final Widget action;
|
||||
final List<Widget> children;
|
||||
@@ -757,6 +768,8 @@ class _StepScaffold extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: onBack == null ? null : BackButton(onPressed: onBack),
|
||||
title: barTitle == null ? null : Text(barTitle!),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
@@ -764,24 +777,51 @@ class _StepScaffold extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
child: _boxed(
|
||||
context,
|
||||
ListView(
|
||||
padding: const EdgeInsets.all(AppSpace.s4),
|
||||
children: [
|
||||
Text(title, style: _title),
|
||||
if (title != null) ...[
|
||||
Text(title!, style: _title),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: AppSpace.s2),
|
||||
Text(subtitle!, style: _body),
|
||||
],
|
||||
const SizedBox(height: AppSpace.s5),
|
||||
],
|
||||
...children,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.all(AppSpace.s4), child: action),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// 사본 가입 화면 입력란 — 회색 테두리 상자(약관 상자와 같은 선 · 모서리 8).
|
||||
Widget _boxed(BuildContext context, Widget child) {
|
||||
if (!boxedInputs) return child;
|
||||
OutlineInputBorder line(Color c, [double w = 1]) => OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
borderSide: BorderSide(color: c, width: w),
|
||||
);
|
||||
final theme = Theme.of(context);
|
||||
return Theme(
|
||||
data: theme.copyWith(
|
||||
inputDecorationTheme: theme.inputDecorationTheme.copyWith(
|
||||
border: line(_stroke),
|
||||
enabledBorder: line(_stroke),
|
||||
focusedBorder: line(AppColors.cosmicAccent, 2),
|
||||
errorBorder: line(AppColors.error),
|
||||
focusedErrorBorder: line(AppColors.error, 2),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrimaryButton extends StatelessWidget {
|
||||
@@ -803,6 +843,9 @@ class _PrimaryButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 사본 입력란 테두리 — 약관 상자(`signup_screens.dart`)와 같은 값.
|
||||
const _stroke = Color(0xFF545454);
|
||||
|
||||
const _title = TextStyle(
|
||||
fontFamily: AppFonts.body,
|
||||
fontSize: 20,
|
||||
@@ -822,6 +865,19 @@ const _body = TextStyle(
|
||||
String countryFlag(String code) =>
|
||||
String.fromCharCodes(code.codeUnits.map((c) => c - 0x41 + 0x1F1E6));
|
||||
|
||||
/// 국가 시트 순서 — 기기 OS 정렬([platformCountryNames] 순서)을 따르고, OS 가 모르는 코드는 표시명 순으로 뒤에 둔다.
|
||||
/// 사본 `178:23489` 목록은 (첫 줄 독일을 빼면) 국가명 가나다순이고 검색 칸이 없다. 범위는 Q-E10 회신 대기.
|
||||
List<String> sortedCountries() {
|
||||
final rest = isoCountries.toSet();
|
||||
final ordered = [
|
||||
for (final c in platformCountryNames.keys)
|
||||
if (rest.remove(c)) c,
|
||||
];
|
||||
return ordered..addAll(
|
||||
rest.toList()..sort((a, b) => countryName(a).compareTo(countryName(b))),
|
||||
);
|
||||
}
|
||||
|
||||
/// ISO 3166-1 alpha-2 전체. 랭킹 집계 · Admin 사용자 일람의 「거주 국가/지역」 값이다.
|
||||
final List<String> isoCountries =
|
||||
('AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ '
|
||||
|
||||
@@ -126,7 +126,15 @@ class _TermsScreenState extends State<TermsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(context.l10n.termsTitle)),
|
||||
// Figma `179:24230` 머리줄 좌측 「<」 — 「이전」과 같은 동작(온보딩 안에서는 로그아웃 후 로그인 화면).
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: BackButton(
|
||||
onPressed: widget.onBack ?? () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Text(context.l10n.termsTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
// Figma `179:24230` — 「이전 · 완료」는 목록 끝이 아니라 화면 하단 고정(56 높이 · 간격 10 · 아래 16).
|
||||
// 시스템 내비게이션 바 위로 올린다(SafeArea) — 10/6 실기기에서 첫 탭이 바에 먹혔다.
|
||||
bottomNavigationBar: SafeArea(
|
||||
|
||||
@@ -137,12 +137,15 @@ class FpiRankRow {
|
||||
final bool isMine;
|
||||
}
|
||||
|
||||
/// 국가 코드 → 표시명(ko). 없으면 코드를 그대로 띄운다.
|
||||
/// 국가 코드 → 표시명(현재 앱 언어). 기기 OS 의 국가 이름([platformCountryNames])을 먼저 쓰고,
|
||||
/// 없으면(위젯 테스트 · `ZZ` 미지정) arb 표시명, 그것도 없으면 코드를 그대로 띄운다.
|
||||
///
|
||||
/// **표시명을 아는 곳은 여기 한 곳뿐이다.** 서버 집계(`fnRankingCountryAggregate`)는
|
||||
/// 국가 코드만 적는다 — 표기 언어는 화면의 몫이고, 두 곳에 두면 서버에 없는 나라가
|
||||
/// 늘 때마다 배포가 필요해진다. 지원 국가 목록·표기 언어는 대조표 7-③ 확인 요청 중.
|
||||
/// 늘 때마다 배포가 필요해진다. 지원 국가 목록은 해석 확인 요청(Q-E10) 중.
|
||||
String countryName(String code) {
|
||||
final platform = platformCountryNames[code];
|
||||
if (platform != null && platform.isNotEmpty) return platform;
|
||||
final name = appL10n.countryName(code);
|
||||
return name == '-' ? code : name;
|
||||
}
|
||||
|
||||
@@ -856,7 +856,8 @@
|
||||
"onbIntro3Body": "Stop the Eclipse together and support\nyour artist through campaigns.",
|
||||
"onbProfileTitle": "Set up your profile",
|
||||
"onbNicknameHint": "2–20 characters · duplicates are allowed",
|
||||
"onbCountry": "Country/Region",
|
||||
"onbCountry": "Country/Region of residence",
|
||||
"onbCountrySheetTitle": "Select your country/region of residence",
|
||||
"onbLanguage": "Display language",
|
||||
"onbReferral": "Referral code (optional)",
|
||||
"onbReferralHint": "Enter a code if a friend shared one",
|
||||
|
||||
@@ -856,7 +856,8 @@
|
||||
"onbIntro3Body": "Detengan juntos el Eclipse y apoyen\na su artista con campañas.",
|
||||
"onbProfileTitle": "Configura tu perfil",
|
||||
"onbNicknameHint": "2–20 caracteres · se permiten duplicados",
|
||||
"onbCountry": "País/Región",
|
||||
"onbCountry": "País/Región de residencia",
|
||||
"onbCountrySheetTitle": "Selecciona tu país/región de residencia",
|
||||
"onbLanguage": "Idioma de visualización",
|
||||
"onbReferral": "Código de referido (opcional)",
|
||||
"onbReferralHint": "Introduce el código si un amigo te lo compartió",
|
||||
|
||||
@@ -856,7 +856,8 @@
|
||||
"onbIntro3Body": "みんなでイクリプスを防ぎ、キャンペーンで\nアーティストを応援しましょう。",
|
||||
"onbProfileTitle": "プロフィールを設定してください",
|
||||
"onbNicknameHint": "2〜20文字・他の人と同じでも大丈夫です",
|
||||
"onbCountry": "国・地域",
|
||||
"onbCountry": "居住国・地域",
|
||||
"onbCountrySheetTitle": "居住国・地域を選択",
|
||||
"onbLanguage": "表示言語",
|
||||
"onbReferral": "紹介コード(任意)",
|
||||
"onbReferralHint": "友だちからもらったコードがあれば入力してください",
|
||||
|
||||
@@ -1799,7 +1799,8 @@
|
||||
"onbIntro3Body": "이클립스를 함께 막고 캠페인으로\n아티스트를 응원하세요.",
|
||||
"onbProfileTitle": "프로필을 설정해 주세요",
|
||||
"onbNicknameHint": "2~20자 · 다른 사람과 같아도 괜찮아요",
|
||||
"onbCountry": "국가/지역",
|
||||
"onbCountry": "거주 국가/지역",
|
||||
"onbCountrySheetTitle": "거주 국가/지역 선택",
|
||||
"onbLanguage": "표시 언어",
|
||||
"onbReferral": "추천인 코드 (선택)",
|
||||
"onbReferralHint": "친구에게 받은 코드가 있으면 입력해 주세요",
|
||||
|
||||
@@ -5255,9 +5255,15 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @onbCountry.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'국가/지역'**
|
||||
/// **'거주 국가/지역'**
|
||||
String get onbCountry;
|
||||
|
||||
/// No description provided for @onbCountrySheetTitle.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'거주 국가/지역 선택'**
|
||||
String get onbCountrySheetTitle;
|
||||
|
||||
/// No description provided for @onbLanguage.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
|
||||
@@ -2991,7 +2991,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get onbNicknameHint => '2–20 characters · duplicates are allowed';
|
||||
|
||||
@override
|
||||
String get onbCountry => 'Country/Region';
|
||||
String get onbCountry => 'Country/Region of residence';
|
||||
|
||||
@override
|
||||
String get onbCountrySheetTitle => 'Select your country/region of residence';
|
||||
|
||||
@override
|
||||
String get onbLanguage => 'Display language';
|
||||
|
||||
@@ -3006,7 +3006,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get onbNicknameHint => '2–20 caracteres · se permiten duplicados';
|
||||
|
||||
@override
|
||||
String get onbCountry => 'País/Región';
|
||||
String get onbCountry => 'País/Región de residencia';
|
||||
|
||||
@override
|
||||
String get onbCountrySheetTitle => 'Selecciona tu país/región de residencia';
|
||||
|
||||
@override
|
||||
String get onbLanguage => 'Idioma de visualización';
|
||||
|
||||
@@ -2891,7 +2891,10 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get onbNicknameHint => '2〜20文字・他の人と同じでも大丈夫です';
|
||||
|
||||
@override
|
||||
String get onbCountry => '国・地域';
|
||||
String get onbCountry => '居住国・地域';
|
||||
|
||||
@override
|
||||
String get onbCountrySheetTitle => '居住国・地域を選択';
|
||||
|
||||
@override
|
||||
String get onbLanguage => '表示言語';
|
||||
|
||||
@@ -2894,7 +2894,10 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get onbNicknameHint => '2~20자 · 다른 사람과 같아도 괜찮아요';
|
||||
|
||||
@override
|
||||
String get onbCountry => '국가/지역';
|
||||
String get onbCountry => '거주 국가/지역';
|
||||
|
||||
@override
|
||||
String get onbCountrySheetTitle => '거주 국가/지역 선택';
|
||||
|
||||
@override
|
||||
String get onbLanguage => '표시 언어';
|
||||
|
||||
@@ -2886,7 +2886,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get onbNicknameHint => '2~20个字符·可与他人重复';
|
||||
|
||||
@override
|
||||
String get onbCountry => '国家/地区';
|
||||
String get onbCountry => '居住国家/地区';
|
||||
|
||||
@override
|
||||
String get onbCountrySheetTitle => '选择居住国家/地区';
|
||||
|
||||
@override
|
||||
String get onbLanguage => '显示语言';
|
||||
|
||||
@@ -856,7 +856,8 @@
|
||||
"onbIntro3Body": "一起阻止日食,通过活动\n为艺人应援。",
|
||||
"onbProfileTitle": "请设置个人资料",
|
||||
"onbNicknameHint": "2~20个字符·可与他人重复",
|
||||
"onbCountry": "国家/地区",
|
||||
"onbCountry": "居住国家/地区",
|
||||
"onbCountrySheetTitle": "选择居住国家/地区",
|
||||
"onbLanguage": "显示语言",
|
||||
"onbReferral": "推荐码(选填)",
|
||||
"onbReferralHint": "如有朋友分享的推荐码请输入",
|
||||
|
||||
@@ -1387,6 +1387,9 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String get onbCountry => _remote['onbCountry'] ?? _base.onbCountry;
|
||||
|
||||
@override
|
||||
String get onbCountrySheetTitle => _remote['onbCountrySheetTitle'] ?? _base.onbCountrySheetTitle;
|
||||
|
||||
@override
|
||||
String get onbIntro1Body => _remote['onbIntro1Body'] ?? _base.onbIntro1Body;
|
||||
|
||||
|
||||
@@ -228,7 +228,32 @@ void main() {
|
||||
releaseMode: true,
|
||||
);
|
||||
|
||||
await expectLater(auth.signOut(), throwsA(isA<StateError>()));
|
||||
// SDK 로그아웃 실패는 로그만 남기고 호출자에게 던지지 않는다(처리되지 않은 예외 방지).
|
||||
await auth.signOut();
|
||||
|
||||
expect(auth.isSignedIn, isFalse);
|
||||
expect(provider.signOutCalls, 1);
|
||||
expect(session.signOutCalls, 1);
|
||||
auth.dispose();
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('SDK 로그아웃이 응답 없이 멈춰도 기다리지 않는다', () async {
|
||||
final provider = _FakeProvider(
|
||||
SocialProvider.google,
|
||||
result: const AuthCancelled(),
|
||||
signOutHangs: true,
|
||||
);
|
||||
final session = _FakeSession(
|
||||
const AuthAccount(uid: 'google-user', provider: SocialProvider.google),
|
||||
);
|
||||
final auth = AuthRepository(
|
||||
providers: [provider],
|
||||
session: session,
|
||||
releaseMode: true,
|
||||
);
|
||||
|
||||
await auth.signOut().timeout(const Duration(milliseconds: 100));
|
||||
|
||||
expect(auth.isSignedIn, isFalse);
|
||||
expect(provider.signOutCalls, 1);
|
||||
@@ -245,6 +270,7 @@ final class _FakeProvider implements SocialAuthProvider {
|
||||
this.result,
|
||||
this.signInError,
|
||||
this.signOutError,
|
||||
this.signOutHangs = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -252,6 +278,7 @@ final class _FakeProvider implements SocialAuthProvider {
|
||||
final AuthOutcome? result;
|
||||
final Object? signInError;
|
||||
final Object? signOutError;
|
||||
final bool signOutHangs;
|
||||
int signOutCalls = 0;
|
||||
|
||||
@override
|
||||
@@ -269,6 +296,7 @@ final class _FakeProvider implements SocialAuthProvider {
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
signOutCalls += 1;
|
||||
if (signOutHangs) return Completer<void>().future;
|
||||
if (signOutError case final error?) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ import 'dart:async';
|
||||
import 'package:fancake/app.dart';
|
||||
import 'package:fancake/core/data/repositories.dart';
|
||||
import 'package:fancake/core/firebase/firebase_boot.dart';
|
||||
import 'package:fancake/core/i18n/app_locale.dart';
|
||||
import 'package:fancake/features/planet/planet_data.dart' show countryName;
|
||||
import 'package:fancake/features/auth/auth.dart';
|
||||
import 'package:fancake/features/auth/auth_repository.dart';
|
||||
import 'package:fancake/features/auth/login_screen.dart';
|
||||
import 'package:fancake/features/auth/onboarding_data.dart';
|
||||
import 'package:fancake/features/auth/onboarding_screens.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -57,7 +60,9 @@ void main() {
|
||||
expect(find.byType(NavigationBar), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('동의한 계정은 셸로 가는 동안 약관 화면이 한 프레임도 그려지지 않는다 — 10/6 콜드 스타트 깜빡임', (tester) async {
|
||||
testWidgets('동의한 계정은 셸로 가는 동안 약관 화면이 한 프레임도 그려지지 않는다 — 10/6 콜드 스타트 깜빡임', (
|
||||
tester,
|
||||
) async {
|
||||
// 서버 판정이 몇 프레임 걸리는 실기기처럼 늦춘다.
|
||||
final (auth, _) = await boot(
|
||||
tester,
|
||||
@@ -148,8 +153,11 @@ void main() {
|
||||
await tester.tap(find.text('완료'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('국가/지역').first);
|
||||
await tester.tap(
|
||||
find.ancestor(of: find.text('거주 국가/지역'), matching: find.byType(InkWell)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('거주 국가/지역 선택'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('대한민국'),
|
||||
300,
|
||||
@@ -210,7 +218,34 @@ void main() {
|
||||
await auth.signIn(SocialProvider.google, signUp: true);
|
||||
await tester.pumpAndSettle();
|
||||
expect(repo.agreed, contains('marketing'));
|
||||
expect(find.text('프로필을 설정해 주세요'), findsOneWidget);
|
||||
expect(find.text('거주 국가/지역'), findsOneWidget); // ③ 프로필
|
||||
expect(find.text('회원가입'), findsOneWidget); // 511:86825 머리줄 가운데 타이틀
|
||||
});
|
||||
|
||||
test('국가명은 OS 이름 · OS 정렬 순서 — 이름 없는 나라도 코드로 보이지 않는다', () async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
const channel = MethodChannel('kr.co.fanuzu.app/locale');
|
||||
final messenger =
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
String? asked;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
asked = (call.arguments as Map)['locale'] as String;
|
||||
return [
|
||||
['GH', '가나'],
|
||||
['AD', '안도라'],
|
||||
['KR', '대한민국'],
|
||||
];
|
||||
});
|
||||
addTearDown(() {
|
||||
messenger.setMockMethodCallHandler(channel, null);
|
||||
platformCountryNames = const {};
|
||||
});
|
||||
|
||||
await loadCountryNames(const Locale('ko'));
|
||||
expect(asked, 'ko');
|
||||
expect(countryName('AD'), '안도라');
|
||||
expect(sortedCountries().take(3), ['GH', 'AD', 'KR']);
|
||||
expect(sortedCountries().length, isoCountries.length);
|
||||
});
|
||||
|
||||
testWidgets('첫 실행은 소개 3장 — 건너뛰기를 눌러도 로그인 화면', (tester) async {
|
||||
|
||||
Reference in New Issue
Block a user