# fission-onboarding
Help people learn your app by using your app. `fission-onboarding` places a
spotlight and a small guided callout over **real Fission widgets**—a button,
field, result, or any other retained widget. The rest of the screen stays in
place, so a first-run tour can guide an actual task instead of showing a stack
of slides.
The crate supplies validated, versioned flows, stable anchors, spotlight and
callout rendering, accessible controls, and host-defined styling. Your app
decides when a step is complete, where to navigate, what to say, and how to save
progress. It works with the Fission application model, not a browser-specific
DOM tour.
## Add it
```toml
[dependencies]
fission = "0.15.1"
fission-onboarding = "0.1"
```
## Smallest working tour
This complete example highlights a real button. Clicking that button—or
choosing **Finish** in the callout—ends the tour.
```rust
use fission::prelude::*;
use fission_onboarding::{OnboardingAnchor, OnboardingFlow, OnboardingHost, OnboardingStep};
#[derive(Clone, Debug, Default)]
struct State { finished: bool }
impl GlobalState for State {}
#[fission_reducer(FinishTour)]
fn finish_tour(state: &mut State) { state.finished = true; }
#[derive(Clone)]
struct App;
impl From<App> for Widget {
fn from(_: App) -> Widget {
let (ctx, view) = fission::build::current::<State>();
let finish = ctx.bind(FinishTour, reduce_with!(finish_tour));
let target = WidgetId::explicit("demo.create");
let flow = OnboardingFlow::new("first-use", 1, vec![OnboardingStep {
id: "create".into(),
anchor: target,
route: None,
title: "Make your first item".into(),
body: "Use this button to start. You can revisit the tour later.".into(),
}]).expect("valid tour");
let mut children = widgets![OnboardingAnchor {
id: target,
child: Button {
child: Some(Text::new("Create item").into()),
on_press: Some(finish.clone()),
..Default::default()
}.into(),
}];
if !view.state().finished {
children.push(OnboardingHost::new(
"first-use", flow, 0, finish.clone(), finish,
).into());
}
Column { children, ..Default::default() }.into()
}
}
fn main() {
DesktopApp::<State, _>::new(App).run().expect("start app");
}
```
Run the checked-in copy:
```sh
cargo run --example minimal --features desktop-demo
```
## Guide a real task
For more than one step, create an `OnboardingFlow` with one stable
`WidgetId::explicit(...)` per target. Wrap the live target in
`OnboardingAnchor`, then render `OnboardingHost` alongside the normal app tree.
The host is presentation only; your reducers own transitions. This makes it
straightforward to require a real result before enabling Continue:
```rust
let mut host = OnboardingHost::new(
"first-task", flow, state.step,
ctx.bind(NextStep, reduce_with!(next_step)),
ctx.bind(FinishTour, reduce_with!(finish_tour)),
);
host.continue_enabled = state.created_item.is_some();
host.on_skip = Some(ctx.bind(SkipTour, reduce_with!(skip_tour)));
host.labels.progress = format!("Step {} of 3", state.step + 1).into();
host.style = my_app_onboarding_style(&view.env().theme);
```
See [the complete guided-task example](https://docs.rs/crate/fission-onboarding/0.1.0/source/examples/guided_task.rs) for a runnable
three-step flow with a field, a gated task action, Back, and Skip:
```sh
cargo run --example guided_task --features desktop-demo
```
The optional `route` on a step is metadata for your app's navigation. If a
target is on another screen or below a scroll viewport, navigate and call
Fission's `ensure_visible` effect before displaying that step. The anchor must
belong to a widget in the active retained tree. For responsive layouts, give
each live branch its own stable anchor and choose the matching flow anchor.
## Save and resume progress
`fission-onboarding` intentionally does not create a second persistence system.
Keep the flow ID, version, current step ID, and completed/skipped state in your
app's normal store. With Fission Store, use a typed `StoreKey<T>` in an
`onboarding` namespace, hydrate it through a startup action, and persist each
transition from reducers. On a new flow version, choose whether to restart or
skip previously completed tours. Your app also decides whether Skip is offered
and where a user can restart the tour.
## What you control
| Spotlight, anchored callout, buttons, accessible dialog | This crate |
| Target widgets and stable IDs | Your app |
| Text, translation, colours, spacing | Your app |
| Step conditions, navigation, scrolling | Your app |
| Progress storage and restart policy | Your app |
The crate has no account service, analytics, network call, or product-specific
state. It depends on Fission and `thiserror` only. Licensed MIT.