Skip to content

Commit 0823f9c

Browse files
committed
fix: smooth social sign-in transitions
1 parent 8ae2e8e commit 0823f9c

14 files changed

Lines changed: 392 additions & 159 deletions

CONTEXT.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ _Avoid_: Tracking vendor, analytics SDK
4141
A Product Usage Event that marks completion or failure of a meaningful user workflow step.
4242
_Avoid_: Tap event, raw navigation log, interaction trace
4343

44+
**Provider Authentication Completed**:
45+
The state where the external Apple or Google account prompt has returned credentials to OnTime.
46+
_Avoid_: Login completed, signed in, session ready
47+
48+
**OnTime Session Established**:
49+
The state where OnTime has accepted provider credentials, created an app session, and can route the user into the signed-in app experience.
50+
_Avoid_: Provider login completed, credential received
51+
4452
**Analytics Event Parameter**:
4553
An allowlisted non-content value attached to a Product Usage Event.
4654
_Avoid_: Event payload, arbitrary metadata, raw detail
@@ -102,6 +110,9 @@ _Avoid_: Notification, native alarm
102110
- A **Product Usage Event** may describe a schedule, preparation, notification, alarm, onboarding, or account action without storing the user's raw schedule names, notes, place names, credentials, tokens, or free text.
103111
- First-release **Product Usage Events** are **Workflow Milestone Events**, not every tap or raw navigation step.
104112
- First-release **Workflow Milestone Events** cover analytics preference, onboarding, authentication, schedule, notification permission, alarm, and schedule-finish outcomes.
113+
- **Provider Authentication Completed** precedes **OnTime Session Established** during Apple or Google sign-in.
114+
- **Provider Authentication Completed** does not mean the user is signed in to OnTime.
115+
- The signed-in app experience begins only after **OnTime Session Established**.
105116
- A **Product Usage Event** may include **Analytics Event Parameters** such as workflow, result, stable error category, coarse count, coarse duration, platform, or app version.
106117
- An **Analytics Event Parameter** must not contain user-authored text, direct identifiers, tokens, raw exception strings, request bodies, or response bodies.
107118
- A **Product Usage Event** uses a stable snake_case name and includes a schema version.
@@ -153,6 +164,7 @@ _Avoid_: Notification, native alarm
153164
- "Third party" was ambiguous for analytics; resolved: the canonical term is **Analytics Provider**.
154165
- "Event taxonomy" was broad; resolved: first-release analytics tracks **Workflow Milestone Events** only.
155166
- "Event payload" was too open-ended; resolved: events use allowlisted **Analytics Event Parameters** only.
167+
- "Login completed" was ambiguous for Apple and Google sign-in; resolved: external account prompt completion is **Provider Authentication Completed**, while usable OnTime sign-in is **OnTime Session Established**.
156168
- "Alarm permission" was ambiguous between **Exact Timing Permission** and notification permission; resolved: notification permission may enable a **Fallback Notification**, but does not mean **Exact Timing Permission** is granted.
157169
- "Pending" was ambiguous for notification status; resolved: the canonical state is **No Scheduled Notification** when notifications are enabled but no upcoming Schedule Notification is armed.
158170
- "Allowed" was ambiguous for permission requests; resolved: a request action is not the same as granted **Exact Timing Permission**.

