Skip to content

Commit

Permalink
Merge pull request #361 from CollActionteam/feat/app-355/force-signin
Browse files Browse the repository at this point in the history
Feat/app 355/force signin
  • Loading branch information
Xazin authored Feb 28, 2023
2 parents 5853ae2 + 2b68a14 commit cbdff0d
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 76 deletions.
Binary file added assets/images/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/unauthenticated_bg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions lib/application/auth/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
String? _phone;
StreamSubscription<Either<AuthFailure, AuthSuccess>>?
_verifyStreamSubscription;
StreamSubscription? _authenticationStateSubscription;

AuthBloc(
this._authRepository,
Expand All @@ -34,6 +35,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
on<AuthEvent>(
(event, emit) async {
await event.map(
initial: (event) async => await _mapObserveUserToState(emit, event),
verifyPhone: (event) async =>
await _mapVerifyPhoneToState(emit, event),
signInWithPhone: (event) async =>
Expand All @@ -51,6 +53,18 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
);
}

Future<void> _mapObserveUserToState(
Emitter<AuthState> emit,
_InitialEvent event,
) async {
_authenticationStateSubscription =
_authRepository.observeUser().listen((event) {
if (event is User && event.isAnonymous) {
emit(const AuthState.unauthenticated());
}
});
}

FutureOr<void> _mapResendCodeToState(
Emitter<AuthState> emit,
_ResendCode event,
Expand Down Expand Up @@ -181,6 +195,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
@override
Future<void> close() async {
_verifyStreamSubscription?.cancel();
_authenticationStateSubscription?.cancel();
super.close();
}
}
2 changes: 2 additions & 0 deletions lib/application/auth/auth_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ part of 'auth_bloc.dart';

@freezed
class AuthEvent with _$AuthEvent {
const factory AuthEvent.initial() = _InitialEvent;

const factory AuthEvent.verifyPhone(String phoneNumber) = _VerifyPhone;

const factory AuthEvent.updated(
Expand Down
2 changes: 1 addition & 1 deletion lib/domain/auth/i_auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'auth_failures.dart';
import 'auth_success.dart';

abstract class IAuthRepository {
Stream<User> observeUser();
Stream<User?> observeUser();

/// TODO - Choose to either observe or getSignedInUser
/// Returns [User] if already authenticated
Expand Down
4 changes: 2 additions & 2 deletions lib/infrastructure/auth/firebase_auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import 'firebase_auth_mapper.dart';
@LazySingleton(as: IAuthRepository)
class FirebaseAuthRepository implements IAuthRepository, Disposable {
final firebase_auth.FirebaseAuth firebaseAuth;
final _userSubject = BehaviorSubject<User>.seeded(User.anonymous);
final _userSubject = BehaviorSubject<User?>();
late final StreamSubscription _userStreamSubscription;

FirebaseAuthRepository({required this.firebaseAuth}) {
Expand Down Expand Up @@ -210,7 +210,7 @@ class FirebaseAuthRepository implements IAuthRepository, Disposable {
}

@override
Stream<User> observeUser() => _userSubject.stream.distinct();
Stream<User?> observeUser() => _userSubject.stream.distinct();

@override
FutureOr onDispose() {
Expand Down
105 changes: 105 additions & 0 deletions lib/presentation/auth/unauthenticated_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

import '../routes/app_routes.gr.dart';
import '../shared_widgets/pill_button.dart';
import '../themes/constants.dart';
import '../../domain/core/i_settings_repository.dart';
import '../../infrastructure/core/injection.dart';

class UnauthenticatedPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
checkAndMaybeShowOnboarding(context);
});

return Scaffold(
backgroundColor: kAccentColor,
body: Stack(
children: [
Container(
height: MediaQuery.of(context).size.height * .8,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 0,
right: 0,
left: 23,
child: Image.asset(
"assets/images/unauthenticated_bg.png",
fit: BoxFit.contain,
),
),
Align(
alignment: Alignment.center,
child: Image.asset(
"assets/images/logo.png",
width: 200,
),
),
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Start acting",
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 28.0,
color: Colors.white,
),
),
Padding(
padding: const EdgeInsets.only(
bottom: 64.0, left: 16, right: 16, top: 8),
child: Text(
"We are ready, join us and others in taking action for a better life, by doing good and having fun!",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
),
),
),
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(
bottom: 100.0, left: 20, right: 20),
child: PillButton(
text: 'Log In',
isEnabled: true,
isLoading: false,
onTap: () => context.router.push(const AuthRoute()),
lightBackground: true,
),
),
),
],
),
],
),
),
),
],
),
);
}

Future<void> checkAndMaybeShowOnboarding(BuildContext context) async {
// Push onboarding screen if first time launching application
final settingsRepository = getIt<ISettingsRepository>();
if (!(await settingsRepository.getWasUserOnboarded())) {
context.router.push(const OnboardingRoute());
}
}
}
8 changes: 7 additions & 1 deletion lib/presentation/core/app_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class AppWidget extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(
create: (_) => getIt<AuthBloc>(),
create: (_) => getIt<AuthBloc>()..add(AuthEvent.initial()),
),
BlocProvider<ProfileBloc>(
create: (_) => getIt<ProfileBloc>()..add(GetUserProfile()),
Expand All @@ -29,6 +29,12 @@ class AppWidget extends StatelessWidget {
listener: (context, state) {
BlocProvider.of<ProfileBloc>(context).add(GetUserProfile());
BlocProvider.of<ProfileTabBloc>(context).add(FetchProfileTabInfo());

state.whenOrNull(
unauthenticated: () {
_appRouter.replaceAll([const UnauthenticatedRoute()]);
},
);
},
child: MaterialApp.router(
color: Colors.white,
Expand Down
27 changes: 1 addition & 26 deletions lib/presentation/home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import '../../../presentation/themes/constants.dart';
import '../../domain/core/i_settings_repository.dart';
import '../../infrastructure/core/injection.dart';
import '../core/collaction_icons.dart';
import '../routes/app_routes.gr.dart';

class HomePage extends StatefulWidget {
const HomePage({super.key});

@override
HomePageState createState() => HomePageState();
}

class HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
checkAndMaybeShowOnboarding();
});
}

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AutoTabsScaffold(
Expand All @@ -38,14 +21,6 @@ class HomePageState extends State<HomePage> {
);
}

