Skip to main content

usesmileid_njia 0.0.1-SNAPSHOT.13 copy "usesmileid_njia: ^0.0.1-SNAPSHOT.13" to clipboard
usesmileid_njia: ^0.0.1-SNAPSHOT.13 copied to clipboard

Smile ID's flow navigation engine for Flutter: a thin bridge to the native njia cores, which own the back stack. Only navigation crosses the bridge.

njia for Flutter #

The usesmileid_njia package, built a layer at a time (AGENTS.md, "Small pull requests"). The reducer doesn't run in Dart: the package starts an engine in the native core (njia-android, njia-ios) and exchanges graphs, intents and transitions with it as JSON (SPEC §11), so the typed vocabulary and the codec live here and the routing doesn't. So far:

Library What it holds
package:usesmileid_njia/usesmileid_njia.dart NjiaHost, the Flutter host (SPEC §10.3), with NjiaStepScope and NjiaNavigator. NjiaEngine, the native engine driven from Dart through the bridge (SPEC §11): start, restore, openLink, send, preview, listeners and dispose, with refusals raised as njia's exceptions. The graph, generic over the SDK's step kinds K and facts F (NjiaGraph, NjiaNode, NjiaStep, NjiaSegment, NjiaCondition, NjiaGuard, NjiaIntentKind), its validate() with the SPEC §4.4 issue codes, the state (NjiaState, NjiaEntry, NjiaPhase, NjiaOutcome), the intents (NjiaIntent), the effects and transitions (NjiaEffect, NjiaTransition), and NjiaCodec, which maps them to the wire's strings through a NjiaWire (SPEC §4, §9). NjiaDefaults, and NjiaException with NjiaJsonException and NjiaInvalidGraphException. Under them, NjiaJsonText, the strict JSON reader and writer (SPEC §4.5). The Flutter SDK only

A graph in your own types #

enum Kind { capture, preview, consent }
enum Fact { needsBack, backDone }

final codec = NjiaCodec(kinds: NjiaWire.ofEnum(Kind.values), facts: NjiaWire.ofEnum(Fact.values));
final NjiaGraph<Kind, Fact> graph = codec.decodeGraph(json);
final again = NjiaCondition.allOf([Fact.needsBack]).butNot([Fact.backDone]);

NjiaCodec.strings reads the same graph with plain strings, which is what the bridge uses. Where Kotlin and Swift write NjiaWire.enum() and allOf(…), Dart writes NjiaWire.ofEnum(values) and NjiaCondition.allOf([…]): enum is a reserved word, and a top-level allOf would clash with package:matcher's in every test that imports both.

Dart can't nest a case inside its type the way Kotlin writes NjiaPhase.Busy, so each case of a sealed type is named <Case><Type> (NjiaBusyPhase) for pattern matching, and built through the type (NjiaPhase.busy(reason)).

Driving the native engine #

final codec = NjiaCodec(kinds: NjiaWire.ofEnum(Kind.values), facts: NjiaWire.ofEnum(Fact.values));
final engine = await NjiaKit.start(codec, graph, facts: {Fact.needsBack});
engine.addListener((transition) => setState(() {}));
await engine.send(NjiaIntent.next(from: engine.state.top!.id));

Showing a flow #

NjiaHost(
  engine: engine,
  builder: (context, scope) => switch (scope.step.kind) {
    Kind.capture => CaptureScreen(isTop: scope.isTop, onDone: scope.navigator.next),
    Kind.preview => PreviewScreen(onRetake: scope.navigator.back, onAccept: scope.navigator.next),
    Kind.consent => ConsentScreen(onAgree: scope.navigator.next),
  },
)

NjiaHost is a Navigator whose pages are the engine's entries, keyed by entry id, with the shared step motion and the njia.step.<step> semantics identifier on each (SPEC §10). The system back, and a step that pops its own route, send back from the entry on top. On iOS a screen also swipes back from its leading edge, while a back from it would pop it.

