diff --git a/app/components/EmptyState.tsx b/app/components/EmptyState.tsx
index f692946..4593cf3 100644
--- a/app/components/EmptyState.tsx
+++ b/app/components/EmptyState.tsx
@@ -1,14 +1,18 @@
+import type { ReactNode } from "react";
+
type EmptyStateProps = {
eyebrow: string;
title: string;
description: string;
actionLabel: string;
onAction?: () => void;
+ /** Optional secondary CTA/help content shown with the primary action. */
+ children?: ReactNode;
/** Optional first-time guidance steps to display below the description. */
guidanceSteps?: string[];
};
-export function EmptyState({ eyebrow, title, description, actionLabel, onAction, guidanceSteps }: EmptyStateProps) {
+export function EmptyState({ eyebrow, title, description, actionLabel, onAction, children, guidanceSteps }: EmptyStateProps) {
return (
{eyebrow}
@@ -28,6 +32,7 @@ export function EmptyState({ eyebrow, title, description, actionLabel, onAction,
+ {children}
);
}
diff --git a/app/components/StreamPrimer.tsx b/app/components/StreamPrimer.tsx
index 96da27a..caf9cbc 100644
--- a/app/components/StreamPrimer.tsx
+++ b/app/components/StreamPrimer.tsx
@@ -44,6 +44,7 @@ export const StreamPrimer = ({ onClose }: StreamPrimerProps) => {
return (
(null);
+
+ const closePrimer = useCallback(() => {
+ setIsPrimerOpen(false);
+ primerLinkRef.current?.focus();
+ }, []);
return (
@@ -147,12 +159,29 @@ export function StreamsPageContent({
onRetry={onRetry}
/>
) : isEmpty ? (
-
+ <>
+
+ {
+ event.preventDefault();
+ setIsPrimerOpen(true);
+ }}
+ ref={primerLinkRef}
+ >
+ {streamListCopy.empty.primerLabel}
+
+
+ {isPrimerOpen && }
+ >
) : (
{streams.map((stream) => (
diff --git a/app/streams/page.test.tsx b/app/streams/page.test.tsx
index 7482f7f..9183b26 100644
--- a/app/streams/page.test.tsx
+++ b/app/streams/page.test.tsx
@@ -2,7 +2,7 @@
* @jest-environment jsdom
*/
-import { render } from "@testing-library/react";
+import { fireEvent, render, waitFor } from "@testing-library/react";
const { screen } = require("@testing-library/react") as any;
import { StreamsPageContent } from "./StreamsPageContent";
@@ -12,6 +12,24 @@ describe("StreamsPageContent", () => {
expect(screen.getByRole("heading", { name: /your streams list is empty/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /create your first stream/i })).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: /what is a payment stream/i })).toBeInTheDocument();
+ });
+
+ it("opens the stream primer from the empty state and restores focus on close", async () => {
+ render();
+
+ const primerLink = screen.getByRole("link", { name: /what is a payment stream/i });
+ primerLink.focus();
+ fireEvent.click(primerLink);
+
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+ const closeButton = screen.getByRole("button", { name: /close onboarding/i });
+ expect(closeButton).toHaveFocus();
+
+ fireEvent.click(closeButton);
+
+ await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
+ expect(primerLink).toHaveFocus();
});
it("renders the loading skeleton state", () => {