Future<void> checkAndMaybeShowOnboarding() async {
// Push onboarding screen if first time launching application
final settingsRepository = getIt<ISettingsRepository>();
if (!(await settingsRepository.getWasUserOnboarded())) {
context.router.push(const OnboardingRoute());
}
}

Widget bottomNavbar(TabsRouter tabsRouter) {
return BottomNavigationBar(
backgroundColor: Colors.white,
Expand Down
2 changes: 2 additions & 0 deletions lib/presentation/routes/app_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:auto_route/empty_router_widgets.dart';

import '../../../presentation/profile/profile_screen.dart';
import '../auth/unauthenticated_screen.dart';
import '../auth/auth_screen.dart';
import '../auth/widgets/verified.dart';
import '../contact_form/contact_form_screen.dart';
Expand Down Expand Up @@ -74,6 +75,7 @@ import '../shared_widgets/web_view_page.dart';
AutoRoute(path: 'settings-layout', page: SettingsLayout),
AutoRoute(path: 'contact-form', page: ContactFormPage),
AutoRoute(path: 'webview', page: WebViewPage),
AutoRoute(path: 'unauthenticated', page: UnauthenticatedPage)
],
)
class $AppRouter {}
55 changes: 30 additions & 25 deletions lib/presentation/shared_widgets/pill_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,29 @@ class PillButton extends StatelessWidget {
final EdgeInsets? margin;
final double? width;
final bool isLoading;
final bool lightBackground;

const PillButton({
super.key,
required this.text,
this.leading,
this.onTap,
this.isEnabled = true,
this.margin,
this.width,
this.isLoading = false,
});
const PillButton(
{super.key,
required this.text,
this.leading,
this.onTap,
this.isEnabled = true,
this.margin,
this.width,
this.isLoading = false,
this.lightBackground = false});

const PillButton.icon({
super.key,
required this.text,
required this.leading,
this.onTap,
this.isEnabled = true,
this.margin,
this.width,
this.isLoading = false,
});
const PillButton.icon(
{super.key,
required this.text,
required this.leading,
this.onTap,
this.isEnabled = true,
this.margin,
this.width,
this.isLoading = false,
this.lightBackground = false});

@override
Widget build(BuildContext context) {
Expand All @@ -48,26 +49,30 @@ class PillButton extends StatelessWidget {
? ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: kAccentColor,
backgroundColor:
lightBackground ? Colors.white : kAccentColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(52),
),
),
child: const CircularProgressIndicator(
color: Colors.white,
child: CircularProgressIndicator(
color: lightBackground ? kAccentColor : Colors.white,
),
)
: ElevatedButton.icon(
icon: leading ?? const SizedBox(),
label: Text(text),
label: Text(text,
style: TextStyle(
color: lightBackground ? kAccentColor : Colors.white,
)),
onPressed: isEnabled ? onTap : null,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(MaterialState.disabled)) {
return kAlmostTransparent;
}
return kAccentColor;
return lightBackground ? Colors.white : kAccentColor;
},
),
elevation: MaterialStateProperty.all<double>(0),
Expand Down
16 changes: 14 additions & 2 deletions test/application/auth/auth_bloc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ class MockAvatarRepository extends Mock implements IAvatarRepository {}
void main() {
group('Authentication BLoC', () {
test('Initial auth state', () {
final bloc = AuthBloc(MockAuthRepository(), MockAvatarRepository());
final userRepository = MockAuthRepository();

when(() => userRepository.observeUser())
.thenAnswer((_) => Stream.empty());

final bloc = AuthBloc(userRepository, MockAvatarRepository());
expect(bloc.state, const AuthState.initial());
});

Expand All @@ -31,6 +36,9 @@ void main() {
final avatarRepository = MockAvatarRepository();

{
when(() => userRepository.observeUser())
.thenAnswer((_) => Stream.empty());

when(
() => userRepository.verifyPhone(
phoneNumber: any(named: 'phoneNumber'),
Expand Down Expand Up @@ -84,6 +92,10 @@ void main() {

{
final userRepository = MockAuthRepository();

when(() => userRepository.observeUser())
.thenAnswer((_) => Stream.empty());

const error = AuthFailure.verificationFailed();
when(
() => userRepository.verifyPhone(
Expand All @@ -106,7 +118,7 @@ void main() {

blocTest(
'Reset to initial auth state',
build: () => AuthBloc(MockAuthRepository(), MockAvatarRepository()),
build: () => AuthBloc(userRepository, MockAvatarRepository()),
act: (AuthBloc bloc) {
bloc.add(const AuthEvent.reset());
},
Expand Down
Loading

0 comments on commit cbdff0d

Please sign in to comment.