@wireai/activation 0.14.1 → 0.14.2
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/CHANGELOG.md +26 -0
- package/dist/coachmarks/index.js +69 -6
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +69 -6
- package/dist/coachmarks/index.mjs.map +1 -1
- package/metro/index.js +3 -1
- package/package.json +1 -1
- package/src/coachmarks/SpotlightOverlay.tsx +38 -11
- package/src/coachmarks/expoBlur.ts +135 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wireai/activation",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Premium, fully-themable drop-in AI onboarding kit for React Native / Expo, on top of the open-source wireai-rn SDK.",
|
|
6
6
|
"author": "Malik Chohra <malik@getwireai.com>",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { BlurView } from "expo-blur";
|
|
2
1
|
import React, { useEffect, useMemo } from "react";
|
|
3
2
|
import {
|
|
4
3
|
Pressable,
|
|
@@ -20,11 +19,10 @@ import Animated, {
|
|
|
20
19
|
} from "react-native-reanimated";
|
|
21
20
|
|
|
22
21
|
import { useOnboardingTheme } from "../theme/ThemeContext";
|
|
22
|
+
import { resolveBlurView } from "./expoBlur";
|
|
23
23
|
import { GestureHint } from "./GestureHint";
|
|
24
24
|
import type { GestureKind, Placement, TargetRect } from "./types";
|
|
25
25
|
|
|
26
|
-
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
|
|
27
|
-
|
|
28
26
|
const BLUR_INTENSITY = 26;
|
|
29
27
|
const FADE_MS = 280;
|
|
30
28
|
/** Breathing room between the highlighted element and the glowing ring. */
|
|
@@ -51,13 +49,20 @@ export interface SpotlightOverlayProps {
|
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
/**
|
|
54
|
-
* A single coachmark step: the whole screen is frosted with an animated blur
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* A single coachmark step: the whole screen is frosted with an animated blur
|
|
53
|
+
* (or a plain dimmed scrim when the optional `expo-blur` peer is absent), and a
|
|
54
|
+
* bright pulsing ring + tooltip point at one measured target. Mounted at the app
|
|
55
|
+
* root (above the tab bar) via CoachmarkOverlayHost.
|
|
57
56
|
*
|
|
58
57
|
* Performance: one BlurView (mounted only while a step is visible), the ring
|
|
59
58
|
* glow is a single reanimated view, and all animation runs on the UI thread.
|
|
60
59
|
* Reduce Motion drops the pulse.
|
|
60
|
+
*
|
|
61
|
+
* `expo-blur` is OPTIONAL. It is resolved through a guarded lazy require (see
|
|
62
|
+
* `expoBlur.ts`) and the animated component is built once on first use, so the
|
|
63
|
+
* subpath never carries a static top-level import of the peer. When it is not
|
|
64
|
+
* installed the frost drops to an equivalent dimmed scrim — a decorative frost
|
|
65
|
+
* missing must never blank a tour, so the ring, copy and gestures are unchanged.
|
|
61
66
|
*/
|
|
62
67
|
const SpotlightOverlayComponent: React.FC<SpotlightOverlayProps> = ({
|
|
63
68
|
message,
|
|
@@ -76,6 +81,14 @@ const SpotlightOverlayComponent: React.FC<SpotlightOverlayProps> = ({
|
|
|
76
81
|
const accent = accentColor ?? theme.colors.primary;
|
|
77
82
|
const accentText = accentTextColor ?? theme.colors.onPrimary;
|
|
78
83
|
|
|
84
|
+
// Resolve the optional `expo-blur` peer and build its animated component ONCE, on first render,
|
|
85
|
+
// instead of at module scope — so merely importing this file (and thus the coachmarks subpath)
|
|
86
|
+
// never needs the peer. `null` = the peer is absent → the scrim path renders instead.
|
|
87
|
+
const AnimatedBlurView = useMemo(() => {
|
|
88
|
+
const BlurView = resolveBlurView();
|
|
89
|
+
return BlurView ? Animated.createAnimatedComponent(BlurView) : null;
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
79
92
|
// Tap / double-tap steps show ONLY the ring + tooltip — no gesture glyph. The
|
|
80
93
|
// pulsing highlight ring already reads as "tap here", so an extra glyph is
|
|
81
94
|
// noise. Every app inherits this centrally, with no config change.
|
|
@@ -138,11 +151,20 @@ const SpotlightOverlayComponent: React.FC<SpotlightOverlayProps> = ({
|
|
|
138
151
|
entering={FadeIn.duration(FADE_MS)}
|
|
139
152
|
exiting={FadeOut.duration(FADE_MS)}
|
|
140
153
|
>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
{AnimatedBlurView ? (
|
|
155
|
+
<AnimatedBlurView
|
|
156
|
+
tint="dark"
|
|
157
|
+
animatedProps={blurAnimatedProps}
|
|
158
|
+
style={StyleSheet.absoluteFill}
|
|
159
|
+
/>
|
|
160
|
+
) : (
|
|
161
|
+
// `expo-blur` absent → a plain dimmed scrim stands in for the frost. Same dark backdrop
|
|
162
|
+
// the tour reads against, so the ring, tooltip and gestures behave identically.
|
|
163
|
+
<Animated.View
|
|
164
|
+
style={[StyleSheet.absoluteFill, styles.scrim]}
|
|
165
|
+
pointerEvents="none"
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
146
168
|
|
|
147
169
|
{/* Tap-the-backdrop to move on (dismisses the tour on the last step). */}
|
|
148
170
|
<Pressable
|
|
@@ -210,6 +232,11 @@ const styles = StyleSheet.create({
|
|
|
210
232
|
zIndex: 9999,
|
|
211
233
|
elevation: 9999,
|
|
212
234
|
},
|
|
235
|
+
// Fallback backdrop when `expo-blur` is absent. A structural dim (not a brand token), tuned to
|
|
236
|
+
// read like the dark blur at BLUR_INTENSITY so the ring + tooltip keep the same contrast.
|
|
237
|
+
scrim: {
|
|
238
|
+
backgroundColor: "rgba(0, 0, 0, 0.55)",
|
|
239
|
+
},
|
|
213
240
|
ring: {
|
|
214
241
|
position: "absolute",
|
|
215
242
|
borderWidth: 2.5,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* expoBlur — resolve `expo-blur`'s `BlurView`, lazily and optionally.
|
|
3
|
+
*
|
|
4
|
+
* ── WHY THIS DOES NOT BREAK THE NO-NATIVE-DEPENDENCY POLICY ──────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `expo-blur` is an OPTIONAL peer of the coachmarks subpath. The spotlight overlay frosts the
|
|
7
|
+
* screen with it WHEN it is installed, and degrades to a plain dimmed scrim when it is not — the
|
|
8
|
+
* ring, tooltip and gestures behave identically either way (see SpotlightOverlay). So a host that
|
|
9
|
+
* never installs `expo-blur` can still run the guided tour, and the free tier that must load under
|
|
10
|
+
* Expo Go is not forced into a native module it cannot take.
|
|
11
|
+
*
|
|
12
|
+
* Before this file, `SpotlightOverlay` did `import { BlurView } from "expo-blur"` at module scope,
|
|
13
|
+
* and `coachmarks/index.ts` re-exports the overlay with no wildcard escape — so importing ANYTHING
|
|
14
|
+
* from `@wireai/activation/coachmarks`, even `setCoachmarkStorage`, dragged the peer in and a host
|
|
15
|
+
* without it hit a Metro resolution failure. This resolver removes that static edge.
|
|
16
|
+
*
|
|
17
|
+
* ── THE SPECIFIER MUST BE A STRING LITERAL, INSIDE A TRY/CATCH ───────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* This follows `icons/expoIcons.ts` exactly, and its header carries the full autopsy. In short:
|
|
20
|
+
*
|
|
21
|
+
* • Metro collects dependencies STATICALLY, matching a call whose callee is literally the
|
|
22
|
+
* identifier `require` and whose argument is a STRING LITERAL. A variable specifier
|
|
23
|
+
* (`const req = require; req(name)`) is collected NOWHERE, so the module never enters the
|
|
24
|
+
* bundle — and the aliased `require` is Metro's own numeric-id-keyed `metroRequire`, which can
|
|
25
|
+
* never resolve a package-name string. That shape shipped broken once; do not restore it.
|
|
26
|
+
* • The call sitting inside a TRY/CATCH is literally how Metro marks the dependency `isOptional`:
|
|
27
|
+
* when the peer is absent Metro puts `null` in the dependencyMap and the require throws "Cannot
|
|
28
|
+
* find module" straight into the catch below. Do NOT "simplify" the try/catch away — it is what
|
|
29
|
+
* keeps the peer optional, not just tidy. (`withWireOnboarding` in metro/index.js adds a
|
|
30
|
+
* stub-to-empty-module safety net for hosts that disable Metro's `allowOptionalDependencies`.)
|
|
31
|
+
*/
|
|
32
|
+
import type { ComponentType } from "react";
|
|
33
|
+
|
|
34
|
+
// Metro injects a module-scoped `require`; it is ABSENT in a pure-ESM runtime (the kit's own
|
|
35
|
+
// tests run under `node --test` as ESM). Declared locally so this type-checks without ambient
|
|
36
|
+
// Node types; the `typeof` guard keeps the reference ESM-safe.
|
|
37
|
+
declare const require: ((id: string) => unknown) | undefined;
|
|
38
|
+
|
|
39
|
+
/** A `require`-like resolver. Injectable in tests; production uses the guarded literal require. */
|
|
40
|
+
export type OptionalRequire = (moduleName: string) => unknown;
|
|
41
|
+
|
|
42
|
+
/** The minimal prop shape the overlay uses from `expo-blur`'s `BlurView`. */
|
|
43
|
+
export type BlurViewComponent = ComponentType<{
|
|
44
|
+
intensity?: number;
|
|
45
|
+
tint?: "light" | "dark" | "default";
|
|
46
|
+
style?: unknown;
|
|
47
|
+
children?: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The production resolver. The specifier is a LITERAL so Metro collects it (see the header); the
|
|
52
|
+
* `moduleName` parameter exists only to keep the `OptionalRequire` seam shape, so anything other
|
|
53
|
+
* than the one module this file owns resolves to undefined.
|
|
54
|
+
*/
|
|
55
|
+
const runtimeRequire: OptionalRequire = (moduleName) => {
|
|
56
|
+
if (moduleName !== "expo-blur") return undefined;
|
|
57
|
+
if (typeof require !== "function") return undefined;
|
|
58
|
+
try {
|
|
59
|
+
return require("expo-blur");
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* TEST-ONLY seam. `SpotlightOverlay`'s public props are frozen, so it cannot take a `requireModule`
|
|
67
|
+
* the way `WireIcon` does — this lets a component render exercise the PRESENT path without the
|
|
68
|
+
* native peer installed. Production never sets it; `runtimeRequire` is the only resolver.
|
|
69
|
+
*/
|
|
70
|
+
let testRequire: OptionalRequire | undefined;
|
|
71
|
+
export const __setBlurRequireForTests = (fn: OptionalRequire | undefined): void => {
|
|
72
|
+
testRequire = fn;
|
|
73
|
+
cached = undefined;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Read a module's `default` (Expo modules are consumed as default exports) or the namespace. */
|
|
77
|
+
const interop = (mod: unknown): Record<string, unknown> | undefined => {
|
|
78
|
+
if (!mod || typeof mod !== "object") return undefined;
|
|
79
|
+
const ns = mod as Record<string, unknown>;
|
|
80
|
+
// `expo-blur` exposes `BlurView` as a NAMED export; prefer the namespace when it already carries
|
|
81
|
+
// it, and only fall back to `default` for a CJS-interop wrapper.
|
|
82
|
+
if (ns.BlurView) return ns;
|
|
83
|
+
const def = (mod as { default?: unknown }).default;
|
|
84
|
+
if (def && typeof def === "object") return def as Record<string, unknown>;
|
|
85
|
+
return ns;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Narrow an unknown export to something mountable. `BlurView` is a real React component (function
|
|
90
|
+
* or class across versions), and React.memo / forwardRef wrappers are objects carrying `$$typeof`.
|
|
91
|
+
* Reject anything else rather than handing the reconciler a non-component.
|
|
92
|
+
*/
|
|
93
|
+
const asComponent = (value: unknown): BlurViewComponent | undefined => {
|
|
94
|
+
if (typeof value === "function") return value as BlurViewComponent;
|
|
95
|
+
if (value && typeof value === "object" && "$$typeof" in (value as object)) {
|
|
96
|
+
return value as BlurViewComponent;
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Module-level memo. `null` = "we looked and it is not there" (distinct from "not looked yet"),
|
|
103
|
+
* so an absent peer costs exactly one failed require per process, not one per overlay mount.
|
|
104
|
+
*/
|
|
105
|
+
let cached: BlurViewComponent | null | undefined;
|
|
106
|
+
|
|
107
|
+
/** Reset the memo. TEST-ONLY seam — production never calls it. */
|
|
108
|
+
export const resetBlurModuleCache = (): void => {
|
|
109
|
+
cached = undefined;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the `BlurView` component, or undefined when the peer is absent/unresolvable.
|
|
114
|
+
* Never throws: an absent blur must degrade to a plain scrim, never break the tour.
|
|
115
|
+
*
|
|
116
|
+
* `requireModule` is injectable so tests can exercise BOTH the found and absent paths without
|
|
117
|
+
* installing the native peer (same convention as `resolveIconFamily` / `detectAppVersion`).
|
|
118
|
+
*/
|
|
119
|
+
export const resolveBlurView = (
|
|
120
|
+
requireModule: OptionalRequire = testRequire ?? runtimeRequire,
|
|
121
|
+
): BlurViewComponent | undefined => {
|
|
122
|
+
try {
|
|
123
|
+
if (cached === undefined || requireModule !== runtimeRequire) {
|
|
124
|
+
const resolved = interop(requireModule("expo-blur"));
|
|
125
|
+
const component = resolved ? asComponent(resolved.BlurView) : undefined;
|
|
126
|
+
// Don't poison the module memo from an injected test require.
|
|
127
|
+
if (requireModule === runtimeRequire) cached = component ?? null;
|
|
128
|
+
return component;
|
|
129
|
+
}
|
|
130
|
+
if (cached === null) return undefined;
|
|
131
|
+
return cached;
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
};
|