The calls cross a Pigeon channel (pigeons/njia_api.dart, regenerated with pigeons/generate.sh) to the core's NjiaBridge, so they complete asynchronously, in order. The plugin takes the core at its own release:

  • iOS: the plugin is a Swift package (ios/usesmileid_njia) that depends on the Njia product of njia-spm, pinned exactly to this release, so enable flutter config --enable-swift-package-manager. Inside this repository, send njia-spm to the core in your checkout with a local mirror, which CI uses too:

    bash scripts/local_njia_spm_mirror.sh /tmp/njia-spm \
      njia-flutter/example/ios/Runner.xcworkspace njia-flutter/example/ios/Runner.xcodeproj
    

    It writes a mirrors.json into each, which git ignores; delete them to use the public package.

  • Android: it depends on com.usesmileid:njia at this release. Inside this repository that is njia-android's -SNAPSHOT, from ./gradlew :njia:publishToMavenLocal.

example/ runs the integration tests against the real engines on both.

Answers #

A step returns a value with its navigator's answer, and the flow moves on. The entry keeps the answer until it is popped (SPEC §6). Dart has no overloads, so the typed calls are answerAs:

final plan = NjiaAnswerFormat<Plan>.of(encode: (plan) => plan.name, decode: Plan.values.byName);

// In the partner's step:
await scope.navigator.answerAs(Plan.gold, plan);

// In the SDK, once the flow has moved on:
final chosen = engine.state.answerAs('partner.plan', plan);

Registering steps #

Instead of switching on the step's kind, register each kind once, with shared decorators and per-entry dependencies, and give the registry to the host (SPEC §10). Dart has no overloads, so it's NjiaHost.withSteps:

final uploads = NjiaDependencyKey<UploadClient>('uploads');

final steps = NjiaSteps<Kind, Fact>((steps) => steps
  ..decorate((context, scope, content) => StepScaffold(child: content))
  ..provide(uploads, (scope) => sdk.uploadClient)
  ..step(Kind.capture, (context, scope) => CaptureScreen(session: scope.retained('camera', CameraSession.new)))
  ..step(Kind.preview, (context, scope) => PreviewScreen(uploads: scope.dependency(uploads))));

NjiaHost.withSteps(engine: engine, steps: steps);

A graph with a kind the registry can't draw throws StateError before anything is drawn; steps.missing(graph) finds it in a test. A dependency is never closed, since a provider may hand every entry one shared client; a resource the entry owns goes in retained, which keeps it until the entry is popped and closes it then if it's NjiaClosable. NjiaPausable builds a covered step with its last value, since the navigator keeps covered routes alive.

Tracing #

Give an engine a trace when it's created, and attach its document to a bug report. It keeps the last records (the creation, then every intent with its effects and state), redacting answers and outcome details by default (SPEC §9):

final trace = NjiaTrace<Kind, Fact>(capacity: 64);
final engine = await NjiaKit.start(codec, graph, trace: trace);

// In a bug report:
final json = trace.document(codec);

python3 scripts/replay_trace.py trace.json checks a document against the reference reducer.

Building #

Flutter 3.44 (Dart 3.12), with the package floor at Dart 3.11 (AGENTS.md).

flutter pub get
dart format --output=none --set-exit-if-changed .   # format
flutter analyze --fatal-infos                         # lint
flutter test                                          # unit tests, reading ../conformance directly
./scripts/coverage.sh                                 # tests with line coverage ≥ 95%
0
likes
155
points
764
downloads

Documentation

API reference

Publisher

verified publishersmileidentity.com

Weekly Downloads

Smile ID's flow navigation engine for Flutter: a thin bridge to the native njia cores, which own the back stack. Only navigation crosses the bridge.

Homepage
Repository (GitHub)

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on usesmileid_njia

Packages that implement usesmileid_njia