Feature/onboarding updates for policies and portal (#6926)
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import React from "react";
|
||||
import { Modal, Stack } from "@mantine/core";
|
||||
import BoltRoundedIcon from "@mui/icons-material/BoltRounded";
|
||||
import GroupAddRoundedIcon from "@mui/icons-material/GroupAddRounded";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AnimatedSlideBackground from "@app/components/onboarding/slides/AnimatedSlideBackground";
|
||||
import OnboardingStepper from "@app/components/onboarding/OnboardingStepper";
|
||||
import { renderButtons } from "@app/components/onboarding/renderButtons";
|
||||
import styles from "@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css";
|
||||
import OnboardingSlideShell, {
|
||||
ShellHero,
|
||||
type ShellButton,
|
||||
} from "@app/components/onboarding/OnboardingSlideShell";
|
||||
import { useSaasOnboardingState } from "@app/components/onboarding/useSaasOnboardingState";
|
||||
import { BASE_PATH } from "@app/constants/app";
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex";
|
||||
import {
|
||||
type SlideId,
|
||||
type ButtonAction,
|
||||
type ButtonDefinition,
|
||||
} from "@app/components/onboarding/saasOnboardingFlowConfig";
|
||||
|
||||
interface SaasOnboardingModalProps {
|
||||
opened: boolean;
|
||||
@@ -20,6 +21,7 @@ interface SaasOnboardingModalProps {
|
||||
* false (slide shown) so the web (saas) flow is unchanged.
|
||||
*/
|
||||
hideDesktopInstall?: boolean;
|
||||
slideIds?: SlideId[];
|
||||
}
|
||||
|
||||
export default function SaasOnboardingModal(props: SaasOnboardingModalProps) {
|
||||
@@ -39,130 +41,45 @@ export default function SaasOnboardingModal(props: SaasOnboardingModalProps) {
|
||||
handleButtonAction,
|
||||
} = flow;
|
||||
|
||||
const renderHero = () => {
|
||||
if (slideDefinition.hero.type === "dual-icon") {
|
||||
return (
|
||||
<div className={styles.heroIconsContainer}>
|
||||
<div className={styles.iconWrapper}>
|
||||
<img
|
||||
src={`${BASE_PATH}/modern-logo/logo512.png`}
|
||||
alt="Stirling icon"
|
||||
className={styles.downloadIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (slideDefinition.hero.type === "logo") {
|
||||
return (
|
||||
<img
|
||||
src={`${BASE_PATH}/modern-logo/logo512.png`}
|
||||
alt="Stirling logo"
|
||||
className={styles.standaloneIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.heroLogoCircle}>
|
||||
{slideDefinition.hero.type === "bolt" && (
|
||||
<BoltRoundedIcon sx={{ fontSize: 64, color: "#000000" }} />
|
||||
)}
|
||||
{slideDefinition.hero.type === "team" && (
|
||||
<GroupAddRoundedIcon sx={{ fontSize: 56, color: "#000000" }} />
|
||||
)}
|
||||
</div>
|
||||
const heroType = slideDefinition.hero.type;
|
||||
const hero =
|
||||
heroType === "dual-icon" || heroType === "logo" ? (
|
||||
<ShellHero appIcon />
|
||||
) : (
|
||||
<ShellHero>
|
||||
{heroType === "bolt" && <BoltRoundedIcon sx={{ fontSize: 30 }} />}
|
||||
{heroType === "team" && <GroupAddRoundedIcon sx={{ fontSize: 30 }} />}
|
||||
</ShellHero>
|
||||
);
|
||||
|
||||
const resolveLabel = (button: ButtonDefinition) => {
|
||||
const label = button.label ?? "";
|
||||
if (!label) return "";
|
||||
const fallback = label.split(".").pop() || label;
|
||||
return t(label, fallback);
|
||||
};
|
||||
|
||||
const buttons: ShellButton[] = slideDefinition.buttons.map((button) => ({
|
||||
key: button.key,
|
||||
back: button.type === "icon",
|
||||
label: resolveLabel(button),
|
||||
primary: (button.variant ?? "secondary") === "primary",
|
||||
action: button.action,
|
||||
disabled: button.disabledWhen?.(flowState) ?? false,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<OnboardingSlideShell
|
||||
opened={props.opened}
|
||||
hero={hero}
|
||||
slideKey={currentSlide.key}
|
||||
title={currentSlide.title}
|
||||
body={currentSlide.body}
|
||||
stepIndex={currentStep}
|
||||
stepCount={totalSteps}
|
||||
buttons={buttons}
|
||||
onAction={(action) => handleButtonAction(action as ButtonAction)}
|
||||
onClose={props.onClose}
|
||||
closeOnClickOutside={false}
|
||||
centered
|
||||
size="lg"
|
||||
radius="lg"
|
||||
withCloseButton={false}
|
||||
zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}
|
||||
styles={{
|
||||
body: { padding: 0 },
|
||||
content: {
|
||||
overflow: "hidden",
|
||||
border: "none",
|
||||
background: "var(--bg-surface)",
|
||||
maxHeight: "90vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={0}
|
||||
className={styles.modalContent}
|
||||
style={{
|
||||
height: "100%",
|
||||
maxHeight: "90vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div className={styles.heroWrapper} style={{ flexShrink: 0 }}>
|
||||
<AnimatedSlideBackground
|
||||
gradientStops={currentSlide.background.gradientStops}
|
||||
circles={currentSlide.background.circles}
|
||||
isActive
|
||||
slideKey={currentSlide.key}
|
||||
/>
|
||||
<div className={styles.heroLogo} key={`logo-${currentSlide.key}`}>
|
||||
{renderHero()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.modalBody}
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}
|
||||
>
|
||||
<Stack gap={16}>
|
||||
<div
|
||||
key={`title-${currentSlide.key}`}
|
||||
className={`${styles.title} ${styles.titleText}`}
|
||||
>
|
||||
{currentSlide.title}
|
||||
</div>
|
||||
|
||||
<div className={styles.bodyText}>
|
||||
<div
|
||||
key={`body-${currentSlide.key}`}
|
||||
className={`${styles.bodyCopy} ${styles.bodyCopyInner}`}
|
||||
>
|
||||
{currentSlide.body}
|
||||
</div>
|
||||
<style>{`div strong{color: var(--onboarding-title); font-weight: 600;}`}</style>
|
||||
</div>
|
||||
|
||||
<OnboardingStepper
|
||||
totalSteps={totalSteps}
|
||||
activeStep={currentStep}
|
||||
/>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
{renderButtons({
|
||||
slideDefinition,
|
||||
flowState,
|
||||
onAction: handleButtonAction,
|
||||
t,
|
||||
})}
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</Modal>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { SlideId } from "@app/components/onboarding/saasOnboardingFlowConfig";
|
||||
import {
|
||||
resolveFlowIds,
|
||||
type FlowStep,
|
||||
} from "@app/components/onboarding/onboardingSlideTypes";
|
||||
|
||||
export interface SaasFlowInputs {
|
||||
/** Free-tier wallet with one-time allowance remaining — show the usage meter. */
|
||||
@@ -13,22 +17,23 @@ export interface SaasFlowInputs {
|
||||
hideDesktopInstall?: boolean;
|
||||
}
|
||||
|
||||
// The SaaS flow as data: free-editor and desktop-install bookend it; the usage
|
||||
// meter and team slides slot in when their conditions hold. This is the same
|
||||
// "flow = the steps that apply, in order" shape the core flow uses, resolved
|
||||
// through the shared {@link resolveFlowIds} helper.
|
||||
const SAAS_FLOW: FlowStep<SlideId, SaasFlowInputs>[] = [
|
||||
{ id: "free-editor", when: () => true },
|
||||
{ id: "usage", when: (input) => input.showUsageSlide },
|
||||
{ id: "team", when: (input) => input.showTeamSlide },
|
||||
{ id: "desktop-install", when: (input) => !input.hideDesktopInstall },
|
||||
];
|
||||
|
||||
/**
|
||||
* Resolves the SaaS onboarding slide sequence. The free-editor pitch and
|
||||
* desktop install bookend the flow; the usage meter and team slides slot in
|
||||
* when their conditions hold. When {@link SaasFlowInputs.hideDesktopInstall} is
|
||||
* set, the closing desktop-install slide is dropped (used by the desktop app,
|
||||
* which has no reason to pitch its own download).
|
||||
* Resolves the SaaS onboarding slide sequence. When
|
||||
* {@link SaasFlowInputs.hideDesktopInstall} is set, the closing desktop-install
|
||||
* slide is dropped (used by the desktop app, which has no reason to pitch its
|
||||
* own download).
|
||||
*/
|
||||
export function resolveSaasFlow({
|
||||
showUsageSlide,
|
||||
showTeamSlide,
|
||||
hideDesktopInstall = false,
|
||||
}: SaasFlowInputs): SlideId[] {
|
||||
return [
|
||||
"free-editor",
|
||||
...(showUsageSlide ? (["usage"] as const) : []),
|
||||
...(showTeamSlide ? (["team"] as const) : []),
|
||||
...(hideDesktopInstall ? [] : (["desktop-install"] as const)),
|
||||
];
|
||||
export function resolveSaasFlow(inputs: SaasFlowInputs): SlideId[] {
|
||||
return resolveFlowIds(SAAS_FLOW, inputs);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@ import FreeEditorSlide from "@app/components/onboarding/slides/FreeEditorSlide";
|
||||
import UsageSnapshotSlide from "@app/components/onboarding/slides/UsageSnapshotSlide";
|
||||
import TeamSlide from "@app/components/onboarding/slides/TeamSlide";
|
||||
import DesktopInstallSlide from "@app/components/onboarding/slides/DesktopInstallSlide";
|
||||
import { SlideConfig } from "@app/types/types";
|
||||
import type {
|
||||
OSOption,
|
||||
ButtonDefinition as ButtonDefinitionBase,
|
||||
HeroDefinition as HeroDefinitionBase,
|
||||
SlideDefinition as SlideDefinitionBase,
|
||||
} from "@app/components/onboarding/onboardingSlideTypes";
|
||||
|
||||
export type { OSOption };
|
||||
|
||||
export type SlideId = "free-editor" | "usage" | "team" | "desktop-install";
|
||||
|
||||
@@ -12,12 +19,6 @@ export type ButtonAction = "next" | "prev" | "close" | "download-selected";
|
||||
|
||||
export type FlowState = Record<string, never>;
|
||||
|
||||
export interface OSOption {
|
||||
label: string;
|
||||
url: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SlideFactoryParams {
|
||||
osLabel: string;
|
||||
osUrl: string;
|
||||
@@ -25,27 +26,17 @@ export interface SlideFactoryParams {
|
||||
onDownloadUrlChange?: (url: string) => void;
|
||||
}
|
||||
|
||||
export interface HeroDefinition {
|
||||
type: HeroType;
|
||||
}
|
||||
export type HeroDefinition = HeroDefinitionBase<HeroType>;
|
||||
|
||||
export interface ButtonDefinition {
|
||||
key: string;
|
||||
type: "button" | "icon";
|
||||
label?: string;
|
||||
icon?: "chevron-left";
|
||||
variant?: "primary" | "secondary" | "default";
|
||||
group: "left" | "right";
|
||||
action: ButtonAction;
|
||||
disabledWhen?: (state: FlowState) => boolean;
|
||||
}
|
||||
export type ButtonDefinition = ButtonDefinitionBase<ButtonAction, FlowState>;
|
||||
|
||||
export interface SlideDefinition {
|
||||
id: SlideId;
|
||||
createSlide: (params: SlideFactoryParams) => SlideConfig;
|
||||
hero: HeroDefinition;
|
||||
buttons: ButtonDefinition[];
|
||||
}
|
||||
export type SlideDefinition = SlideDefinitionBase<
|
||||
SlideId,
|
||||
ButtonAction,
|
||||
FlowState,
|
||||
HeroType,
|
||||
SlideFactoryParams
|
||||
>;
|
||||
|
||||
const BACK_BUTTON: ButtonDefinition = {
|
||||
key: "back",
|
||||
@@ -95,7 +86,6 @@ export const SLIDE_DEFINITIONS: Record<SlideId, SlideDefinition> = {
|
||||
DesktopInstallSlide({ osLabel, osUrl, osOptions, onDownloadUrlChange }),
|
||||
hero: { type: "dual-icon" },
|
||||
buttons: [
|
||||
{ ...BACK_BUTTON, key: "desktop-back" },
|
||||
{
|
||||
key: "desktop-skip",
|
||||
type: "button",
|
||||
|
||||
@@ -31,12 +31,14 @@ interface UseSaasOnboardingStateProps {
|
||||
* (slide shown) so the web (saas) flow is unchanged.
|
||||
*/
|
||||
hideDesktopInstall?: boolean;
|
||||
slideIds?: SlideId[];
|
||||
}
|
||||
|
||||
export function useSaasOnboardingState({
|
||||
opened,
|
||||
onClose,
|
||||
hideDesktopInstall = false,
|
||||
slideIds,
|
||||
}: UseSaasOnboardingStateProps): UseSaasOnboardingStateResult | null {
|
||||
const { loading } = useAuth();
|
||||
const { wallet } = useWallet();
|
||||
@@ -89,8 +91,9 @@ export function useSaasOnboardingState({
|
||||
|
||||
const flowSlideIds = useMemo(
|
||||
() =>
|
||||
slideIds ??
|
||||
resolveSaasFlow({ showUsageSlide, showTeamSlide, hideDesktopInstall }),
|
||||
[showUsageSlide, showTeamSlide, hideDesktopInstall],
|
||||
[slideIds, showUsageSlide, showTeamSlide, hideDesktopInstall],
|
||||
);
|
||||
const totalSteps = flowSlideIds.length;
|
||||
const maxIndex = Math.max(totalSteps - 1, 0);
|
||||
|
||||
Reference in New Issue
Block a user