smf_sign_in 0.5.0
smf_sign_in: ^0.5.0 copied to clipboard
The SMF module of the sign-in screens of the Flutter app that SMF generates: sign-in, sign-up, password reset and an account screen for an email address, with BLoC or Riverpod.
smf_sign_in #
The SMF module of the sign-in screens of the app. It is a feature: four screens with their routes, which the module that provides the router renders, two guards of the routes, which that module asks, and an entry of the settings screen. On three of the screens, a user signs in to an account with an email address and a password, creates an account, and asks for the message that resets a password. The fourth is the screen of the account, where the user signs out or deletes the account.
The module keeps no accounts. It requires a module that provides sign-in, and reaches it only through appSession, the session of the app, so the screens are the same whichever module that is.
The screens #
The sign-in is at /sign_in. It has a field for the email address and one for the password, Forgot password?, the button that signs in, and Create account.
Create account leads to the sign-up at /sign_in/sign_up, with the same two fields for a new account. There the device may suggest a password and offer to keep it.
Forgot password? leads to the password reset at /sign_in/reset_password, which asks for the email address of an account. Once the message with the link is sent, the screen shows the address that it went to. It says the same whether the address has an account or not, so nobody learns from the app which addresses have one.
Each screen shows the cell of the app, with the symbol that SMF makes from its name, Ma for my_app, next to the cells of a table that light up one after another. Below it are a title, a line of text and the form. A field has its label above it, and the password field has a button that shows the password. The button that submits is as wide as the form. While its call is on its way, it spins and the form takes no input: a field takes no typing, though one that has the focus keeps it, and the actions of the screen take neither a tap nor a key. A failure shows above the button, in a text of its own for each reason, such as a wrong password or no connection.
The fields, the buttons and the cards of the screens are those of the theme of the app, in its light and in its dark mode. The screens give a field no colour, border or padding of their own, so for another look of the fields, change the theme of the app. The cells of the picture, which a screen draws itself, take their colours from the theme too.
One button has a colour of its own. The action that leads to the other screen is Create account on the sign-in and I already have an account on the sign-up. It is a text button of the theme in the accent of the colour scheme, colorScheme.secondary, because a theme keeps its text buttons quiet and this one is the second way on from its screen. Forgot password? stays as the theme has it.
The parts of a screen rise in one after another, once. On a device that asks for less motion, they are there at once, and a busy button is disabled and keeps its label. A screen scrolls when it is too small for what it shows, as with the keyboard open or with a large text size on a small phone, and the labels and the buttons grow with their texts. A title grows only by half. A title and the label of a button break between their words and never inside one. Where the longest word would not fit, as on a narrow phone with a large text size, the text is only as large as that word allows. The letters in the cell keep their size, since they are part of a picture. A screen reader announces the title as a header, each field by its label, and a failure when it appears.
The account #
The screen of the account is at /sign_in/account. It shows the email address of the account that the user is signed in to, the button Sign out, and the action Delete account.
Delete account is a text button in the colour that the theme has for an error. It asks first, in a sheet over the page: the sheet says that the deletion cannot be undone, and has the buttons Delete and Cancel. The screen closes the sheet before it deletes the account. A provider of sign-in may refuse to delete an account whose user signed in long ago. The screen then stays, with a text that tells the user to sign out and sign in again.
The settings screen of the app has an entry for the account. It shows the email address of the account, or Not signed in, and a tap on it opens the screen of the account over the settings screen. The route of that screen needs an account. So for a user without one, in an app that everyone may use, the router opens the sign-in over the settings screen first, and shows the screen of the account once the user is signed in. In an app that signs in anonymous users, such a user creates the account on the sign-up and keeps everything that the anonymous user had.
The entry is within reach where the settings screen is. A module that provides the layout role, the main navigation of the app, shows the settings screen among its destinations. In an app without a main navigation, no screen leads to the settings screen, and smf create warns of that unless the app starts on it. A screen of yours then opens it through the navigation of the app, with context.nav.settings.settings().push<void>() for the settings screen of the settings module, or opens the account directly with context.nav.signIn.account().push<void>().
The screen neither closes itself nor navigates. After a sign-out or a deletion the session has no account, and the router closes the page: the user is on the settings screen again, or on the sign-in in an app that nobody may use without an account. The same happens when the session ends elsewhere, as when the account is deleted on another device. An open sheet goes with the page.
Look and state #
The module generates its files in lib/features/sign_in/. The look of the screens is the same in every app:
sign_in_view.dart,sign_up_view.dartandreset_password_view.darthaveSignInView,SignUpViewandResetPasswordView. A view is a plain Flutter widget: it keeps its form, gets the state of its call, and takes callbacks for what the user asks for.account_view.darthasAccountView, the look of the screen of the account, andconfirmDeleteAccount(), which asks in the sheet.account_setting_row.darthasAccountSettingRow, the row of the entry on the settings screen.sign_in_page.darthasAuthPage, the frame of a screen: the cell, the title, the line of text, and the entrance.sign_in_widgets.darthas the fields, the button that submits,OtherScreenActionfor the way to the other screen,DestructiveActionfor what cannot be undone, andFailureMessage. It also hasauthFailureText(), which has a text for every reason of a failure, andWholeWords, the text of a title or of the label of a button, which keeps its words whole.sign_in_state.darthasAuthActionState,ResetPasswordStateandAccountState, the values of the state of a screen.sign_in_guards.darthas the functions of the two guards.
What keeps the state of a screen depends on the module that manages state in the app, so the module has a variant for each:
- With
bloc, each screen has a cubit:SignInCubit,SignUpCubit,ResetPasswordCubitandAccountCubit. A screen provides its cubit and builds its view from the state.sign_in_composition.dartcreates the cubits withappSession. - With
riverpod, each screen has a provider that lasts as long as the screen:signInProvider,signUpProvider,resetPasswordProviderandaccountProvider. They reach the session throughappSessionProviderinsession_provider.dart.
The screens are SignInScreen, SignUpScreen, ResetPasswordScreen and AccountScreen, in sign_in_screen.dart, sign_up_screen.dart, reset_password_screen.dart and account_screen.dart in both variants. A screen only gives its view the state. AccountSetting in account_setting.dart is the entry of the settings screen. The module adds the package of the state manager to the app, in the version that the module of that state manager asks for, and no other package.
While a call is on its way, the state of a screen is busy and takes no second call. A failure ends that, and the state has the failure. A sign-in or a sign-up that succeeded leaves the state busy for as long as the session has the account: the user is signed in then, and the router takes the screen away. When the session has no account again before that, as when code of the app signs the user out at once, the form is back. For the screen of the account it is the reverse. After a sign-out or a deletion that succeeded, the state is busy for as long as the session has no account, and the screen is back when the session has one again.
With bloc, each page of a screen has a cubit of its own. With riverpod, a provider is one for the app, so two pages of one screen that are open at once show the same state.
Who uses the app, in a widget #
The module also gives the widgets of the app the session, so that a widget rebuilds when the user signs in or out:
- With
bloc,SessionCubitinsession_cubit.dartfollowsappSession, and the module puts it around the root of the app. A widget readscontext.watch<SessionCubit>().state. - With
riverpod,sessionProviderinsession_provider.dartfollows it. A widget readsref.watch(sessionProvider).
Both give an AppSession: nobody, an anonymous user, or the user of an account with its email address. The entry of the settings screen reads the session this way.
What shows the sign-in #
The screens neither close themselves nor navigate once the user is signed in. The module declares two guards of the routes, and the router shows the sign-in and leaves it by what they say:
sign_in.gatereadsappSession.allowsApp. In an app that nobody may use without an account, the router shows the sign-in in place of every other screen until the user is signed in.sign_in.hasAccountreadsappSession.hasAccount. In an app that everyone may use, a request for a route that needs an account, such as the screen of the account, opens the sign-in over the page that the user is on, and the rest of the app stays open. Back returns to that page. Once the user is signed in, the router closes the sign-in and shows the route that was asked for. When the user has no account any more, the router closes the pages that need one.
Which of the two keeps a user out is up to the sign-in mode of the app, which smf create asks for. The code of the module is the same in every mode.
In an app that everyone may use, a screen may also offer to sign in. It opens the sign-in with context.nav.signIn.signIn().push<void>(), so that back leads back. In an app that needs an account, no code navigates to the sign-in: the gate shows it.
A link to one of the three routes shows its screen while the user has no account. In an app that everyone may use, the router shows that screen over the screen that the app starts on, so that back leads into the app.
Once the user is signed in to an account, both guards allow, and the router leaves the sign-in. From then on, a navigation to one of the three routes, such as a link, shows the screen that the app starts on. After a sign-out in an app that needs an account, the router shows the sign-in again. The next user then starts on the screen that the app starts on, wherever the last user was.
Both guards have the stage identity. In an app with a guard of a first launch, such as that of an onboarding, that one comes first, whichever order the modules were named in.
Texts #
The texts of the screens are in English and in Ukrainian. In an app with a module that provides localization, they follow the language of the app. An app without such a module shows them in English.
Every reason of a failure has a text of its own. When sign-in is not set up at the provider, the user reads only that sign-in is unavailable. What is wrong is in the developerHint of the failure, which the screen shows below the text in debug mode only.
In your tests #
A cubit takes the session through its constructor, and a provider reaches it through appSessionProvider, which a test overrides. So a test of the state of a screen gives it an AppSessionController of its own, with a service that the test scripts.
The button that submits spins for as long as its call takes. A widget test that waits for a screen with pumpAndSettle() returns only once the call has ended, so while a call is on its way, such a test shows frames with pump().
A page starts its entrance when it is first built. A widget test that starts the app in real time, as with tester.runAsync(), does so once in its file: a second start in the same file fails an assertion of Flutter in that animation.
Use with the SMF CLI #
smf create asks which features the app has. To choose this one without the question, here with a start screen, BLoC and bottom_tabs, which provides the layout role, the main navigation of the app:
smf create my_app -m home,sign_in,bloc,bottom_tabs
The tabs of that app lead to the start screen and to the settings screen, where the entry of the account is.
The feature requires a router, a module that provides sign-in, a settings screen and a module that manages state. smf create adds each when only one module provides it, and asks, or takes it from -m, when several do.
You don't add this package to an app yourself: smf create of the SMF CLI puts what the module generates into the app.
SMF generates apps for Flutter 3.44 or newer and Dart 3.12 or newer. It is tested on macOS, Linux and Windows.