lib/l10n/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@
123123
"@signInSlogan": {
124124
"description": "Slogan on the sign-in screen"
125125
},
126+
"signInFailedTitle": "Sign-in failed",
127+
"@signInFailedTitle": {
128+
"description": "Dialog title shown when social sign-in fails after provider authentication"
129+
},
130+
"signInFailedDescription": "Please try again in a moment.",
131+
"@signInFailedDescription": {
132+
"description": "Dialog description shown when social sign-in fails after provider authentication"
133+
},
126134
"welcome": "Welcome!",
127135
"@welcome": {
128136
"description": "Title on the onboarding start screen"

lib/l10n/app_ko.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"finishPreparation": "준비 종료",
3131
"preparationReadyToGo": "출발 준비 완료",
3232
"signInSlogan": "당신의 잃어버린 여유를 찾아드립니다.",
33+
"signInFailedTitle": "로그인에 실패했어요",
34+
"signInFailedDescription": "잠시 후 다시 시도해 주세요.",
3335
"welcome": "반가워요!",
3436
"onboardingStartSubtitle": "Ontime과 함께 준비하기 위해서\n평소 본인의 준비 과정을 알려주세요",
3537
"start": "시작하기",

lib/l10n/app_localizations.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,18 @@ abstract class AppLocalizations {
284284
/// **'We\'ll find your lost leisure.'**
285285
String get signInSlogan;
286286

287+
/// Dialog title shown when social sign-in fails after provider authentication
288+
///
289+
/// In en, this message translates to:
290+
/// **'Sign-in failed'**
291+
String get signInFailedTitle;
292+
293+
/// Dialog description shown when social sign-in fails after provider authentication
294+
///
295+
/// In en, this message translates to:
296+
/// **'Please try again in a moment.'**
297+
String get signInFailedDescription;
298+
287299
/// Title on the onboarding start screen
288300
///
289301
/// In en, this message translates to:

lib/l10n/app_localizations_en.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ class AppLocalizationsEn extends AppLocalizations {
107107
@override
108108
String get signInSlogan => 'We\'ll find your lost leisure.';
109109

110+
@override
111+
String get signInFailedTitle => 'Sign-in failed';
112+
113+
@override
114+
String get signInFailedDescription => 'Please try again in a moment.';
115+
110116
@override
111117
String get welcome => 'Welcome!';
112118

lib/l10n/app_localizations_ko.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ class AppLocalizationsKo extends AppLocalizations {
105105
@override
106106
String get signInSlogan => '당신의 잃어버린 여유를 찾아드립니다.';
107107

108+
@override
109+
String get signInFailedTitle => '로그인에 실패했어요';
110+
111+
@override
112+
String get signInFailedDescription => '잠시 후 다시 시도해 주세요.';
113+
108114
@override
109115
String get welcome => '반가워요!';
110116

lib/presentation/login/components/google_sign_in_button/apple_sign_in_button_mobile.dart

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,19 @@
11
import 'package:flutter/material.dart' hide IconAlignment;
2-
import 'package:on_time_front/core/di/di_setup.dart';
3-
import 'package:on_time_front/core/dio/api_error_message.dart';
4-
import 'package:on_time_front/core/logging/app_logger.dart';
5-
import 'package:on_time_front/domain/repositories/user_repository.dart';
6-
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
72

83
class AppleSignInButton extends StatelessWidget {
9-
const AppleSignInButton({super.key});
4+
const AppleSignInButton({super.key, this.onPressed});
5+
6+
final VoidCallback? onPressed;
107

118
@override
129
Widget build(BuildContext context) {
13-
final UserRepository userRepository = getIt.get<UserRepository>();
14-
1510
return SizedBox(
1611
width: 358,
1712
height: 54,
1813
child: Material(
1914
color: Colors.transparent,
2015
child: InkWell(
21-
onTap: () async {
22-
try {
23-
final credential = await SignInWithApple.getAppleIDCredential(
24-
scopes: [
25-
AppleIDAuthorizationScopes.email,
26-
AppleIDAuthorizationScopes.fullName,
27-
],
28-
);
29-
30-
final fullNameRaw =
31-
'${credential.givenName ?? ''} ${credential.familyName ?? ''}'
32-
.trim();
33-
final fullName = fullNameRaw.isNotEmpty
34-
? fullNameRaw
35-
: 'Apple User';
36-
37-
final identityToken = credential.identityToken;
38-
final authorizationCode = credential.authorizationCode;
39-
if (identityToken == null) {
40-
throw Exception('Apple Sign In Failed: Missing credentials');
41-
}
42-
43-
await userRepository.signInWithApple(
44-
idToken: identityToken,
45-
authCode: authorizationCode,
46-
fullName: fullName,
47-
email: credential.email,
48-
);
49-
} catch (error, stackTrace) {
50-
AppLogger.debug(
51-
'Apple Sign-In button failed errorType=${error.runtimeType} '
52-
'message=$error stackTrace=$stackTrace',
53-
);
54-
if (!context.mounted) {
55-
return;
56-
}
57-
final message =
58-
ApiErrorMessage.fromException(error) ??
59-
'Apple 로그인에 실패했습니다. 잠시 후 다시 시도해 주세요.';
60-
ScaffoldMessenger.of(
61-
context,
62-
).showSnackBar(SnackBar(content: Text(message)));
63-
}
64-
},
16+
onTap: onPressed,
6517
borderRadius: BorderRadius.circular(14),
6618
child: Image.asset(
6719
'appleid_button.png',

lib/presentation/login/components/google_sign_in_button/google_sign_in_button_mobile.dart

Lines changed: 20 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,26 @@
11
import 'package:flutter/material.dart';
2-
import 'package:flutter_bloc/flutter_bloc.dart';
32
import 'package:flutter_svg/svg.dart';
4-
import 'package:on_time_front/core/di/di_setup.dart';
5-
import 'package:on_time_front/core/logging/app_logger.dart';
6-
import 'package:on_time_front/domain/repositories/user_repository.dart';
7-
import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart';
83

9-
class GoogleSignInButton extends StatefulWidget {
10-
const GoogleSignInButton({super.key});
4+
class GoogleSignInButton extends StatelessWidget {
5+
const GoogleSignInButton({super.key, this.onPressed});
116

12-
@override
13-
State<GoogleSignInButton> createState() => _GoogleSignInButtonState();
14-
}
15-
16-
class _GoogleSignInButtonState extends State<GoogleSignInButton> {
17-
bool _isSigningIn = false;
7+
final VoidCallback? onPressed;
188

199
@override
2010
Widget build(BuildContext context) {
21-
final UserRepository authenticationRepository = getIt.get<UserRepository>();
22-
final canSignIn = context.select<AuthBloc, bool>(
23-
(bloc) => bloc.state.status == AuthStatus.unauthenticated,
24-
);
25-
2611
return SizedBox(
2712
width: 358,
2813
height: 54,
2914
child: ElevatedButton(
30-
onPressed: !canSignIn || _isSigningIn
31-
? null
32-
: () async {
33-
setState(() {
34-
_isSigningIn = true;
35-
});
36-
try {
37-
final googleAccount = await authenticationRepository
38-
.authenticateWithGoogle();
39-
await authenticationRepository.signInWithGoogle(
40-
googleAccount,
41-
);
42-
} catch (error) {
43-
AppLogger.debug(
44-
'Google Sign-In button failed errorType=${error.runtimeType}',
45-
);
46-
} finally {
47-
if (mounted) {
48-
setState(() {
49-
_isSigningIn = false;
50-
});
51-
}
52-
}
53-
},
15+
onPressed: onPressed,
5416
style: ElevatedButton.styleFrom(
5517
backgroundColor: Colors.white,
18+
disabledBackgroundColor: Colors.white,
5619
foregroundColor: Colors.black,
20+
disabledForegroundColor: Colors.black,
5721
elevation: 1,
5822
shadowColor: Colors.black26,
23+
disabledMouseCursor: SystemMouseCursors.basic,
5924
shape: RoundedRectangleBorder(
6025
borderRadius: BorderRadius.circular(14),
6126
side: BorderSide(color: Color(0xFFDADCE0), width: 1),
@@ -74,15 +39,19 @@ class _GoogleSignInButtonState extends State<GoogleSignInButton> {
7439
height: 20,
7540
),
7641
SizedBox(width: 10),
77-
Text(
78-
'Sign in with Google',
79-
style: TextStyle(
80-
fontFamily: 'Pretendard',
81-
fontWeight: FontWeight.w600,
82-
fontSize: 21,
83-
height: 1.4,
84-
letterSpacing: 0,
85-
color: Colors.black87,
42+
Flexible(
43+
child: Text(
44+
'Sign in with Google',
45+
maxLines: 1,
46+
overflow: TextOverflow.ellipsis,
47+
style: TextStyle(
48+
fontFamily: 'Pretendard',
49+
fontWeight: FontWeight.w600,
50+
fontSize: 21,
51+
height: 1.4,
52+
letterSpacing: 0,
53+
color: Colors.black87,
54+
),
8655
),
8756
),
8857
],

lib/presentation/login/components/google_sign_in_button/google_sign_in_button_web.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import 'package:on_time_front/core/di/di_setup.dart';
77
import 'package:on_time_front/domain/repositories/user_repository.dart';
88

99
class GoogleSignInButton extends StatefulWidget {
10-
const GoogleSignInButton({super.key});
10+
const GoogleSignInButton({super.key, this.onPressed});
11+
12+
final VoidCallback? onPressed;
1113

1214
@override
1315
State<GoogleSignInButton> createState() => _GoogleSignInButtonState();

lib/presentation/login/components/google_sign_in_button/unsupported.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import 'package:flutter/widgets.dart';
22

33
class GoogleSignInButton extends StatelessWidget {
4-
const GoogleSignInButton({super.key});
4+
const GoogleSignInButton({super.key, this.onPressed});
5+
6+
final VoidCallback? onPressed;
57

68
@override
79
Widget build(BuildContext context) {

0 commit comments

Comments
 (0)