@wireai/activation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +73 -0
- package/CHANGELOG.md +572 -0
- package/INTEGRATION_PROMPT.md +55 -0
- package/LICENSE +21 -0
- package/README.md +923 -0
- package/dist/CenteredModal-C3qQBHsA.d.mts +38 -0
- package/dist/CenteredModal-Cdgns6--.d.ts +38 -0
- package/dist/coachmarks/index.d.mts +352 -0
- package/dist/coachmarks/index.d.ts +352 -0
- package/dist/coachmarks/index.js +944 -0
- package/dist/coachmarks/index.js.map +1 -0
- package/dist/coachmarks/index.mjs +918 -0
- package/dist/coachmarks/index.mjs.map +1 -0
- package/dist/decision-CFvGY6nP.d.ts +228 -0
- package/dist/decision-Cl8OFYzu.d.mts +228 -0
- package/dist/index.d.mts +1390 -0
- package/dist/index.d.ts +1390 -0
- package/dist/index.js +3260 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3186 -0
- package/dist/index.mjs.map +1 -0
- package/dist/questionnaire/index.d.mts +249 -0
- package/dist/questionnaire/index.d.ts +249 -0
- package/dist/questionnaire/index.js +922 -0
- package/dist/questionnaire/index.js.map +1 -0
- package/dist/questionnaire/index.mjs +906 -0
- package/dist/questionnaire/index.mjs.map +1 -0
- package/dist/reviews/index.d.mts +213 -0
- package/dist/reviews/index.d.ts +213 -0
- package/dist/reviews/index.js +1001 -0
- package/dist/reviews/index.js.map +1 -0
- package/dist/reviews/index.mjs +971 -0
- package/dist/reviews/index.mjs.map +1 -0
- package/dist/showcase/index.d.mts +119 -0
- package/dist/showcase/index.d.ts +119 -0
- package/dist/showcase/index.js +660 -0
- package/dist/showcase/index.js.map +1 -0
- package/dist/showcase/index.mjs +651 -0
- package/dist/showcase/index.mjs.map +1 -0
- package/dist/types-BKfpdZzX.d.mts +84 -0
- package/dist/types-BKfpdZzX.d.ts +84 -0
- package/dist/types-CMuOexw0.d.mts +116 -0
- package/dist/types-CMuOexw0.d.ts +116 -0
- package/dist/types-GL_hQ0TN.d.mts +46 -0
- package/dist/types-GL_hQ0TN.d.ts +46 -0
- package/llms.txt +36 -0
- package/metro/index.d.ts +32 -0
- package/metro/index.js +83 -0
- package/package.json +171 -0
- package/src/DemoOnboarding.tsx +133 -0
- package/src/OnboardingFlow.tsx +504 -0
- package/src/WireOnboarding.tsx +248 -0
- package/src/analytics/analyticsEvent.ts +69 -0
- package/src/analytics/reportClientEvent.ts +119 -0
- package/src/analytics/sendPreview.ts +73 -0
- package/src/attribution/attribution.ts +46 -0
- package/src/cards/ChipSelectCard.tsx +265 -0
- package/src/cards/InterstitialCard.tsx +299 -0
- package/src/cards/NumberStepperCard.tsx +121 -0
- package/src/cards/SelectionCard.tsx +270 -0
- package/src/cards/StatusCard.tsx +170 -0
- package/src/cards/TextInputCard.tsx +147 -0
- package/src/cards/index.ts +23 -0
- package/src/coachmarks/CoachmarkOverlayHost.tsx +44 -0
- package/src/coachmarks/CoachmarkProvider.tsx +108 -0
- package/src/coachmarks/GestureHint.tsx +331 -0
- package/src/coachmarks/SpotlightOverlay.tsx +254 -0
- package/src/coachmarks/coachmarkAnchorRegistry.ts +70 -0
- package/src/coachmarks/coachmarkOverlayStore.ts +63 -0
- package/src/coachmarks/index.ts +62 -0
- package/src/coachmarks/runtime.ts +103 -0
- package/src/coachmarks/selectTourSteps.ts +45 -0
- package/src/coachmarks/types.ts +58 -0
- package/src/coachmarks/useCoachmarkAnchor.ts +49 -0
- package/src/coachmarks/useCoachmarkTour.ts +200 -0
- package/src/components/AnimatedSparkle.tsx +101 -0
- package/src/components/Button.tsx +129 -0
- package/src/components/CardHandoff.tsx +212 -0
- package/src/components/CardLayout.tsx +103 -0
- package/src/components/CenteredModal.tsx +184 -0
- package/src/components/CompletionView.tsx +186 -0
- package/src/components/DoneBlock.tsx +37 -0
- package/src/components/ErrorBlock.tsx +42 -0
- package/src/components/Illustration.tsx +36 -0
- package/src/components/LoadingBlock.tsx +182 -0
- package/src/components/LoadingScreen.tsx +35 -0
- package/src/components/OnboardingScaffold.tsx +105 -0
- package/src/components/StepProgress.tsx +85 -0
- package/src/components/loaderChrome.ts +28 -0
- package/src/config/onboardingFlag.ts +39 -0
- package/src/config/wireConfigFromEnv.ts +52 -0
- package/src/device/deviceContext.ts +158 -0
- package/src/features/WireFeaturesProvider.tsx +81 -0
- package/src/features/cache.ts +92 -0
- package/src/features/defaults.ts +67 -0
- package/src/features/fetchWireFeatures.ts +86 -0
- package/src/features/index.ts +39 -0
- package/src/features/types.ts +61 -0
- package/src/features/useWireFeatures.ts +57 -0
- package/src/identity/userIdentity.ts +100 -0
- package/src/illustrations/defaultIllustrations.tsx +99 -0
- package/src/index.ts +159 -0
- package/src/motion/centeredModalMotion.ts +45 -0
- package/src/motion/motionSpec.ts +133 -0
- package/src/motion/useReducedMotion.ts +43 -0
- package/src/questionnaire/QuestionnaireGate.tsx +235 -0
- package/src/questionnaire/decision.ts +63 -0
- package/src/questionnaire/index.ts +54 -0
- package/src/questionnaire/runtime.ts +20 -0
- package/src/questionnaire/transport.ts +83 -0
- package/src/questionnaire/types.ts +182 -0
- package/src/questionnaire/useQuestionnaireGate.ts +159 -0
- package/src/reviews/ReviewGate.tsx +316 -0
- package/src/reviews/ReviewModal.tsx +13 -0
- package/src/reviews/decision.ts +120 -0
- package/src/reviews/equality.ts +54 -0
- package/src/reviews/expo-store-review.d.ts +16 -0
- package/src/reviews/feedbackForm.ts +41 -0
- package/src/reviews/index.ts +78 -0
- package/src/reviews/presentation.ts +29 -0
- package/src/reviews/runtime.ts +45 -0
- package/src/reviews/storeReview.ts +62 -0
- package/src/reviews/transport.ts +89 -0
- package/src/reviews/types.ts +172 -0
- package/src/reviews/useReviewGate.ts +137 -0
- package/src/session/persistedSession.ts +143 -0
- package/src/session-analytics/index.ts +18 -0
- package/src/session-analytics/reportSessionStart.ts +144 -0
- package/src/session-analytics/useSessionStart.ts +114 -0
- package/src/showcase/FeatureShowcase.tsx +233 -0
- package/src/showcase/index.ts +19 -0
- package/src/showcase/selectShowcaseSlides.ts +45 -0
- package/src/showcase/showcaseColors.ts +91 -0
- package/src/showcase/types.ts +74 -0
- package/src/theme/ThemeContext.tsx +38 -0
- package/src/theme/defaultTheme.ts +70 -0
- package/src/theme/themeFromBrand.ts +93 -0
- package/src/theme/types.ts +88 -0
- package/src/theme/typography.ts +46 -0
- package/src/types.ts +215 -0
- package/src/utils/deriveAnswers.ts +43 -0
- package/src/utils/readProgress.ts +33 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ImageSourcePropType } from "react-native";
|
|
2
|
+
|
|
3
|
+
import type { GestureKind } from "../coachmarks/types";
|
|
4
|
+
import type { OnboardingTheme } from "../theme/types";
|
|
5
|
+
import type { CoachmarkStorage } from "../coachmarks/types";
|
|
6
|
+
import type { WireFeatures, WireFeaturesConfig } from "../features/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One slide in the pre-onboarding feature showcase (the "app intro"). Maps onto
|
|
10
|
+
* a `@blazejkustra/react-native-onboarding` step, with an optional perf-tuned
|
|
11
|
+
* GestureHint overlaid on top. An `image` is strongly recommended — the
|
|
12
|
+
* underlying package renders an image area per slide; slides without one fall
|
|
13
|
+
* back to a transparent placeholder so nothing crashes.
|
|
14
|
+
*/
|
|
15
|
+
export interface ShowcaseSlide {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
image?: ImageSourcePropType;
|
|
20
|
+
/** Where the image sits relative to the copy. Default `top`. */
|
|
21
|
+
imagePosition?: "top" | "bottom";
|
|
22
|
+
/** Per-slide primary button label; falls back to the showcase default. */
|
|
23
|
+
buttonLabel?: string;
|
|
24
|
+
/** Optional animated hand overlaid on the slide (the perf-tuned GestureHint). */
|
|
25
|
+
gesture?: GestureKind;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Optional intro panel shown before the slides begin. */
|
|
29
|
+
export interface ShowcaseIntroPanel {
|
|
30
|
+
title?: string;
|
|
31
|
+
subtitle?: string;
|
|
32
|
+
/** Start-button label (required by the underlying package). */
|
|
33
|
+
button: string;
|
|
34
|
+
image?: ImageSourcePropType;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The whole declarative feature-showcase an app supplies. */
|
|
38
|
+
export interface ShowcaseConfig {
|
|
39
|
+
/** Gate key — persisted as `wire_showcase_<id>_seen`. */
|
|
40
|
+
id: string;
|
|
41
|
+
introPanel?: ShowcaseIntroPanel;
|
|
42
|
+
slides: ShowcaseSlide[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FeatureShowcaseProps {
|
|
46
|
+
config: ShowcaseConfig;
|
|
47
|
+
/** Called when the showcase completes, is skipped, or was already seen. */
|
|
48
|
+
onDone: () => void;
|
|
49
|
+
/** Partial theme merged over the active onboarding theme for the showcase colors. */
|
|
50
|
+
theme?: Partial<OnboardingTheme>;
|
|
51
|
+
/** Accent override (buttons + emphasis). Defaults to the theme's `primary`. */
|
|
52
|
+
accentColor?: string;
|
|
53
|
+
/** Default primary button label when a slide/intro omits one. Default "Next". */
|
|
54
|
+
buttonLabel?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Sync gate storage. Defaults to the storage injected by CoachmarkProvider, so
|
|
57
|
+
* you usually don't pass this — the showcase gates through the same store as
|
|
58
|
+
* the coachmark tours.
|
|
59
|
+
*/
|
|
60
|
+
storage?: CoachmarkStorage | null;
|
|
61
|
+
/** Testing override; defaults to the global `isTestingCoachmark` flag. */
|
|
62
|
+
isTesting?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* OPTIONAL feature-flag wiring. When `flags.showcase.enabled` is false, the showcase renders
|
|
65
|
+
* NOTHING and calls `onDone()` from an effect so the host's navigation flow continues exactly
|
|
66
|
+
* as if it had completed — it can never strand a flow waiting on a screen that won't appear. It
|
|
67
|
+
* does NOT write the seen gate, so re-enabling replays it. Provide pre-resolved `features`, OR a
|
|
68
|
+
* `featuresConfig` to lazily fetch once, OR mount a `WireFeaturesProvider` above (context is
|
|
69
|
+
* read automatically). Omit all three and the showcase stays on (fail-open).
|
|
70
|
+
*/
|
|
71
|
+
features?: WireFeatures;
|
|
72
|
+
/** Lazy-fetch config for the flags, used when `features` is absent and no provider is above. */
|
|
73
|
+
featuresConfig?: WireFeaturesConfig;
|
|
74
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeContext — provides the resolved OnboardingTheme to every shell element and
|
|
3
|
+
* themed card via `useOnboardingTheme()`. WHY a context rather than prop-drilling:
|
|
4
|
+
* the cards are mounted by the SDK's ComponentRenderer (not by us), so we can't
|
|
5
|
+
* pass them props directly — they must read the theme from context.
|
|
6
|
+
*/
|
|
7
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
8
|
+
import { mergeTheme } from "./defaultTheme";
|
|
9
|
+
import type { OnboardingTheme } from "./types";
|
|
10
|
+
|
|
11
|
+
const OnboardingThemeContext = createContext<OnboardingTheme | null>(null);
|
|
12
|
+
|
|
13
|
+
export type OnboardingThemeProviderProps = {
|
|
14
|
+
/** Partial overrides merged over the neutral default theme. */
|
|
15
|
+
theme?: Partial<OnboardingTheme>;
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const OnboardingThemeProvider: React.FC<OnboardingThemeProviderProps> = ({
|
|
20
|
+
theme,
|
|
21
|
+
children,
|
|
22
|
+
}) => {
|
|
23
|
+
const resolved = useMemo(() => mergeTheme(theme), [theme]);
|
|
24
|
+
return (
|
|
25
|
+
<OnboardingThemeContext.Provider value={resolved}>
|
|
26
|
+
{children}
|
|
27
|
+
</OnboardingThemeContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read the active theme. Falls back to the default theme when used outside a
|
|
33
|
+
* provider so a stray card never crashes — it just renders neutral.
|
|
34
|
+
*/
|
|
35
|
+
export const useOnboardingTheme = (): OnboardingTheme => {
|
|
36
|
+
const ctx = useContext(OnboardingThemeContext);
|
|
37
|
+
return ctx ?? mergeTheme();
|
|
38
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defaultOnboardingTheme — a neutral, light, brand-agnostic baseline. Consumers
|
|
3
|
+
* pass a `Partial<OnboardingTheme>` that is deep-merged over this (see
|
|
4
|
+
* `mergeTheme`), so they only override what differs from neutral.
|
|
5
|
+
*/
|
|
6
|
+
import type { OnboardingTheme } from "./types";
|
|
7
|
+
|
|
8
|
+
export const defaultOnboardingTheme: OnboardingTheme = {
|
|
9
|
+
colors: {
|
|
10
|
+
background: "#FFFFFF",
|
|
11
|
+
surface: "#F7F7F8",
|
|
12
|
+
text: "#101114",
|
|
13
|
+
textMuted: "#6B7280",
|
|
14
|
+
primary: "#111111",
|
|
15
|
+
onPrimary: "#FFFFFF",
|
|
16
|
+
primarySoft: "rgba(17,17,17,0.06)",
|
|
17
|
+
border: "#E4E4E7",
|
|
18
|
+
borderStrong: "#C7C7CC",
|
|
19
|
+
disabled: "#EBEBEB",
|
|
20
|
+
success: "#16A34A",
|
|
21
|
+
warning: "#D97706",
|
|
22
|
+
error: "#DC2626",
|
|
23
|
+
info: "#2563EB",
|
|
24
|
+
},
|
|
25
|
+
fonts: {
|
|
26
|
+
regular: undefined,
|
|
27
|
+
medium: undefined,
|
|
28
|
+
bold: undefined,
|
|
29
|
+
baseSize: 16,
|
|
30
|
+
scale: 1,
|
|
31
|
+
},
|
|
32
|
+
radius: {
|
|
33
|
+
sm: 8,
|
|
34
|
+
md: 12,
|
|
35
|
+
lg: 16,
|
|
36
|
+
full: 9999,
|
|
37
|
+
},
|
|
38
|
+
spacing: {
|
|
39
|
+
xs: 4,
|
|
40
|
+
sm: 8,
|
|
41
|
+
md: 16,
|
|
42
|
+
lg: 24,
|
|
43
|
+
xl: 32,
|
|
44
|
+
},
|
|
45
|
+
// Circled (pill) CTA by default; an app can pass `button: { radius: 12 }` for
|
|
46
|
+
// a rounded rectangle, or override height/paddingX to match its design system.
|
|
47
|
+
button: {
|
|
48
|
+
radius: 9999,
|
|
49
|
+
height: 52,
|
|
50
|
+
paddingX: 24,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Deep-merge a partial theme over the default. Two levels deep is enough — the
|
|
56
|
+
* theme is intentionally shallow (colors/fonts/radius/spacing), so we merge each
|
|
57
|
+
* known sub-object and leave scalars to overwrite.
|
|
58
|
+
*/
|
|
59
|
+
export const mergeTheme = (
|
|
60
|
+
override?: Partial<OnboardingTheme>,
|
|
61
|
+
): OnboardingTheme => {
|
|
62
|
+
if (!override) return defaultOnboardingTheme;
|
|
63
|
+
return {
|
|
64
|
+
colors: { ...defaultOnboardingTheme.colors, ...override.colors },
|
|
65
|
+
fonts: { ...defaultOnboardingTheme.fonts, ...override.fonts },
|
|
66
|
+
radius: { ...defaultOnboardingTheme.radius, ...override.radius },
|
|
67
|
+
spacing: { ...defaultOnboardingTheme.spacing, ...override.spacing },
|
|
68
|
+
button: { ...defaultOnboardingTheme.button, ...override.button },
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* themeFromBrand — the fastest path to a branded flow. Give it a primary brand
|
|
3
|
+
* color (and optionally a background + a font), and it derives a sensible full
|
|
4
|
+
* `Partial<OnboardingTheme>` over the neutral `defaultOnboardingTheme` (via
|
|
5
|
+
* `mergeTheme`): a tinted soft wash for selected states, a readable on-primary
|
|
6
|
+
* label, and matching success/info accents left at the neutral default.
|
|
7
|
+
*
|
|
8
|
+
* theme={themeFromBrand({ primary: "#15B0B0" })}
|
|
9
|
+
*
|
|
10
|
+
* It stays intentionally small: it only touches the values that flow from a brand
|
|
11
|
+
* color. Anything else (radius, spacing, button shape) keeps the default; pass
|
|
12
|
+
* `extra` to override those without re-authoring the palette.
|
|
13
|
+
*/
|
|
14
|
+
import { defaultOnboardingTheme, mergeTheme } from "./defaultTheme";
|
|
15
|
+
import type { OnboardingTheme } from "./types";
|
|
16
|
+
|
|
17
|
+
export type ThemeFromBrandInput = {
|
|
18
|
+
/** Brand / accent color — drives selected states, primary buttons, progress fill. */
|
|
19
|
+
primary: string;
|
|
20
|
+
/** App background behind the whole flow. Default: the neutral light background. */
|
|
21
|
+
background?: string;
|
|
22
|
+
/** Card / elevated surface background. Default: the neutral surface. */
|
|
23
|
+
surface?: string;
|
|
24
|
+
/** Primary body text color. Default: the neutral near-black. */
|
|
25
|
+
text?: string;
|
|
26
|
+
/** Text/icon color on top of `primary`. Default: white. */
|
|
27
|
+
onPrimary?: string;
|
|
28
|
+
/**
|
|
29
|
+
* A single font family applied to regular/medium/bold (the kit derives weight
|
|
30
|
+
* from size when a family isn't weight-split). Pass `fonts` in `extra` for
|
|
31
|
+
* per-weight families.
|
|
32
|
+
*/
|
|
33
|
+
font?: string;
|
|
34
|
+
/** Any further theme overrides, deep-merged last (radius/spacing/button/fonts/colors). */
|
|
35
|
+
extra?: Partial<OnboardingTheme>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Parse a #RGB or #RRGGBB hex into [r,g,b]; returns null for anything else (e.g. rgba()). */
|
|
39
|
+
const hexToRgb = (hex: string): [number, number, number] | null => {
|
|
40
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
|
41
|
+
if (!m) return null;
|
|
42
|
+
let h = m[1] as string;
|
|
43
|
+
if (h.length === 3) h = h[0]! + h[0]! + h[1]! + h[1]! + h[2]! + h[2]!;
|
|
44
|
+
const int = parseInt(h, 16);
|
|
45
|
+
return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** A low-alpha wash of the primary for selected backgrounds, falling back gracefully. */
|
|
49
|
+
const softFromPrimary = (primary: string): string => {
|
|
50
|
+
const rgb = hexToRgb(primary);
|
|
51
|
+
if (!rgb) return defaultOnboardingTheme.colors.primarySoft;
|
|
52
|
+
const [r, g, b] = rgb;
|
|
53
|
+
return `rgba(${r},${g},${b},0.10)`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const themeFromBrand = (input: ThemeFromBrandInput): Partial<OnboardingTheme> => {
|
|
57
|
+
const { primary, background, surface, text, onPrimary, font, extra } = input;
|
|
58
|
+
|
|
59
|
+
const derived: Partial<OnboardingTheme> = {
|
|
60
|
+
colors: {
|
|
61
|
+
...defaultOnboardingTheme.colors,
|
|
62
|
+
primary,
|
|
63
|
+
primarySoft: softFromPrimary(primary),
|
|
64
|
+
onPrimary: onPrimary ?? defaultOnboardingTheme.colors.onPrimary,
|
|
65
|
+
...(background ? { background } : {}),
|
|
66
|
+
...(surface ? { surface } : {}),
|
|
67
|
+
...(text ? { text } : {}),
|
|
68
|
+
},
|
|
69
|
+
...(font
|
|
70
|
+
? {
|
|
71
|
+
fonts: {
|
|
72
|
+
...defaultOnboardingTheme.fonts,
|
|
73
|
+
regular: font,
|
|
74
|
+
medium: font,
|
|
75
|
+
bold: font,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// mergeTheme resolves the derived palette over the neutral default into a full
|
|
82
|
+
// theme. Then deep-merge `extra` per sub-object so callers can override
|
|
83
|
+
// radius/spacing/button/fonts/colors without re-authoring the palette.
|
|
84
|
+
const base = mergeTheme(derived);
|
|
85
|
+
if (!extra) return base;
|
|
86
|
+
return {
|
|
87
|
+
colors: { ...base.colors, ...extra.colors },
|
|
88
|
+
fonts: { ...base.fonts, ...extra.fonts },
|
|
89
|
+
radius: { ...base.radius, ...extra.radius },
|
|
90
|
+
spacing: { ...base.spacing, ...extra.spacing },
|
|
91
|
+
button: { ...base.button, ...extra.button },
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OnboardingTheme — the single source of truth for every visual value the kit
|
|
3
|
+
* renders. It deliberately mirrors the SHAPE of the OSS SDK's design tokens
|
|
4
|
+
* (`wire-rn/.../styles/tokens.ts`: colors / radii / spacing / textStyles) so the
|
|
5
|
+
* themed cards port over cleanly — but here EVERY value comes from the theme, so
|
|
6
|
+
* a consumer app can repaint the whole flow to match its brand.
|
|
7
|
+
*
|
|
8
|
+
* WHY a flat-ish object (not nested per-component): a host app already has its
|
|
9
|
+
* own palette; mapping it onto one object once is far less friction than theming
|
|
10
|
+
* each card. Cards read semantic roles (surface, primary, border…), never raw hex.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type OnboardingColors = {
|
|
14
|
+
/** App background behind the whole flow. */
|
|
15
|
+
background: string;
|
|
16
|
+
/** Card / elevated surface background. */
|
|
17
|
+
surface: string;
|
|
18
|
+
/** Primary body text. */
|
|
19
|
+
text: string;
|
|
20
|
+
/** Secondary / hint / placeholder text. */
|
|
21
|
+
textMuted: string;
|
|
22
|
+
/** Brand / accent — selected states, primary buttons, progress fill. */
|
|
23
|
+
primary: string;
|
|
24
|
+
/** Text/icon color on top of `primary` (e.g. primary button label). */
|
|
25
|
+
onPrimary: string;
|
|
26
|
+
/** A tinted wash of `primary` for selected backgrounds. */
|
|
27
|
+
primarySoft: string;
|
|
28
|
+
/** Hairline borders + unselected outlines. */
|
|
29
|
+
border: string;
|
|
30
|
+
/** Stronger border for emphasis. */
|
|
31
|
+
borderStrong: string;
|
|
32
|
+
/** Disabled control fill. */
|
|
33
|
+
disabled: string;
|
|
34
|
+
success: string;
|
|
35
|
+
warning: string;
|
|
36
|
+
error: string;
|
|
37
|
+
info: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type OnboardingFonts = {
|
|
41
|
+
/** Family for normal-weight text. Pass undefined to use the platform default. */
|
|
42
|
+
regular?: string;
|
|
43
|
+
/** Family for medium-weight text. */
|
|
44
|
+
medium?: string;
|
|
45
|
+
/** Family for bold/heading text. */
|
|
46
|
+
bold?: string;
|
|
47
|
+
/** Base body font size in px. The text scale derives from this × `scale`. */
|
|
48
|
+
baseSize: number;
|
|
49
|
+
/** Multiplier applied to derived sizes (e.g. 1.1 for a larger flow). */
|
|
50
|
+
scale: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type OnboardingRadius = {
|
|
54
|
+
sm: number;
|
|
55
|
+
md: number;
|
|
56
|
+
lg: number;
|
|
57
|
+
full: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type OnboardingSpacing = {
|
|
61
|
+
xs: number;
|
|
62
|
+
sm: number;
|
|
63
|
+
md: number;
|
|
64
|
+
lg: number;
|
|
65
|
+
xl: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Button shape + size, as its own token so an app can decide how its CTAs look
|
|
70
|
+
* without touching card internals. Set `radius` to a large value (e.g. 9999) for
|
|
71
|
+
* a fully circled pill, or a small one (e.g. 12) for a rounded rectangle.
|
|
72
|
+
*/
|
|
73
|
+
export type OnboardingButtonStyle = {
|
|
74
|
+
/** Corner radius. 9999 = fully circled pill; ~12 = rounded rectangle. */
|
|
75
|
+
radius: number;
|
|
76
|
+
/** Minimum touch-target height. */
|
|
77
|
+
height: number;
|
|
78
|
+
/** Horizontal padding (the button's own spacing). */
|
|
79
|
+
paddingX: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type OnboardingTheme = {
|
|
83
|
+
colors: OnboardingColors;
|
|
84
|
+
fonts: OnboardingFonts;
|
|
85
|
+
radius: OnboardingRadius;
|
|
86
|
+
spacing: OnboardingSpacing;
|
|
87
|
+
button: OnboardingButtonStyle;
|
|
88
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* typography — derive concrete RN text styles from the theme's font config, so
|
|
3
|
+
* every label/title/caption in the kit scales from one `baseSize` + `scale` and
|
|
4
|
+
* picks the right family. Cards call these instead of hardcoding fontSize/weight.
|
|
5
|
+
*/
|
|
6
|
+
import type { TextStyle } from "react-native";
|
|
7
|
+
import type { OnboardingFonts } from "./types";
|
|
8
|
+
|
|
9
|
+
const round = (n: number) => Math.round(n);
|
|
10
|
+
|
|
11
|
+
/** Body text — the reference size. */
|
|
12
|
+
export const bodyStyle = (f: OnboardingFonts): TextStyle => ({
|
|
13
|
+
fontFamily: f.regular,
|
|
14
|
+
fontSize: round(f.baseSize * f.scale),
|
|
15
|
+
lineHeight: round(f.baseSize * f.scale * 1.5),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Slightly smaller secondary/hint text. */
|
|
19
|
+
export const captionStyle = (f: OnboardingFonts): TextStyle => ({
|
|
20
|
+
fontFamily: f.regular,
|
|
21
|
+
fontSize: round(f.baseSize * 0.8125 * f.scale),
|
|
22
|
+
lineHeight: round(f.baseSize * 0.8125 * f.scale * 1.5),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Emphasised label (e.g. a card question). */
|
|
26
|
+
export const labelStyle = (f: OnboardingFonts): TextStyle => ({
|
|
27
|
+
fontFamily: f.medium ?? f.bold,
|
|
28
|
+
fontSize: round(f.baseSize * f.scale),
|
|
29
|
+
lineHeight: round(f.baseSize * f.scale * 1.4),
|
|
30
|
+
fontWeight: f.medium ? undefined : ("600" as const),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Heading (status title, done block). */
|
|
34
|
+
export const headingStyle = (f: OnboardingFonts): TextStyle => ({
|
|
35
|
+
fontFamily: f.bold,
|
|
36
|
+
fontSize: round(f.baseSize * 1.25 * f.scale),
|
|
37
|
+
lineHeight: round(f.baseSize * 1.25 * f.scale * 1.3),
|
|
38
|
+
fontWeight: f.bold ? undefined : ("700" as const),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** Button label. */
|
|
42
|
+
export const buttonLabelStyle = (f: OnboardingFonts): TextStyle => ({
|
|
43
|
+
fontFamily: f.bold ?? f.medium,
|
|
44
|
+
fontSize: round(f.baseSize * f.scale),
|
|
45
|
+
fontWeight: f.bold ? undefined : ("600" as const),
|
|
46
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the wireai-onboarding kit.
|
|
3
|
+
*/
|
|
4
|
+
import type { Message } from "wireai-rn";
|
|
5
|
+
import type { OnboardingTheme } from "./theme/types";
|
|
6
|
+
import type { WireOnboardingStorage } from "./session/persistedSession";
|
|
7
|
+
|
|
8
|
+
export type { OnboardingTheme } from "./theme/types";
|
|
9
|
+
export type { WireOnboardingStorage } from "./session/persistedSession";
|
|
10
|
+
|
|
11
|
+
/** Transport + tenant config for the managed Wire AI onboarding backend (A2A). */
|
|
12
|
+
export type WireOnboardingConfig = {
|
|
13
|
+
/** Tenant API key (resolves the app server-side). */
|
|
14
|
+
apiKey: string;
|
|
15
|
+
/** Base server URL; the kit appends `/a2a`. */
|
|
16
|
+
serverUrl: string;
|
|
17
|
+
/** App id — passed as the A2A `model` (informational; the key resolves the app). */
|
|
18
|
+
appId: string;
|
|
19
|
+
/**
|
|
20
|
+
* Extra metadata merged into every A2A request (e.g. install attribution).
|
|
21
|
+
* NOTE: the kit reserves `sessionId` (correlation seed) and `supportedComponents`
|
|
22
|
+
* (the renderable card names this device advertises to the backend) — it sets both
|
|
23
|
+
* automatically, so don't override them here.
|
|
24
|
+
*/
|
|
25
|
+
metadata?: Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Host app version string (e.g. "1.4.2"). HOST-INJECTED — the kit adds no dependency to
|
|
28
|
+
* read it; hosts typically pass it from `expo-constants`
|
|
29
|
+
* (`Constants.expoConfig?.version`). Forwarded to the backend on the session metadata and
|
|
30
|
+
* on client events (merged into the `device` snapshot as `device.appVersion`) so analytics
|
|
31
|
+
* can segment the funnel by app version. Optional; omit if unknown.
|
|
32
|
+
*/
|
|
33
|
+
appVersion?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A per-step validator, keyed by a base-question `key` (e.g. `username`). Runs
|
|
38
|
+
* before the answer is sent to the backend. Return `{ ok: false, error }` to
|
|
39
|
+
* block advancing and show the error inline on the card.
|
|
40
|
+
*/
|
|
41
|
+
export type StepValidator = (value: string) => Promise<{ ok: boolean; error?: string }>;
|
|
42
|
+
|
|
43
|
+
/** Result handed to `onComplete` when the flow reaches its terminal StatusCard. */
|
|
44
|
+
export type OnboardingResult = {
|
|
45
|
+
/** Question-key → captured value, derived from the thread. */
|
|
46
|
+
answers: Record<string, unknown>;
|
|
47
|
+
/** The raw message thread, for custom downstream parsing. */
|
|
48
|
+
raw: Message[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Lifecycle events emitted as the flow runs, for host-side analytics. The kit owns
|
|
53
|
+
* the thread loop, so this is how a host recovers per-turn telemetry it used to get
|
|
54
|
+
* by driving the loop itself.
|
|
55
|
+
* - `started`: the very first message was sent to the backend. Carries `contextId` (the
|
|
56
|
+
* A2A session id). Capture it if you may need to bind a user AFTER the flow
|
|
57
|
+
* finishes (see `identifyOnboarding` and the `userId` prop).
|
|
58
|
+
* - `resumed`: a persisted session was restored after an app kill (fires INSTEAD of
|
|
59
|
+
* `started`, so host funnels don't double-count the same session). Also carries
|
|
60
|
+
* `contextId`. Requires the `storage` prop.
|
|
61
|
+
* - `turn`: a new assistant card arrived (`step` = 1-based index of cards seen).
|
|
62
|
+
* - `error`: the backend errored or the first-card watchdog timed out.
|
|
63
|
+
* - `retry`: a transient failure is being auto-retried (`attempt` = 1-based).
|
|
64
|
+
* - `fallback`: retries are exhausted; the kit degraded to the static `fallbackFlow`
|
|
65
|
+
* (or handed off to `onError`). This is the client-side mirror of the
|
|
66
|
+
* backend's `llm_fallback` reliability event.
|
|
67
|
+
*/
|
|
68
|
+
export type OnboardingEvent =
|
|
69
|
+
| { type: "started"; contextId: string }
|
|
70
|
+
| { type: "resumed"; contextId: string }
|
|
71
|
+
| { type: "turn"; step: number; component?: string }
|
|
72
|
+
| { type: "error"; reason: "backend" | "timeout" }
|
|
73
|
+
| { type: "retry"; reason: "backend" | "timeout"; attempt: number }
|
|
74
|
+
| { type: "fallback"; reason: "backend" | "timeout" };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Copy overrides for the kit's built-in (English) strings, so a host can localize
|
|
78
|
+
* the loaders / completion fallback via its own i18n. Any field left unset keeps
|
|
79
|
+
* the kit default.
|
|
80
|
+
*/
|
|
81
|
+
export type OnboardingCopy = {
|
|
82
|
+
/** First "thinking" screen, before any card arrives. */
|
|
83
|
+
startingTitle: string;
|
|
84
|
+
startingHint: string;
|
|
85
|
+
/** While the persisted session id is being restored from `storage` (pre-mount gate). */
|
|
86
|
+
restoringTitle: string;
|
|
87
|
+
restoringHint: string;
|
|
88
|
+
/** Between-turns loader, while the next card is generated. */
|
|
89
|
+
thinkingTitle: string;
|
|
90
|
+
thinkingHint: string;
|
|
91
|
+
/** While a per-step validator is running. */
|
|
92
|
+
checkingTitle: string;
|
|
93
|
+
checkingHint: string;
|
|
94
|
+
/** Completion fallback when the backend omits a title/CTA. */
|
|
95
|
+
completeTitle: string;
|
|
96
|
+
completeCta: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type WireOnboardingProps = {
|
|
100
|
+
config: WireOnboardingConfig;
|
|
101
|
+
/** Partial theme merged over the neutral default. */
|
|
102
|
+
theme?: Partial<OnboardingTheme>;
|
|
103
|
+
/** Override the registered cards (defaults to the kit's `onboardingComponents`). */
|
|
104
|
+
components?: import("wireai-rn").WireAIComponent[];
|
|
105
|
+
/**
|
|
106
|
+
* App-supplied artwork for InterstitialCard, keyed by name. The backend names one
|
|
107
|
+
* via `illustration`; the kit slots the matching node (keeping itself dependency-free).
|
|
108
|
+
*/
|
|
109
|
+
illustrations?: Record<string, import("react").ReactNode>;
|
|
110
|
+
/** Per-step validators keyed by base-question key, e.g. `{ username: checkUsername }`. */
|
|
111
|
+
validators?: Record<string, StepValidator>;
|
|
112
|
+
/** Fired once the flow reaches its terminal StatusCard. */
|
|
113
|
+
onComplete: (result: OnboardingResult) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Retained for back-compat. NOTE: per-question Skip is now INTERNAL — the kit shows
|
|
116
|
+
* a Skip control only on screens the backend marks `skippable`, and it advances ONE
|
|
117
|
+
* question (it does not exit the flow). This callback is no longer wired to that control.
|
|
118
|
+
*/
|
|
119
|
+
onSkip?: () => void;
|
|
120
|
+
/**
|
|
121
|
+
* Fired on a backend error or first-card timeout, AFTER retries are exhausted.
|
|
122
|
+
* When `fallbackFlow` is NOT supplied and this is provided, the host owns recovery
|
|
123
|
+
* (e.g. navigate to its own static onboarding) instead of the kit's inline retry.
|
|
124
|
+
*/
|
|
125
|
+
onError?: (err: unknown) => void;
|
|
126
|
+
/**
|
|
127
|
+
* Your predefined STATIC onboarding, rendered in place when the AI flow fails and
|
|
128
|
+
* retries are exhausted — so a generation/backend/timeout error degrades to your
|
|
129
|
+
* static flow instead of breaking the app. Takes precedence over `onError`. This
|
|
130
|
+
* is the "it can never break your onboarding" guarantee: supply the same flow you
|
|
131
|
+
* shipped before adding Wire AI and the user always keeps moving.
|
|
132
|
+
*/
|
|
133
|
+
fallbackFlow?: import("react").ReactNode;
|
|
134
|
+
/**
|
|
135
|
+
* Consecutive failures to auto-retry before degrading to `fallbackFlow`/`onError`.
|
|
136
|
+
* Default 1 (one silent retry, then degrade). 0 = degrade on the first failure.
|
|
137
|
+
*/
|
|
138
|
+
maxRetries?: number;
|
|
139
|
+
/** Lifecycle hook for host-side analytics (started / per-turn / error). */
|
|
140
|
+
onEvent?: (event: OnboardingEvent) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Host-injected, non-PII context the app already knows about the user — signup method,
|
|
143
|
+
* referral source, plan tier, a HASHED user id, etc. Same host-injection philosophy as
|
|
144
|
+
* `storage`: the kit collects nothing here; the host passes what it wants. Forwarded to the
|
|
145
|
+
* backend on the session metadata AND on client events so analytics can segment the funnel.
|
|
146
|
+
*
|
|
147
|
+
* MUST NOT contain PII such as raw emails, names, or phone numbers — pass a hash if you need
|
|
148
|
+
* a user key. Values are limited to primitives (`string | number | boolean`); the server caps
|
|
149
|
+
* key count / size and drops deep nesting. Old servers ignore it (backward compatible).
|
|
150
|
+
*/
|
|
151
|
+
userContext?: Record<string, string | number | boolean>;
|
|
152
|
+
/**
|
|
153
|
+
* The host's own user id, so onboarding sessions can be reconciled to real users later
|
|
154
|
+
* (console sessions ↔ your user table / GA4 users). First-class alongside `userContext`.
|
|
155
|
+
*
|
|
156
|
+
* An OPAQUE PSEUDONYMOUS string YOU own — your internal user id, NOT an email/name/phone.
|
|
157
|
+
* Trimmed and capped at 128 chars (longer is truncated). **No PII.**
|
|
158
|
+
*
|
|
159
|
+
* LATE BINDING: users often register DURING or AFTER onboarding, so this is fully optional
|
|
160
|
+
* and can arrive late:
|
|
161
|
+
* - present at MOUNT → rides the A2A session-start metadata (server binds it at session start);
|
|
162
|
+
* - CHANGES mid-session (e.g. the user just registered) → the kit emits an `identify` event
|
|
163
|
+
* that attaches the id to the LIVE session;
|
|
164
|
+
* - available only AFTER completion → capture the `contextId` from the `started`/`resumed`
|
|
165
|
+
* `onEvent` while the flow runs, then call `identifyOnboarding({ contextId, userId })` once
|
|
166
|
+
* the user registers. Completion clears the persisted session, so the captured `contextId`
|
|
167
|
+
* is the reliable post-flow handle.
|
|
168
|
+
*
|
|
169
|
+
* Backward compatible: omit it and nothing changes; old servers ignore the extra field.
|
|
170
|
+
*/
|
|
171
|
+
userId?: string;
|
|
172
|
+
/** Localized overrides for the kit's built-in English strings. */
|
|
173
|
+
copy?: Partial<OnboardingCopy>;
|
|
174
|
+
/**
|
|
175
|
+
* Approximate total number of screens, if known (e.g. the backend screen budget).
|
|
176
|
+
* Paces the progress bar as `step / approxScreens` (capped, never shown as a
|
|
177
|
+
* number) instead of the asymptotic default. A backend-supplied `progress.total`
|
|
178
|
+
* takes precedence when present.
|
|
179
|
+
*/
|
|
180
|
+
approxScreens?: number;
|
|
181
|
+
/** First message that kicks off the backend flow. Default `"start"`. */
|
|
182
|
+
startMessage?: string;
|
|
183
|
+
/** Ms to wait for the first card before showing the error/retry state. Default 15000. */
|
|
184
|
+
startTimeoutMs?: number;
|
|
185
|
+
/**
|
|
186
|
+
* Host-injected storage for session-id persistence (AsyncStorage-compatible subset:
|
|
187
|
+
* pass `@react-native-async-storage/async-storage` as-is, or a small MMKV wrapper).
|
|
188
|
+
* When set, the kit caches its session id so an app KILL mid-onboarding resumes the
|
|
189
|
+
* SAME backend session instead of minting a new one — keeping the analytics funnel's
|
|
190
|
+
* `started` count honest (no phantom drops). Omit for the previous per-mount behavior.
|
|
191
|
+
* This persists the kit's own correlation seed only — never answers.
|
|
192
|
+
*/
|
|
193
|
+
storage?: WireOnboardingStorage;
|
|
194
|
+
/**
|
|
195
|
+
* How long a persisted session id stays resumable, in ms. Default 3 600 000 (1h),
|
|
196
|
+
* matching the backend's session TTL. Only meaningful with `storage`.
|
|
197
|
+
*/
|
|
198
|
+
sessionTtlMs?: number;
|
|
199
|
+
/**
|
|
200
|
+
* Override the storage key (default `wireai:session:<config.appId>`). Scope it
|
|
201
|
+
* per-user (e.g. append a user id) if one device can run onboarding for multiple
|
|
202
|
+
* accounts mid-flow. Only meaningful with `storage`.
|
|
203
|
+
*/
|
|
204
|
+
persistKey?: string;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** Backend-supplied progress, read off `response.props.progress` when present. */
|
|
208
|
+
export type OnboardingProgress = {
|
|
209
|
+
step: number;
|
|
210
|
+
total: number;
|
|
211
|
+
/** Base-question key for the CURRENT screen, when known (used to pick a validator). */
|
|
212
|
+
key?: string;
|
|
213
|
+
/** Whether the CURRENT screen may be skipped (backend-marked; default false → no Skip shown). */
|
|
214
|
+
skippable?: boolean;
|
|
215
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deriveAnswers — read the user's structured choices off the thread.
|
|
3
|
+
*
|
|
4
|
+
* The cards send answers back as natural-language strings (the SDK's
|
|
5
|
+
* useWireAIAction wraps them, e.g. `I selected: ["Food","Travel"]`,
|
|
6
|
+
* `My answer is: "Casablanca"`). We pair each rendered question with the reply
|
|
7
|
+
* that followed it and key the answer by the backend's progress `key` when
|
|
8
|
+
* present (so `username`/`interests`/`city` come back under their real keys),
|
|
9
|
+
* else by the question text. No second LLM call — the values ARE the data.
|
|
10
|
+
*/
|
|
11
|
+
import type { Message } from "wireai-rn";
|
|
12
|
+
|
|
13
|
+
const stripPrefix = (s: string): string =>
|
|
14
|
+
s.replace(/^(I selected:|My answer is:|I tapped:)\s*/i, "").trim();
|
|
15
|
+
|
|
16
|
+
const tryParse = (s: string): unknown => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(s);
|
|
19
|
+
} catch {
|
|
20
|
+
return s;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const deriveAnswers = (messages: Message[]): Record<string, unknown> => {
|
|
25
|
+
const out: Record<string, unknown> = {};
|
|
26
|
+
for (let i = 0; i < messages.length; i++) {
|
|
27
|
+
const m = messages[i];
|
|
28
|
+
const r = m?.response;
|
|
29
|
+
if (!m || m.role !== "assistant" || !r || r.action !== "render") continue;
|
|
30
|
+
|
|
31
|
+
const props = (r.props ?? {}) as Record<string, unknown>;
|
|
32
|
+
const progress = props.progress as { key?: string } | undefined;
|
|
33
|
+
const questionText =
|
|
34
|
+
(props.title as string) ?? (props.label as string) ?? (props.question as string);
|
|
35
|
+
const key = progress?.key || questionText;
|
|
36
|
+
|
|
37
|
+
const reply = messages[i + 1];
|
|
38
|
+
if (key && reply?.role === "user") {
|
|
39
|
+
out[key] = tryParse(stripPrefix(reply.content));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
};
|