Skip to content

Commit 1568cee

Browse files
docs(plugin-activity-guard): add README (#746)
1 parent 2b66188 commit 1568cee

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# @stackflow/plugin-activity-guard
2+
3+
Applications often need to control users' entry into an Activity based on
4+
conditions such as sign-in, onboarding, or terms acceptance. Implementing
5+
those entry policies at individual navigation call sites or inside Activity
6+
components duplicates the policy and can apply it inconsistently.
7+
8+
`@stackflow/plugin-activity-guard` centralizes these entry policies as typed,
9+
synchronous Guards. Before an Activity is pushed, replaced, or selected as the
10+
initial Activity, a Guard can allow the requested Activity or redirect the
11+
entry to another registered Activity before the Stack changes.
12+
13+
The package controls client-side navigation. It is not an authorization
14+
boundary for protected data or server resources.
15+
16+
## Installation
17+
18+
```bash
19+
yarn add @stackflow/plugin-activity-guard
20+
```
21+
22+
## Setup
23+
24+
Add `activityGuardPlugin()` to your Stackflow configuration and map each
25+
guarded Activity to its Guard. This example assumes `Checkout`, `SignIn`, and
26+
`Terms` are already registered in `@stackflow/config`.
27+
28+
```tsx
29+
import { activityGuardPlugin } from "@stackflow/plugin-activity-guard";
30+
import { stackflow } from "@stackflow/react";
31+
import { config } from "./stackflow.config";
32+
import { Checkout, SignIn, Terms } from "./activities";
33+
import { checkoutGuard } from "./checkoutGuard";
34+
35+
export const { Stack } = stackflow({
36+
config,
37+
components: {
38+
Checkout,
39+
SignIn,
40+
Terms,
41+
},
42+
plugins: [
43+
activityGuardPlugin({
44+
guards: {
45+
Checkout: checkoutGuard,
46+
},
47+
}),
48+
],
49+
});
50+
```
51+
52+
## Usage
53+
54+
The following Guard requires both sign-in and terms acceptance before entering
55+
`Checkout`. Activity names and parameters are inferred from your
56+
`@stackflow/config` registration.
57+
58+
```ts
59+
import type { ActivityGuardFor } from "@stackflow/plugin-activity-guard";
60+
import { all, redirect } from "@stackflow/plugin-activity-guard";
61+
62+
const requireSignIn: ActivityGuardFor<"Checkout"> = ({ activityName, activityParams }) =>
63+
isSignedIn()
64+
? true
65+
: redirect("SignIn", { returnTo: { activityName, activityParams } });
66+
67+
const requireTerms: ActivityGuardFor<"Checkout"> = ({ activityParams }) =>
68+
hasAcceptedTerms()
69+
? true
70+
: redirect("Terms", { orderId: activityParams.orderId });
71+
72+
export const checkoutGuard = all(requireSignIn, requireTerms);
73+
```
74+
75+
Each Guard receives the requested `activityName` and its typed
76+
`activityParams`. Return `true` to allow the entry, or return
77+
`redirect(activityName, activityParams)` to replace its target. In the example,
78+
`all()` evaluates both Guards in order and stops at the first redirect.
79+
80+
## Behavior and limitations
81+
82+
- Redirect destinations are guarded again. Redirect chains must eventually
83+
reach an allowed or unguarded Activity; redirect cycles are not detected.
84+
- Guards must not throw any errors.
85+
- A redirect preserves whether the original operation was a `push` or
86+
`replace`, along with its other action parameters.
87+
- Guards run for fresh initial navigation, but not when Stackflow restores a
88+
snapshot. They also do not run for `pop`, Activity reactivation, or step
89+
navigation.
90+
- When a fresh initial entry is redirected, later events in that initial event
91+
sequence are discarded.
92+
93+
Stackflow invokes plugins in array order. During initialization, this plugin
94+
guards the initial events returned by earlier plugins. Place it after a plugin
95+
that chooses the initial Activity, such as `historySyncPlugin()`, when that
96+
plugin's destination should be guarded. For `push` and `replace`, a Guard sees
97+
action parameters overridden by earlier plugins, and later plugins can override
98+
the redirected target again.
99+
100+
## Public API
101+
102+
### `activityGuardPlugin(options)`
103+
104+
Creates the Stackflow plugin. `options.guards` is a partial map from registered
105+
Activity names to their Guards.
106+
107+
```ts
108+
interface ActivityGuardPluginOptions {
109+
guards: Guards;
110+
}
111+
```
112+
113+
### `ActivityGuardFor<ActivityName>`
114+
115+
A synchronous Guard for one registered Activity.
116+
117+
```ts
118+
type ActivityGuardFor<ActivityName extends RegisteredActivityName> = (input: {
119+
activityName: ActivityName;
120+
activityParams: InferActivityParams<ActivityName>;
121+
}) => GuardResolution;
122+
```
123+
124+
`GuardResolution` is either `true` or a redirect target. Use the exported
125+
`redirect()` helper to create a redirect resolution so that the destination
126+
name and parameters remain type-checked.
127+
128+
### `all(...guards)`
129+
130+
Combines one or more Guards for the same Activity. It evaluates them in the
131+
given order, returns the first redirect, and returns `true` only when every
132+
Guard returns `true`.
133+
134+
### `redirect(activityName, activityParams)`
135+
136+
Creates a typed redirect resolution. Calling `redirect()` does not navigate by
137+
itself; the redirect is applied only when a Guard returns the resolution.

0 commit comments

Comments
 (0)