# Write an infrastructure module

An infrastructure module sets up a library or a service without screens of its own. It declares no routes, keeps nothing in `lib/features/`, has no variants, and never takes services from the DI container itself. It puts its code and settings into the app entry, which every module may use, and into the sockets of the roles and modules it declares.

The examples on this page come from two small modules that were generated into an app and checked with `flutter analyze`: `logging`, which sets up the [logging](https://pub.dev/packages/logging) package, and `git`, which makes the new app a Git repository.

## Start-up code

`bootstrap()` runs the start-up code of all modules in four phases: early, platform, DI and late. A module adds a fragment of code to a phase, with the imports it needs:

```dart
@override
List<Contribution> contribute(ModuleContext context) => [
  const PubspecContribution.hosted('logging', '^1.3.0'),
  const SocketContribution.code(
    AppEntryRole.bootstrapEarly,
    Fragment(
      r"Logger.root.onRecord.listen((record) => debugPrint('${record.level.name}: ${record.message}'));",
      imports: [
        ImportRef('package:flutter/foundation.dart', show: ['debugPrint']),
        ImportRef('package:logging/logging.dart', show: ['Logger']),
      ],
    ),
  ),
];
```

The pipeline adds the imports to `lib/bootstrap.dart`, merges them with those of other modules, and formats the file:

```dart title="lib/bootstrap.dart"
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:logging/logging.dart' show Logger;

Future<void> bootstrap() async {
  Logger.root.onRecord.listen(
    (record) => debugPrint('${record.level.name}: ${record.message}'),
  );
}
```

Pick the phase by what the code needs: early for what must come first, platform for platform services such as `Firebase.initializeApp`, DI for the registration of the services, and late for code that needs the services. Code that must be declared at the top level of `lib/bootstrap.dart`, such as a handler of background messages, goes into `AppEntryRole.topLevel`. `lib/bootstrap.dart` imports neither `material` nor `cupertino`, so start-up code does not depend on a design library.

## Wrappers around the app

`AppEntryRole.rootWrappers` wraps the root widget in `main()`, and `AppEntryRole.appBuilder` wraps the content of every route, in the `builder` of the root `MaterialApp`. A wrapper has an opening and a closing part. This is how `riverpod` adds its `ProviderScope`:

```dart
const SocketContribution.wrap(
  AppEntryRole.rootWrappers,
  Fragment.wrap(
    'ProviderScope(child: ',
    ')',
    imports: [
      ImportRef(
        'package:flutter_riverpod/flutter_riverpod.dart',
        show: ['ProviderScope'],
      ),
    ],
  ),
),
```

The first contribution is the outermost wrapper, as [the order of contributions](https://doc.saymyframe.com/concepts/sockets.md#order) decides.

`AppEntryRole.appArgs` sets arguments of the root `MaterialApp`: `theme`, `darkTheme`, `themeMode` and `locale`, which take one value, `localizationsDelegates`, which takes the list items of every contributor, and `supportedLocales`, which takes the list items of one contributor. The root is created in the `build` of a widget below the root wrappers, so the expression of an argument may read `context`. An inherited widget among the root wrappers can give the root a value this way, and the root rebuilds when the widget notifies. The theme role gives the root its mode like this:

```dart
const SocketContribution.wrap(
  AppEntryRole.rootWrappers,
  Fragment.wrap(
    'AppThemeModeScope(notifier: appThemeMode, child: ',
    ')',
    imports: [_themeMode],
  ),
),
// ...
const SocketContribution.arg(
  AppEntryRole.appArgs,
  'themeMode',
  Fragment('AppThemeModeScope.of(context)', imports: [_themeMode]),
),
```

That context is above the `MaterialApp`, so it has neither the theme nor the localizations of the app. Two rules of the app entry role make it the same with every module that provides the app entry. The tags of the root wrappers are in the body of `main()`, which runs the root widget inside them (`app_entry.root_wrappers_in_main`). And the provider creates the root `MaterialApp` in a method `build(BuildContext context)` of a class (`app_entry.root_in_build`).

Two different values of an argument that takes one are an error that names both contributors, and `smf create` stops. The items of a second contributor of `supportedLocales` are a conflict too, also when they are the same items: every delegate of the root has to support each of its locales, so a locale that one contributor adds would break the delegates of another. `smf create` leaves the module of those items out with a warning, or stops with `--strict`. In an app with the theme role, the role sets `theme`, `darkTheme` and `themeMode`, and in an app with the localization role, the role sets `locale` and is the one contributor of `supportedLocales`. So a module leaves these arguments to the roles: a theme comes from a module that [provides the theme role](https://doc.saymyframe.com/extending/provide-theme.md), and a module gives its [texts](https://doc.saymyframe.com/extending/texts.md) to the localization role instead of bringing a delegate of its own.

## Dependencies

`PubspecContribution` adds to the `pubspec.yaml` of the app:

- `PubspecContribution.hosted('logging', '^1.3.0')` depends on a package of pub.dev, and `dev: true` makes it a dev dependency.
- `PubspecContribution.sdk('flutter_localizations')` depends on a package of the Flutter SDK.
- `PubspecContribution.flutter(assets: [...], fonts: [...], generate: true)` adds to the `flutter:` section.
- `PubspecContribution.environment(sdk: ..., flutter: ...)` constrains the Dart and Flutter versions of the app.

The pipeline intersects the constraints of a package that several modules add, and an empty intersection is an error. Declare `environment` only when your package needs a newer Dart or Flutter than the app has anyway. `flutter_core` sets Dart `^3.12.0` and Flutter `>=3.44.0`, and `smf create` checks the Flutter SDK against the merged constraints before it generates anything.

A package that the provider of a role brings, such as `flutter_riverpod` of `riverpod`, is that provider's. Another module adds it only in its variant for the provider, with the constraint `any`, or when it depends on the provider. A package that no provider brings is shared, and every module that needs it adds it. A provider brings a package when it adds the package outside a variant with a constraint other than `any`. In a provider, add a shared package, such as `collection`, with `any`. See [The packages of a provider](https://doc.saymyframe.com/extending/role-provider.md#the-packages-of-a-provider).

## Native settings

The app entry has sockets for the native files, so a module never edits them:

```dart
AppEntryRole.iosDeploymentTarget.value('16.0'),
AppEntryRole.androidManifestPermissions.key('android.permission.INTERNET'),
AppEntryRole.androidManifestApplicationMeta.entry(
  'com.example.sdk.CHANNEL',
  const AndroidMetaData.value('stable'),
),
AppEntryRole.infoPlist.entry(
  'UIBackgroundModes',
  const PlistStringArray(['fetch']),
),
AppEntryRole.gradleSettingsPlugins.entry('io.github.ben-manes.versions', '0.64.0'),
AppEntryRole.gradleAppPlugins.key('io.github.ben-manes.versions'),
AppEntryRole.gradleAppDependencies.entry('androidx.annotation:annotation', '1.9.1'),
AppEntryRole.androidCoreLibraryDesugaring.value('2.1.4'),
...AppEntryRole.paidTeamEntitlement(
  'com.apple.developer.applesignin',
  const PlistStringArray(['Default']),
),
const SocketContribution.code(
  AppEntryRole.iosAppDelegateLaunch,
  Fragment('    UNUserNotificationCenter.current().delegate = self'),
),
const SocketContribution.code(
  AppEntryRole.androidManifestApplicationComponents,
  Fragment('''
        <receiver
            android:exported="false"
            android:name="com.example.sdk.AlarmReceiver" />'''),
),
```

- The minimum iOS version is the highest that any module asks for, and `flutter_core` gives 15.0.
- Equal entries of several modules merge, and different values of one key are an error that names both modules. Arrays of strings in `Info.plist` and in the entitlements are united, and Gradle versions take the highest.
- A key that the template of `flutter_core` has already, such as `CFBundleName`, cannot be added again. The Gradle sockets do not take the plugins that the template declares or applies, or the Kotlin plugin, which the Flutter Gradle plugin applies itself.
- `AppEntryRole.mainActivityIntentFilters` takes `<intent-filter>` elements of the main activity as XML, such as deep links.
- `androidCoreLibraryDesugaring` takes the version of `desugar_jdk_libs` that a library of the app asks for, when the Android build of an app with that library fails without desugaring. The highest version wins, and the one value turns desugaring on in `compileOptions` and adds the library to `dependencies`. The build fails with either line alone.
- `iosEntitlements` takes the entitlements of the iOS app into `ios/Runner/Runner.entitlements`. On a device, the capability of an entitlement may need a team of the paid Apple Developer Program. The simulator signs the app without one. An entitlement that only such a team signs, such as the one of Sign in with Apple above or `aps-environment` for push messages, goes in with `AppEntryRole.paidTeamEntitlement`, which also lists the step by hand in which the developer signs the app with that team; see [a step of a role](#a-step-of-a-role).
- `iosAppDelegateLaunch` takes Swift statements, indented by four spaces, that run in `application(_:didFinishLaunchingWithOptions:)` before the launch goes to Flutter. They read the app delegate as `self`, and the parameters `application` and `launchOptions`, and take no imports: the file imports `Flutter` and `UIKit`, and `Flutter` brings `UserNotifications`. Flutter registers the plugins of the app once its scene connects, after this method, so the statements have no registry of plugins and no channel.
- `androidManifestApplicationComponents` takes `<receiver>`, `<service>` and `<provider>` elements as XML, indented by eight spaces, for a component that the app has to declare itself, such as a receiver that the manifest of a library leaves out. Declare each component once. The contract harness reports two declarations of one `android:name`, also from two modules, and reads a name that starts with a dot, or has none, against the `namespace` of the app, as Android does. Android fails to build an app whose two declarations of a component differ.

[`local_notifications`](https://doc.saymyframe.com/modules/local-notifications.md) uses three of these sockets: desugaring, the receivers of its package with the permission to start after a restart of the device, and the delegate of the notification center in the `AppDelegate`.

## README sections

`AppEntryRole.readmeSections` adds a section to the `README.md` of the app, under a heading of its own, in Markdown:

```dart
AppEntryRole.readmeSections.entry(
  'Logging',
  'The app logs with the [logging](https://pub.dev/packages/logging) '
      'package: create a `Logger` per class. `bootstrap()` prints the '
      'records at `Logger.root.level` and above, which is INFO by default.',
),
```

Use it for what someone who opens the app needs to know about your module, such as how to set up a service again. `firebase_core` explains there how to configure Firebase. The heading "Finish the setup" is taken: a step that only the developer can do is a [step by hand](#steps-by-hand), which the app entry role lists in that section.

## Notes for coding agents

Every app has a guide for the coding agents that work on it, `AGENTS.md`. `AppEntryRole.agentSections` is the socket of its sections, and a module adds a note to it under a heading, in Markdown:

```dart
static const _agentNote = '''
- For the feedback of a tap, call `tapFeedback()` of `lib/core/haptics/haptics.dart`. Do not call `HapticFeedback` yourself: the user can turn the feedback off, and only `tapFeedback()` asks.
- Change the choice only with `chooseHaptics()`, which also saves it when the app has preferences.
''';
```

```dart
AppEntryRole.agentSections.entry('Haptics', AgentNote(_agentNote)),
```

A note tells what the code of the app does not show: a rule that holds across files, an order, what not to do and what to do instead, a placeholder, a step that needs a person. What the code shows needs no note, and setup that needs a person or an account goes into a README section. The built-in modules keep the texts of their notes in `lib/src/agents.dart` of their package.

### The heading of a note

- The template of a role writes the note of the role under the description of the role, such as `routerRole.description`: where the files of the role are, how code uses it, and what code must not do. Such a note holds whichever module provides the role, so it never names a provider or its package. Only a template contributes it, with `AgentNote.ofRole`, and the socket refuses one from a module; see [Define a role](https://doc.saymyframe.com/extending/new-role.md#the-template).
- A module that provides a role adds its note under the same heading, with `AgentNote`, and the section has the note of the role first. A provider that wraps a package opens its note with a line that names the package, such as "With `go_router`:". Then it tells where it writes what the role describes, and how to add one more by hand. A provider that wraps a tool of Flutter names the tool instead, as `gen_l10n` does with "With `gen-l10n` of Flutter:".
- A module without a role uses a heading of its own, as `haptics` does above.

The notes under one heading are united into one section: the notes of roles first, then the others in the order of the ids of their contributors, each once. The section of the app entry comes first in the guide, and the others follow in the order of their headings. So replacing the provider of a role changes only its own note.

### The rules of a note

- It is true in every app that has it. A note about a file that only some apps have goes into a contribution of its own, with the role of that file in its `when`. `firebase_analytics` adds its note on screen views with `when: {routerRole}`, since only an app with a router has the listener that logs them.
- What depends on the data of a role comes from the render hook of the role or of the module that provides it, as a fragment for the socket. `go_router` tells this way where the destinations of the main navigation go, only in an app that has one, and the router role adds what it tells of the guards only in an app with guards.
- It stays in its section. It has no line that starts a title or another section, with `# ` or `## ` or with a line of `=` or `-` under a line of text, and it closes its fenced code blocks. A heading is one line.
- It names a file or a directory of the app in backticks, by its path from the root of the app, such as `lib/core/haptics/haptics.dart`. A file that a later step writes, such as one of a tool that runs after generation, is named as a pattern, with `<…>` or `*`, or left to the README.

### What is checked

In every app that the contract harness renders, the rule `app_entry.agent_guide_paths` reads the paths that the guide names in backticks below a top-level directory of a Flutter project, such as `lib/`, `test/` or `android/`, and its paths to Dart files. It reports one that the app does not have. It does not read the name of a file at the root of the app, such as `pubspec.yaml`.

The socket puts its sections in an order of its own, so a `when` of a note adds nothing to [the order of contributions](https://doc.saymyframe.com/concepts/sockets.md#order), and no note can make an app impossible to generate.

A test can also hold the notes of the roles against the modules that provide the roles, with `roleNoteProblems`: see [The notes of roles](https://doc.saymyframe.com/extending/testing.md#the-notes-of-roles).

Nothing checks what a sentence of a note says about the code, which only a test of that code can pin. A test of the module can pin the names: it reads the note from the app, and checks that each name of code that the note gives is one that a file of the app declares or uses. [Test a module](https://doc.saymyframe.com/extending/testing.md#checking-one-app) shows how a test reads the notes.

## Building on another module

When your module builds on another module itself, and not on a role, depend on it with the id constant of its package:

```dart
dependsOn: {FirebaseCoreModule.id},
```

The other module comes into the app with yours, and the constant makes its package a dependency of yours. Your module may then put code into the sockets of that module. A module can declare sockets of its own, such as `SocketRef.module(id, 'setup', CodeSocket())`, which only the modules that depend on it directly may fill. Prefer a role whenever one describes what you need, so that the user can pick its provider.

## Code generation

A module that needs `build_runner` contributes a `CodegenRequest`, and the builders it needs as dev dependencies:

```dart
const PubspecContribution.hosted('json_annotation', '^4.9.0'),
const PubspecContribution.hosted('json_serializable', '^6.9.0', dev: true),
const CodegenRequest(description: 'JSON code of FixtureModel'),
```

However many modules ask, the pipeline runs `dart run build_runner build --force-jit` once, after `flutter pub get`, and adds `build_runner` to the dev dependencies itself. A library that the builders generate and that code of the app imports, such as `lib/core/di/dependencies.config.dart`, goes into `outputs`, so that the pipeline and the harness know about it. Part files, such as `.g.dart`, need no mention.

## Checking the machine

A module that needs a tool on the machine contributes a `Preflight` with checks. A check has an id, unique in its module, and a description, which the report of `--explain` and the warnings show:

```dart
/// Checks that Git is installed.
final class GitCheck extends PreflightCheck {
  const GitCheck();

  @override
  String get id => 'git';

  @override
  String get description => 'Git';

  @override
  Future<PreflightStatus> check(SmfEnvironment environment) async {
    if (await environment.findExecutable('git') != null) {
      return const PreflightPassed();
    }
    return const PreflightMissing(
      instructions: 'Install Git from https://git-scm.com/downloads.',
    );
  }
}
```

- `check` only reads the machine, because `--explain` runs it too. It does not install anything, log in, ask the user or change files other than its temporary files. It returns `PreflightPassed`, `PreflightMissing` with instructions, or `PreflightFailed` when the check itself could not run.
- When the check finds something other than what it looks for, such as an older version of the tool, it says what it found in `PreflightMissing(found: ...)`, as a clause such as `'git 2.20.0 is installed'`. The pipeline then says that the tool is needed, but git 2.20.0 is installed, rather than that the tool is missing.
- A missing tool that the check can install is `PreflightMissing(installable: true)`. In a terminal, unless the run skips external setup, the pipeline asks the user, showing what the check found and its instructions, calls `install`, and runs `check` again. `install` returns a `ToolInstall` with the directories of what it installed, which the pipeline adds to the `PATH` of later checks and commands.
- A failed check is a warning by default. Set `required` only when the app cannot work without the tool. A failed required check leaves the module out of the app, or stops the run with `--strict` or when no app can be made without the module.
- Never show the output of a command that can hold a secret, such as a token of a login. The login check of `firebase_core` reports only the error of the command.
- Run commands with the `processRunner` of the environment, and find them with `findExecutable`, which also looks in the directories of tools installed during the run. The names `flutter` and `dart` stand for the Flutter SDK that the pipeline checked. On Windows, a batch file such as `dart.bat` cannot take arguments with `^`, `<`, `>`, `%`, `&`, `|` or `"`, because `cmd.exe` would read them as its own.
- A command that may wait a long time, such as one that asks a service over the network, gets a `timeout` in `processRunner.run`. The runner then stops the command along with the processes it started, and the result says so with `timedOut`. The login check of `firebase_core` gives `firebase projects:list` 40 seconds.
- The pipeline shows the progress "Checking the machine" while the checks run, and another progress for a check that runs again after an installation, so a check does not need to show one itself.
- Read an environment variable of `smf` with `environmentVariable`, such as `SSH_CONNECTION`, which tells the login check of `firebase_core` that the session is over SSH. On Windows, the name may have any case.

See the checks of [`firebase_core`](https://doc.saymyframe.com/guides/firebase.md#before-generation-the-machine) for checks that install tools and log in.

### The order of the checks

The pipeline checks the machine for a module after the modules it depends on, directly or not. It does not matter where the user named those modules, or whether the pipeline added them. The checks of the other modules keep the order of the modules of the app, and those of the templates of the roles come last.

After an installation, the pipeline runs again only the checks after it that have not passed. So a check of your module may need a tool that a check of a module it depends on installs. It cannot count on a check of any other module, whose place depends on the order in which the user named the modules.

### A check from the module you depend on

A step names only checks of its own module in `needs`. So when your module needs more of a tool than the module it depends on checks, that module can offer you a check to put into your own `Preflight`. `smf_firebase_core` does: for a module that depends on it and runs a command that only newer versions of the Firebase CLI have, it exports `FirebaseCliVersionCheck`.

```dart
const version = FirebaseCliVersionCheck(minimum: '15.34.0');

const Preflight([version]),
PostGenStep(tool, arguments, skippable: true, needs: [version.id]),
```

`smf_firebase_core` knows how to ask the Firebase CLI, and your module knows the number: the lowest version that has its command, as three numbers. `firebase_auth` asks for 15.34.0 this way, the first Firebase CLI that enables sign-in methods without adding a web app to a project that has the apps of `flutterfire configure`.

- The check reads `firebase --version` as the check of the Firebase CLI of `firebase_core` runs it. It accepts that version and any later one, of a later major version too. A pre-release of the lowest version comes before it.
- For an older Firebase CLI, it tells which command it found, in which version, and how to update it. It installs nothing: other projects of the user run the Firebase CLI too, so the user updates it.
- On a machine without the Firebase CLI, it reports it missing, and the check of `firebase_core` offers the installation. That check runs first, so after the installation the check of the version runs again and reads the version that was installed.

## Commands after generation

A `PostGenStep` runs a command in the new app, after `flutter pub get` and code generation:

```dart
const Preflight([GitCheck()]),
PostGenStep(
  ToolRef('git'),
  ['init', '--quiet'],
  description: 'Creating a Git repository',
  skippable: true,
  needs: ['git'],
),
```

```text
✓ Getting the packages of the app (1.0s)
✓ Creating a Git repository (0.0s)
✓ Cleaning up the imports (1.9s)
```

| Option | Meaning |
| --- | --- |
| `description` | What the step does, for its progress and for the instructions when it is left for later. |
| `interactive` | The command talks to the user, so it runs with the terminal attached, and only in a run that can ask. |
| `external` | The command needs something outside the app, such as an account, so a run with `--skip-external-setup` leaves it for later. |
| `skippable` | The app is complete without it, so the pipeline may leave it for later, printing its command, instead of failing. In a run that can ask, the pipeline asks before it runs the step, so the user can leave it for later too. |
| `needs` | Ids of checks of the module's `Preflight`. When one has not passed, the step does not run, is not asked about, and is left for later with the check as the reason. |
| `id` | The id by which other steps continue this one, a `PostGenStepId` of the module and a name. See [Continuing a step](#continuing-a-step). |
| `followUpOf` | The id of the step that this one continues. |
| `notice` | What the user has to know before the step runs and its description does not tell, such as a side effect of the tool that it runs. The step then runs only once the user agreed to it. See [A step with a notice](#a-step-with-a-notice). |

A step that is not skippable fails the generation when it fails or cannot run, unless it continues a step that is not done. `ToolRef` names the executable, arguments that always come first, and environment variables. `firebase_core`, for example, runs a script of the app as `ToolRef('dart')` with the arguments `[configureScript, AppEntryRole.defaultFlavor.name]`.

When a run ends, it reports the steps that are not done. A step that could not run or failed is a warning. A step that was left for later is in a list after the line that the app is created, with its description, the reason, its command and its notice. [The machine and external setup](https://doc.saymyframe.com/guides/smf-create.md#the-machine-and-external-setup) has an example of each.

The step runs in the temporary directory of the app, which then moves to its place, so it must not write the absolute path of its working directory into the app. The pipeline writes the files where Flutter records such paths again in the app's directory.

### Continuing a step

A step can continue another step, such as with a tool that needs what that step set up. The module of the step to continue gives it an `id` and publishes the id as a constant of its module class, next to its `ModuleId`. `firebase_core` does this for the step that configures Firebase, which has `id: configureStep`:

```dart
final class FirebaseCoreModule extends SmfModule {
  static const id = ModuleId('firebase_core');
  static const configureStep = PostGenStepId(id, 'configure');
  // ...
}
```

`firebase_auth` depends on `firebase_core`, and continues that step with the script that enables the sign-in methods in the project that the user chose:

```dart
PostGenStep(
  const ToolRef('dart'),
  const [enableSignInScript],
  followUpOf: FirebaseCoreModule.configureStep,
  description: 'Enabling the sign-in methods of the app in its Firebase project',
  notice: enableSignInNotice,
  skippable: true,
  external: true,
  needs: [firebaseCliWithSignIn.id],
),
```

A step that continues another is part of it:

- It runs right after the step it continues, once that step succeeded, rather than in its own place among the steps. The user is not asked about it, unless it has a [notice](#a-step-with-a-notice).
- When the step it continues does not run or fails, it does not run either. It is left for later after that step, with that step as the reason, whether it is skippable or not.
- It applies to the app whenever the step it continues does, so it has no `when` of its own. Otherwise it runs by its own options, such as `needs` and `skippable`.
- Another step can continue it in turn, and then runs right after it.

A module continues its own steps and the steps of the modules it depends on directly. So the module of a step never learns which modules continue it. The pipeline reports as problems of the module a step whose `id` names another module, two steps with one id, a step that continues a step it may not continue or a step that the app does not have, a step that continues another and has a `when`, and steps that continue each other in a cycle.

A step runs once, when the app is generated. For what has to follow every later run of a tool too, the app needs a place of its own, which a module gives it as a script of the app. `tool/configure_firebase.dart` of `firebase_core` is such a script: it runs each Dart file of `tool/firebase_configured/`, in the order of their names, after each flavor that it configures. So `firebase_crashlytics`, which has to fix a file that flutterfire writes again in each of its runs, puts a brick file there and has no step, and `firebase_core` knows nothing of it. The file has only the libraries of Dart, since `dart` cannot load a file that imports Flutter.

### A step with a notice

Some steps do more than their description says, such as a tool that also changes something in an account of the user. A step tells the user of that in its `notice`, and then runs only once the user agreed to it. `firebase_auth` has such a step. It continues the step of `firebase_core` that configures Firebase, and enables the sign-in methods of the app in the Firebase project with a script of the app. For a way to sign in of Google, the script also changes the project and files of the app, which the description of the step does not tell:

```dart
const enableSignInNotice = 'If the app signs in with Google, it also gives '
    "Google the app's name and the Firebase CLI account's email for its "
    'consent screen, registers the SHA-1 and SHA-256 of this '
    "machine's Android debug key (creating the key if there is none, as the "
    "first debug build would), downloads the app's Firebase files again and "
    'writes the iOS URL scheme into ios/Runner/Info.plist.';
```

A step cannot tell what the roles of the app chose, since the pipeline needs the steps before any role chooses, and a step that continues another takes no condition at all. So the step and its notice are the same in every app, and the notice says "if the app signs in with Google", which is true of every app.

```dart
final PostGenStep enableSignIn = PostGenStep(
  const ToolRef('dart'),
  const [enableSignInScript],
  followUpOf: FirebaseCoreModule.configureStep,
  description: 'Enabling the sign-in methods of the app in its Firebase '
      'project',
  notice: enableSignInNotice,
  skippable: true,
  external: true,
  needs: [firebaseCliWithSignIn.id],
);
```

The step needs `firebaseCliWithSignIn`, the module's `FirebaseCliVersionCheck(minimum: '15.34.0')`, and the question of a run in a terminal is one line:

```text
Enabling the sign-in methods of the app in its Firebase project (dart tool/enable_firebase_sign_in.dart), for firebase_auth. If the app signs in with Google, it also gives Google the app's name and the Firebase CLI account's email for its consent screen, registers the SHA-1 and SHA-256 of this machine's Android debug key (creating the key if there is none, as the first debug build would), downloads the app's Firebase files again and writes the iOS URL scheme into ios/Runner/Info.plist. Run it now?
```

What a run does with such a step:

| The run | What happens to a step with a notice |
| --- | --- |
| It can ask, and the step can run. | It asks about the step, with the notice in the question: `<description> (<command>), for <module>. <notice> Run it now?`. Enter answers yes, as for every step. For a step that continues another, the question comes once that step succeeded. |
| It can ask, and the step cannot run. | It does not ask, and leaves the step for later like any other. That is a step that is `external` in a run that skips external setup, one that `needs` a check which has not passed, one whose tool is missing, and one that continues a step that is not done. |
| It cannot ask. | It leaves the step for later, although it runs a step without a notice unasked. |

When the user declines a step, the steps that continue it are left for later after it, without a question.

The notice follows the command of a step that is left for later, whatever the reason, and `--explain` shows it under the command. So write it as a sentence or two on one line that read on their own in each of these places, without a reference to the question, such as "answer no". The pipeline reports a notice that is blank or has a line break.

The user may decline the step, so a step with a notice is `skippable`, and the pipeline reports one that is not.

## Steps by hand

Some of the setup of an app is out of reach of any command, such as uploading a key to the Firebase console or signing the iOS app with a paid Apple Developer team. A module tells of such a step with a `ManualStep`: an id and a short title, and nothing else. How to do the step, click by click, is in its guide, on a page that the id names.

`firebase_messaging` publishes its step as a constant of its module class, next to its `ModuleId`, and lists it among its contributions, since every app of the module needs it:

```dart
final class FirebaseMessagingModule extends SmfModule {
  static const id = ModuleId('firebase_messaging');

  static const apnsKeyStep = ManualStep(
    ManualStepId(id, 'apns_key'),
    'Give Firebase your Apple push key (APNs key)',
  );
  // ...
}
```

An app with steps by hand gets them in four places:

- At the end of `smf create`, after the steps that are left for later, the line `Finish the setup by hand in 2 steps:`, or `in one step:`, with the link to their guide on a line of its own. The run asks nothing about them, so a run without a terminal prints the same line. See [steps by hand](https://doc.saymyframe.com/guides/smf-create.md#steps-by-hand).
- In `smf create --explain`, the section `By hand`: the link, then each step as `<id>: <title> (<contributors>)`. See [Explain](https://doc.saymyframe.com/guides/smf-create.md#explain).
- In the README of the app, the section "Finish the setup" (`AppEntryRole.setupHeading`), which comes before every other section. It lists the titles, numbered, with the link to their guide. Only the template of the app entry role writes it: `AppEntryRole.readmeSections` refuses that heading from anyone else.
- In `AGENTS.md`, a second note of the app entry role, which tells coding agents that only the developer of the app does these steps.

An app without steps has none of these.

### The id and the title

`ManualStepId(module, name)` names a step of a module. The name is lower snake_case, such as `apns_key`, and no other step of the owner has it. `ManualStepId.wire` is the id as the link carries it, `<owner>.<name>`, such as `firebase_messaging.apns_key`. It names the step for good: a link that a run once printed still leads to the guide of the step, so never rename a step.

The title tells what the developer does, and the guide tells how. It is one line of at most 80 characters (`ManualStep.maxTitleLength`) with no spaces around it, starts with an upper-case letter or with `iOS`, and has neither a final period nor a backslash.

When only an app with a role needs the step, and the module only uses that role, the step lists the role in its `when`, as other contributions do.

The pipeline reports, as a problem of the contributor:

- a name that is not lower snake_case;
- an id of another module (a variant counts as its module), and an id of a module from the template of a role;
- an id of a role that the contributor does not provide, require or use, and that is not open to all modules;
- a title against its rules;
- a second title under one id, at the contributor that comes later in the order of the steps.

So in lenient mode, `smf create` leaves out the module at fault.

### A step of a role

Contributors that do not know each other may need the same step. A role publishes such a step as a constant of its class, with `ManualStepId.role(role, name)`. Each contributor lists it, and the app has it once. A step with an id of a role applies only in an app with that role. The registry refuses a role with the id of a module, so steps of two owners never share an id in the link.

The app entry role publishes one such step, `AppEntryRole.iosAppIdStep` (`app_entry.ios_app_id`), in which the developer signs the iOS app in Xcode once with a team of the paid Apple Developer Program. That registers the App ID of the app with the capabilities of its entitlements. An app needs the step when its entitlements have one that only such a team signs, one of `AppEntryRole.paidTeamEntitlements`: `aps-environment`, `com.apple.developer.applesignin` and `com.apple.developer.associated-domains`. `AppEntryRole.paidTeamEntitlement(key, value, when:)` gives the entry of `AppEntryRole.iosEntitlements` and the step together, under the same roles:

```dart
...AppEntryRole.paidTeamEntitlement(
  'com.apple.developer.applesignin',
  const PlistStringArray(['Default']),
),
```

It throws an `ArgumentError` for any other key: a free team signs any other entitlement, which goes into `iosEntitlements` alone and needs no step. The structural rule `app_entry.ios_app_id_step` reports an app whose `ios/Runner/Runner.entitlements` has a key of `paidTeamEntitlements` and whose steps lack this one. In SMF, `apple_sign_in` and the template of the push role add their entitlements this way.

### Steps that depend on the data of a role

A step that depends on what the modules give a role comes from the `manualSteps` hook of the template of the role or of a provider of it. `firebase_auth` lists the steps of Apple only in an app with a way to sign in of Apple, and the SHA of a release only in one with a way to sign in that needs the certificate of the Android app, such as Google:

```dart
@override
List<ManualStep> manualSteps(RoleHookInput<AuthData> input) {
  final methods = firebaseMethodsOf(authRole.methodsIn(input));
  return [
    if (methods.containsKey('apple.com')) ...const [
      FirebaseAuthModule.appleProviderStep,
      FirebaseAuthModule.appleRevocationStep,
    ],
    if (methods.values
        .any((needs) => needs.contains(SignInNeed.androidCertificate)))
      FirebaseAuthModule.googleReleaseShaStep,
  ];
}
```

- The hook runs once, in the validation, before the roles choose, so `input.choice` is `null`. No step depends on a choice, and `--explain`, which stops before the choices, has the steps too.
- A step of the hook has no `when`, since the hook asks for the presence of the roles itself with `input.has(role)`. The pipeline reports a step with `when` and a hook that throws.
- The hook of a provider gives an id of its own module, or of a role that its module provides, requires or uses, or that is open to all modules. The hook of a template gives an id of its role, of a role that the role requires or uses, or of a role that is open to all modules.
- A step that every app with the role or with the provider has is a contribution of the template or of the module instead.

### The order

The pipeline plans the steps once, in the validation. It orders them by the edges between their contributors, as it orders the steps after generation. A cycle of those edges is no problem here, since the order only tells the developer which step to do first, and the contributors on the cycle are ordered by name. Each id comes once, in the place of its first step, with every contributor that lists it. The steps of roles come first, because a role publishes a step that its contributors need before their own. The link, the README and `--explain` give the steps in this order.

The render hooks and the structural rules of a role that is open to all modules, which only the app entry is, get the planned steps and the link as `RoleHookInput.manualSetup`. Every other role gets `ManualSetup.none`, since the ids name modules that the role does not know of.

### The link and the guides

The pipeline knows no site. The front end names the page that guides the developer: a `SetupLink`, a function from the ids of the steps, in their order, to a `Uri`, which `runSmf` and `CreatePipeline` take as `setupLink`. Without one, the run prints no line about the steps, and `--explain` and the README list them without a link. `smf create` names [the setup page](https://doc.saymyframe.com/setup.md) of this site, with the ids after `?steps=`, separated by commas, as in `https://doc.saymyframe.com/setup?steps=app_entry.ios_app_id,firebase_messaging.apns_key`. A [command of your own](https://doc.saymyframe.com/extending/publishing.md#a-command-of-your-own) built on `runCli` names the same page, which shows a step that it has no guide for by its id and points to the README of the app for its title.

This site keeps a guide for every step of the modules that `smf create` offers. `packages/smf_flutter_cli/setup_steps.json` is their catalog: each id once, with its title, its owner and the modules that bring it into an app. After a change of the steps of such a module, run `dart run tool/setup_steps.dart` in `packages/smf_flutter_cli`, which writes the file. `test/setup_steps_test.dart` fails until the file is up to date. A new step also needs its guide on this site, whose build compares its copy of the catalog with the guides. An id that no app has any more moves to `retired` in the catalog and keeps its guide, since a link that a run once printed names it.

### In tests

`ContractResult.manualSteps` has the planned steps of the app of a case, each a `PlannedManualStep` with its `step` and its `contributors`, and `ContractResult.setupLink` their link. The harness plans them before the roles choose, so a harness that does not render has them too, and `result.app!.manualSteps` has the same steps. The harness takes a `setupLink`, and has none by default. The tests of `apple_sign_in` check the step of its entitlement this way:

```dart
final planned = result.manualSteps.single;
expect(planned.step.id, AppEntryRole.iosAppIdStep.id);
expect(planned.step.title, AppEntryRole.iosAppIdStep.title);
```

## Services

An infrastructure module that creates services registers them as data of the DI role, and gets the services they need through the factory, as in `FactoryRef('createApiClient', import: ..., deps: [ServiceRef(TypeRef('HttpClient', import: ...))])`. The container calls the factory with those services, and the module never calls `resolve`. See [registering services](https://doc.saymyframe.com/extending/role-provider.md#registering-services).

A module that implements analytics, crash reporting, events, preferences, notifications, push messages or sign-in provides the role instead; see [Provide a role](https://doc.saymyframe.com/extending/role-provider.md). A module that keeps a setting of its own in the preferences gives the preferences role a restorer; see [Add a setting](https://doc.saymyframe.com/extending/settings.md).
