@@ -21,18 +21,25 @@ import 'package:amplify_authenticator/src/blocs/auth/auth_data.dart';
21
21
import 'package:amplify_authenticator/src/services/amplify_auth_service.dart' ;
22
22
import 'package:amplify_authenticator/src/state/auth_state.dart' ;
23
23
import 'package:amplify_flutter/amplify_flutter.dart' ;
24
+ import 'package:async/async.dart' ;
25
+ import 'package:stream_transform/stream_transform.dart' ;
24
26
25
27
part 'auth_event.dart' ;
26
28
27
29
/// {@template amplify_authenticator.state_machine_bloc}
28
30
/// Internal state machine for the authenticator. Listens to authentication events
29
31
/// and maps them to appropriate state transitions.
30
32
/// {@endtemplate}
31
- class StateMachineBloc {
33
+ class StateMachineBloc
34
+ with AWSDebuggable , AmplifyLoggerMixin
35
+ implements Closeable {
32
36
final AuthService _authService;
33
37
final bool preferPrivateSession;
34
38
final AuthenticatorStep initialStep;
35
39
40
+ @override
41
+ String get runtimeTypeName => 'StateMachineBloc' ;
42
+
36
43
/// State controller.
37
44
final StreamController <AuthState > _authStateController =
38
45
StreamController <AuthState >.broadcast ();
@@ -63,18 +70,28 @@ class StateMachineBloc {
63
70
required this .preferPrivateSession,
64
71
this .initialStep = AuthenticatorStep .signIn,
65
72
}) : _authService = authService {
66
- _subscription =
67
- _authEventStream.asyncExpand (_eventTransformer).listen ((state) {
68
- _controllerSink.add (state);
69
- _currentState = state;
70
- });
73
+ final blocStream = _authEventStream.asyncExpand (_eventTransformer);
74
+ final hubStream =
75
+ _authService.hubEvents.map (_mapHubEvent).whereType <AuthState >();
76
+ final mergedStream = StreamGroup <AuthState >()
77
+ ..add (blocStream)
78
+ ..add (hubStream)
79
+ ..close ();
80
+ _subscription = mergedStream.stream.listen (_emit);
71
81
}
72
82
73
83
/// Adds an event to the Bloc.
74
84
void add (AuthEvent event) {
75
85
_authEventController.add (event);
76
86
}
77
87
88
+ /// Emits a new state to the bloc.
89
+ void _emit (AuthState state) {
90
+ logger.debug ('Emitting next state: $state ' );
91
+ _controllerSink.add (state);
92
+ _currentState = state;
93
+ }
94
+
78
95
/// Manages exception events separate from the bloc's state.
79
96
final StreamController <AuthenticatorException > _exceptionController =
80
97
StreamController <AuthenticatorException >.broadcast ();
@@ -119,6 +136,27 @@ class StateMachineBloc {
119
136
}
120
137
}
121
138
139
+ /// Listens for asynchronous events which occurred outside the control of the
140
+ /// [Authenticator] and [StateMachineBloc] .
141
+ AuthState ? _mapHubEvent (AuthHubEvent event) {
142
+ logger.debug ('Handling hub event: ${event .type }' );
143
+ switch (event.type) {
144
+ case AuthHubEventType .signedIn:
145
+ if (_currentState is ! AuthenticatedState ) {
146
+ return const AuthenticatedState ();
147
+ }
148
+ break ;
149
+ case AuthHubEventType .signedOut:
150
+ case AuthHubEventType .sessionExpired:
151
+ case AuthHubEventType .userDeleted:
152
+ if (_currentState is AuthenticatedState ) {
153
+ return UnauthenticatedState (step: initialStep);
154
+ }
155
+ break ;
156
+ }
157
+ return null ;
158
+ }
159
+
122
160
Stream <AuthState > _authLoad () async * {
123
161
yield const LoadingState ();
124
162
await Amplify .asyncConfig;
@@ -246,62 +284,77 @@ class StateMachineBloc {
246
284
_infoMessageController.add (MessageResolverKey .codeSent (destination));
247
285
}
248
286
287
+ Future <void > _processSignInResult (
288
+ SignInResult result, {
289
+ required bool isSocialSignIn,
290
+ }) async {
291
+ switch (result.nextStep! .signInStep) {
292
+ case 'CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE' :
293
+ _notifyCodeSent (result.nextStep? .codeDeliveryDetails? .destination);
294
+ _emit (UnauthenticatedState .confirmSignInMfa);
295
+ break ;
296
+ case 'CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE' :
297
+ _emit (ConfirmSignInCustom (
298
+ publicParameters:
299
+ result.nextStep? .additionalInfo ?? < String , String > {},
300
+ ));
301
+ break ;
302
+ case 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD' :
303
+ _emit (UnauthenticatedState .confirmSignInNewPassword);
304
+ break ;
305
+ case 'RESET_PASSWORD' :
306
+ _emit (UnauthenticatedState .confirmResetPassword);
307
+ break ;
308
+ case 'CONFIRM_SIGN_UP' :
309
+ _notifyCodeSent (result.nextStep? .codeDeliveryDetails? .destination);
310
+ _emit (UnauthenticatedState .confirmSignUp);
311
+ break ;
312
+ case 'DONE' :
313
+ if (isSocialSignIn) {
314
+ _emit (const AuthenticatedState ());
315
+ } else {
316
+ await for (final state in _checkUserVerification ()) {
317
+ _emit (state);
318
+ }
319
+ }
320
+ break ;
321
+ default :
322
+ break ;
323
+ }
324
+ }
325
+
249
326
Stream <AuthState > _signIn (AuthSignInData data) async * {
250
327
try {
251
328
// Make sure no user is signed in before calling the sign in method
252
329
if (await _authService.isLoggedIn) {
253
330
await _authService.signOut ();
254
331
}
255
332
256
- final SignInResult result;
257
-
258
- final bool isSocialSignIn = data is AuthSocialSignInData ;
259
-
260
333
if (data is AuthUsernamePasswordSignInData ) {
261
- result = await _authService.signIn (
334
+ final result = await _authService.signIn (
262
335
data.username,
263
336
data.password,
264
337
);
338
+ await _processSignInResult (result, isSocialSignIn: false );
265
339
} else if (data is AuthSocialSignInData ) {
266
- result = await _authService.signInWithProvider (
267
- data.provider,
268
- preferPrivateSession: preferPrivateSession,
269
- );
340
+ // Do not await a social sign-in since multiple sign-in attempts
341
+ // can occur.
342
+ _authService
343
+ .signInWithProvider (
344
+ data.provider,
345
+ preferPrivateSession: preferPrivateSession,
346
+ )
347
+ .then (
348
+ (result) => _processSignInResult (result, isSocialSignIn: true ),
349
+ )
350
+ .onError <Exception >((error, stackTrace) {
351
+ final log =
352
+ error is UserCancelledException ? logger.info : logger.error;
353
+ log ('Error signing in' , error, stackTrace);
354
+ });
270
355
} else {
271
356
throw StateError ('Bad sign in data: $data ' );
272
357
}
273
-
274
- switch (result.nextStep! .signInStep) {
275
- case 'CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE' :
276
- _notifyCodeSent (result.nextStep? .codeDeliveryDetails? .destination);
277
- yield UnauthenticatedState .confirmSignInMfa;
278
- break ;
279
- case 'CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE' :
280
- yield ConfirmSignInCustom (
281
- publicParameters:
282
- result.nextStep? .additionalInfo ?? < String , String > {},
283
- );
284
- break ;
285
- case 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD' :
286
- yield UnauthenticatedState .confirmSignInNewPassword;
287
- break ;
288
- case 'RESET_PASSWORD' :
289
- yield UnauthenticatedState .confirmResetPassword;
290
- break ;
291
- case 'CONFIRM_SIGN_UP' :
292
- _notifyCodeSent (result.nextStep? .codeDeliveryDetails? .destination);
293
- yield UnauthenticatedState .confirmSignUp;
294
- break ;
295
- case 'DONE' :
296
- if (isSocialSignIn) {
297
- yield const AuthenticatedState ();
298
- } else {
299
- yield * _checkUserVerification ();
300
- }
301
- break ;
302
- default :
303
- break ;
304
- }
305
358
} on UserNotConfirmedException catch (e) {
306
359
_exceptionController.add (AuthenticatorException (
307
360
e.message ?? 'An unknown error occurred' ,
@@ -446,7 +499,8 @@ class StateMachineBloc {
446
499
yield * const Stream .empty ();
447
500
}
448
501
449
- Future <void > dispose () {
502
+ @override
503
+ Future <void > close () {
450
504
return Future .wait <void >([
451
505
_subscription.cancel (),
452
506
_authStateController.close (),
0 commit comments