Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ _Avoid_: Tracking vendor, analytics SDK
A Product Usage Event that marks completion or failure of a meaningful user workflow step.
_Avoid_: Tap event, raw navigation log, interaction trace

**Provider Authentication Completed**:
The state where the external Apple or Google account prompt has returned credentials to OnTime.
_Avoid_: Login completed, signed in, session ready

**OnTime Session Established**:
The state where OnTime has accepted provider credentials, created an app session, and can route the user into the signed-in app experience.
_Avoid_: Provider login completed, credential received

**Analytics Event Parameter**:
An allowlisted non-content value attached to a Product Usage Event.
_Avoid_: Event payload, arbitrary metadata, raw detail
Expand Down Expand Up @@ -102,6 +110,9 @@ _Avoid_: Notification, native alarm
- 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.
- First-release **Product Usage Events** are **Workflow Milestone Events**, not every tap or raw navigation step.
- First-release **Workflow Milestone Events** cover analytics preference, onboarding, authentication, schedule, notification permission, alarm, and schedule-finish outcomes.
- **Provider Authentication Completed** precedes **OnTime Session Established** during Apple or Google sign-in.
- **Provider Authentication Completed** does not mean the user is signed in to OnTime.
- The signed-in app experience begins only after **OnTime Session Established**.
- A **Product Usage Event** may include **Analytics Event Parameters** such as workflow, result, stable error category, coarse count, coarse duration, platform, or app version.
- An **Analytics Event Parameter** must not contain user-authored text, direct identifiers, tokens, raw exception strings, request bodies, or response bodies.
- A **Product Usage Event** uses a stable snake_case name and includes a schema version.
Expand Down Expand Up @@ -153,6 +164,7 @@ _Avoid_: Notification, native alarm
- "Third party" was ambiguous for analytics; resolved: the canonical term is **Analytics Provider**.
- "Event taxonomy" was broad; resolved: first-release analytics tracks **Workflow Milestone Events** only.
- "Event payload" was too open-ended; resolved: events use allowlisted **Analytics Event Parameters** only.
- "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**.
- "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.
- "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.
- "Allowed" was ambiguous for permission requests; resolved: a request action is not the same as granted **Exact Timing Permission**.
Expand Down
8 changes: 8 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@
"@signInSlogan": {
"description": "Slogan on the sign-in screen"
},
"signInFailedTitle": "Sign-in failed",
"@signInFailedTitle": {
"description": "Dialog title shown when social sign-in fails after provider authentication"
},
"signInFailedDescription": "Please try again in a moment.",
"@signInFailedDescription": {
"description": "Dialog description shown when social sign-in fails after provider authentication"
},
"welcome": "Welcome!",
"@welcome": {
"description": "Title on the onboarding start screen"
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/app_ko.arb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"finishPreparation": "준비 종료",
"preparationReadyToGo": "출발 준비 완료",
"signInSlogan": "당신의 잃어버린 여유를 찾아드립니다.",
"signInFailedTitle": "로그인에 실패했어요",
"signInFailedDescription": "잠시 후 다시 시도해 주세요.",
"welcome": "반가워요!",
"onboardingStartSubtitle": "Ontime과 함께 준비하기 위해서\n평소 본인의 준비 과정을 알려주세요",
"start": "시작하기",
Expand Down
12 changes: 12 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,18 @@ abstract class AppLocalizations {
/// **'We\'ll find your lost leisure.'**
String get signInSlogan;

/// Dialog title shown when social sign-in fails after provider authentication
///
/// In en, this message translates to:
/// **'Sign-in failed'**
String get signInFailedTitle;

/// Dialog description shown when social sign-in fails after provider authentication
///
/// In en, this message translates to:
/// **'Please try again in a moment.'**
String get signInFailedDescription;

/// Title on the onboarding start screen
///
/// In en, this message translates to:
Expand Down
6 changes: 6 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get signInSlogan => 'We\'ll find your lost leisure.';

@override
String get signInFailedTitle => 'Sign-in failed';

@override
String get signInFailedDescription => 'Please try again in a moment.';

@override
String get welcome => 'Welcome!';

Expand Down
6 changes: 6 additions & 0 deletions lib/l10n/app_localizations_ko.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get signInSlogan => '당신의 잃어버린 여유를 찾아드립니다.';

@override
String get signInFailedTitle => '로그인에 실패했어요';

@override
String get signInFailedDescription => '잠시 후 다시 시도해 주세요.';

@override
String get welcome => '반가워요!';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,67 +1,19 @@
import 'package:flutter/material.dart' hide IconAlignment;
import 'package:on_time_front/core/di/di_setup.dart';
import 'package:on_time_front/core/dio/api_error_message.dart';
import 'package:on_time_front/core/logging/app_logger.dart';
import 'package:on_time_front/domain/repositories/user_repository.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

class AppleSignInButton extends StatelessWidget {
const AppleSignInButton({super.key});
const AppleSignInButton({super.key, this.onPressed});

final VoidCallback? onPressed;

@override
Widget build(BuildContext context) {
final UserRepository userRepository = getIt.get<UserRepository>();

return SizedBox(
width: 358,
height: 54,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
try {
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);

final fullNameRaw =
'${credential.givenName ?? ''} ${credential.familyName ?? ''}'
.trim();
final fullName = fullNameRaw.isNotEmpty
? fullNameRaw
: 'Apple User';

final identityToken = credential.identityToken;
final authorizationCode = credential.authorizationCode;
if (identityToken == null) {
throw Exception('Apple Sign In Failed: Missing credentials');
}

await userRepository.signInWithApple(
idToken: identityToken,
authCode: authorizationCode,
fullName: fullName,
email: credential.email,
);
} catch (error, stackTrace) {
AppLogger.debug(
'Apple Sign-In button failed errorType=${error.runtimeType} '
'message=$error stackTrace=$stackTrace',
);
if (!context.mounted) {
return;
}
final message =
ApiErrorMessage.fromException(error) ??
'Apple 로그인에 실패했습니다. 잠시 후 다시 시도해 주세요.';
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
},
onTap: onPressed,
borderRadius: BorderRadius.circular(14),
child: Image.asset(
'appleid_button.png',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:on_time_front/core/di/di_setup.dart';
import 'package:on_time_front/core/logging/app_logger.dart';
import 'package:on_time_front/domain/repositories/user_repository.dart';
import 'package:on_time_front/presentation/app/bloc/auth/auth_bloc.dart';

class GoogleSignInButton extends StatefulWidget {
const GoogleSignInButton({super.key});
class GoogleSignInButton extends StatelessWidget {
const GoogleSignInButton({super.key, this.onPressed});

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

class _GoogleSignInButtonState extends State<GoogleSignInButton> {
bool _isSigningIn = false;
final VoidCallback? onPressed;

@override
Widget build(BuildContext context) {
final UserRepository authenticationRepository = getIt.get<UserRepository>();
final canSignIn = context.select<AuthBloc, bool>(
(bloc) => bloc.state.status == AuthStatus.unauthenticated,
);

return SizedBox(
width: 358,
height: 54,
child: ElevatedButton(
onPressed: !canSignIn || _isSigningIn
? null
: () async {
setState(() {
_isSigningIn = true;
});
try {
final googleAccount = await authenticationRepository
.authenticateWithGoogle();
await authenticationRepository.signInWithGoogle(
googleAccount,
);
} catch (error) {
AppLogger.debug(
'Google Sign-In button failed errorType=${error.runtimeType}',
);
} finally {
if (mounted) {
setState(() {
_isSigningIn = false;
});
}
}
},
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
disabledBackgroundColor: Colors.white,
foregroundColor: Colors.black,
disabledForegroundColor: Colors.black,
elevation: 1,
shadowColor: Colors.black26,
disabledMouseCursor: SystemMouseCursors.basic,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(color: Color(0xFFDADCE0), width: 1),
Expand All @@ -74,15 +39,19 @@ class _GoogleSignInButtonState extends State<GoogleSignInButton> {
height: 20,
),
SizedBox(width: 10),
Text(
'Sign in with Google',
style: TextStyle(
fontFamily: 'Pretendard',
fontWeight: FontWeight.w600,
fontSize: 21,
height: 1.4,
letterSpacing: 0,
color: Colors.black87,
Flexible(
child: Text(
'Sign in with Google',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: 'Pretendard',
fontWeight: FontWeight.w600,
fontSize: 21,
height: 1.4,
letterSpacing: 0,
color: Colors.black87,
),
),
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import 'package:on_time_front/core/di/di_setup.dart';
import 'package:on_time_front/domain/repositories/user_repository.dart';

class GoogleSignInButton extends StatefulWidget {
const GoogleSignInButton({super.key});
const GoogleSignInButton({super.key, this.onPressed});

final VoidCallback? onPressed;

@override
State<GoogleSignInButton> createState() => _GoogleSignInButtonState();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:flutter/widgets.dart';

class GoogleSignInButton extends StatelessWidget {
const GoogleSignInButton({super.key});
const GoogleSignInButton({super.key, this.onPressed});

final VoidCallback? onPressed;

@override
Widget build(BuildContext context) {
Expand Down
Loading
Loading