@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.
Files changed (142) hide show
  1. package/AGENTS.md +73 -0
  2. package/CHANGELOG.md +572 -0
  3. package/INTEGRATION_PROMPT.md +55 -0
  4. package/LICENSE +21 -0
  5. package/README.md +923 -0
  6. package/dist/CenteredModal-C3qQBHsA.d.mts +38 -0
  7. package/dist/CenteredModal-Cdgns6--.d.ts +38 -0
  8. package/dist/coachmarks/index.d.mts +352 -0
  9. package/dist/coachmarks/index.d.ts +352 -0
  10. package/dist/coachmarks/index.js +944 -0
  11. package/dist/coachmarks/index.js.map +1 -0
  12. package/dist/coachmarks/index.mjs +918 -0
  13. package/dist/coachmarks/index.mjs.map +1 -0
  14. package/dist/decision-CFvGY6nP.d.ts +228 -0
  15. package/dist/decision-Cl8OFYzu.d.mts +228 -0
  16. package/dist/index.d.mts +1390 -0
  17. package/dist/index.d.ts +1390 -0
  18. package/dist/index.js +3260 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/index.mjs +3186 -0
  21. package/dist/index.mjs.map +1 -0
  22. package/dist/questionnaire/index.d.mts +249 -0
  23. package/dist/questionnaire/index.d.ts +249 -0
  24. package/dist/questionnaire/index.js +922 -0
  25. package/dist/questionnaire/index.js.map +1 -0
  26. package/dist/questionnaire/index.mjs +906 -0
  27. package/dist/questionnaire/index.mjs.map +1 -0
  28. package/dist/reviews/index.d.mts +213 -0
  29. package/dist/reviews/index.d.ts +213 -0
  30. package/dist/reviews/index.js +1001 -0
  31. package/dist/reviews/index.js.map +1 -0
  32. package/dist/reviews/index.mjs +971 -0
  33. package/dist/reviews/index.mjs.map +1 -0
  34. package/dist/showcase/index.d.mts +119 -0
  35. package/dist/showcase/index.d.ts +119 -0
  36. package/dist/showcase/index.js +660 -0
  37. package/dist/showcase/index.js.map +1 -0
  38. package/dist/showcase/index.mjs +651 -0
  39. package/dist/showcase/index.mjs.map +1 -0
  40. package/dist/types-BKfpdZzX.d.mts +84 -0
  41. package/dist/types-BKfpdZzX.d.ts +84 -0
  42. package/dist/types-CMuOexw0.d.mts +116 -0
  43. package/dist/types-CMuOexw0.d.ts +116 -0
  44. package/dist/types-GL_hQ0TN.d.mts +46 -0
  45. package/dist/types-GL_hQ0TN.d.ts +46 -0
  46. package/llms.txt +36 -0
  47. package/metro/index.d.ts +32 -0
  48. package/metro/index.js +83 -0
  49. package/package.json +171 -0
  50. package/src/DemoOnboarding.tsx +133 -0
  51. package/src/OnboardingFlow.tsx +504 -0
  52. package/src/WireOnboarding.tsx +248 -0
  53. package/src/analytics/analyticsEvent.ts +69 -0
  54. package/src/analytics/reportClientEvent.ts +119 -0
  55. package/src/analytics/sendPreview.ts +73 -0
  56. package/src/attribution/attribution.ts +46 -0
  57. package/src/cards/ChipSelectCard.tsx +265 -0
  58. package/src/cards/InterstitialCard.tsx +299 -0
  59. package/src/cards/NumberStepperCard.tsx +121 -0
  60. package/src/cards/SelectionCard.tsx +270 -0
  61. package/src/cards/StatusCard.tsx +170 -0
  62. package/src/cards/TextInputCard.tsx +147 -0
  63. package/src/cards/index.ts +23 -0
  64. package/src/coachmarks/CoachmarkOverlayHost.tsx +44 -0
  65. package/src/coachmarks/CoachmarkProvider.tsx +108 -0
  66. package/src/coachmarks/GestureHint.tsx +331 -0
  67. package/src/coachmarks/SpotlightOverlay.tsx +254 -0
  68. package/src/coachmarks/coachmarkAnchorRegistry.ts +70 -0
  69. package/src/coachmarks/coachmarkOverlayStore.ts +63 -0
  70. package/src/coachmarks/index.ts +62 -0
  71. package/src/coachmarks/runtime.ts +103 -0
  72. package/src/coachmarks/selectTourSteps.ts +45 -0
  73. package/src/coachmarks/types.ts +58 -0
  74. package/src/coachmarks/useCoachmarkAnchor.ts +49 -0
  75. package/src/coachmarks/useCoachmarkTour.ts +200 -0
  76. package/src/components/AnimatedSparkle.tsx +101 -0
  77. package/src/components/Button.tsx +129 -0
  78. package/src/components/CardHandoff.tsx +212 -0
  79. package/src/components/CardLayout.tsx +103 -0
  80. package/src/components/CenteredModal.tsx +184 -0
  81. package/src/components/CompletionView.tsx +186 -0
  82. package/src/components/DoneBlock.tsx +37 -0
  83. package/src/components/ErrorBlock.tsx +42 -0
  84. package/src/components/Illustration.tsx +36 -0
  85. package/src/components/LoadingBlock.tsx +182 -0
  86. package/src/components/LoadingScreen.tsx +35 -0
  87. package/src/components/OnboardingScaffold.tsx +105 -0
  88. package/src/components/StepProgress.tsx +85 -0
  89. package/src/components/loaderChrome.ts +28 -0
  90. package/src/config/onboardingFlag.ts +39 -0
  91. package/src/config/wireConfigFromEnv.ts +52 -0
  92. package/src/device/deviceContext.ts +158 -0
  93. package/src/features/WireFeaturesProvider.tsx +81 -0
  94. package/src/features/cache.ts +92 -0
  95. package/src/features/defaults.ts +67 -0
  96. package/src/features/fetchWireFeatures.ts +86 -0
  97. package/src/features/index.ts +39 -0
  98. package/src/features/types.ts +61 -0
  99. package/src/features/useWireFeatures.ts +57 -0
  100. package/src/identity/userIdentity.ts +100 -0
  101. package/src/illustrations/defaultIllustrations.tsx +99 -0
  102. package/src/index.ts +159 -0
  103. package/src/motion/centeredModalMotion.ts +45 -0
  104. package/src/motion/motionSpec.ts +133 -0
  105. package/src/motion/useReducedMotion.ts +43 -0
  106. package/src/questionnaire/QuestionnaireGate.tsx +235 -0
  107. package/src/questionnaire/decision.ts +63 -0
  108. package/src/questionnaire/index.ts +54 -0
  109. package/src/questionnaire/runtime.ts +20 -0
  110. package/src/questionnaire/transport.ts +83 -0
  111. package/src/questionnaire/types.ts +182 -0
  112. package/src/questionnaire/useQuestionnaireGate.ts +159 -0
  113. package/src/reviews/ReviewGate.tsx +316 -0
  114. package/src/reviews/ReviewModal.tsx +13 -0
  115. package/src/reviews/decision.ts +120 -0
  116. package/src/reviews/equality.ts +54 -0
  117. package/src/reviews/expo-store-review.d.ts +16 -0
  118. package/src/reviews/feedbackForm.ts +41 -0
  119. package/src/reviews/index.ts +78 -0
  120. package/src/reviews/presentation.ts +29 -0
  121. package/src/reviews/runtime.ts +45 -0
  122. package/src/reviews/storeReview.ts +62 -0
  123. package/src/reviews/transport.ts +89 -0
  124. package/src/reviews/types.ts +172 -0
  125. package/src/reviews/useReviewGate.ts +137 -0
  126. package/src/session/persistedSession.ts +143 -0
  127. package/src/session-analytics/index.ts +18 -0
  128. package/src/session-analytics/reportSessionStart.ts +144 -0
  129. package/src/session-analytics/useSessionStart.ts +114 -0
  130. package/src/showcase/FeatureShowcase.tsx +233 -0
  131. package/src/showcase/index.ts +19 -0
  132. package/src/showcase/selectShowcaseSlides.ts +45 -0
  133. package/src/showcase/showcaseColors.ts +91 -0
  134. package/src/showcase/types.ts +74 -0
  135. package/src/theme/ThemeContext.tsx +38 -0
  136. package/src/theme/defaultTheme.ts +70 -0
  137. package/src/theme/themeFromBrand.ts +93 -0
  138. package/src/theme/types.ts +88 -0
  139. package/src/theme/typography.ts +46 -0
  140. package/src/types.ts +215 -0
  141. package/src/utils/deriveAnswers.ts +43 -0
  142. 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
+ };