@wireai/activation 0.14.1 → 0.14.3
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 +97 -0
- package/README.md +12 -2
- package/dist/coachmarks/index.d.mts +12 -4
- package/dist/coachmarks/index.d.ts +12 -4
- package/dist/coachmarks/index.js +270 -58
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +224 -11
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/showcase/index.js +235 -46
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +201 -10
- package/dist/showcase/index.mjs.map +1 -1
- package/metro/index.js +18 -5
- package/package.json +1 -1
- package/src/coachmarks/GestureHint.tsx +16 -7
- package/src/coachmarks/SpotlightOverlay.tsx +51 -18
- package/src/coachmarks/expoBlur.ts +135 -0
- package/src/coachmarks/index.ts +7 -2
- package/src/coachmarks/reanimated.ts +342 -0
- package/src/showcase/FeatureShowcase.tsx +25 -9
- package/src/showcase/blazejOnboarding.ts +136 -0
- package/src/showcase/index.ts +8 -4
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type {
|
|
2
|
+
OnboardingColors,
|
|
3
|
+
OnboardingFonts,
|
|
4
|
+
OnboardingProps,
|
|
5
|
+
OnboardingStep,
|
|
6
6
|
} from "@blazejkustra/react-native-onboarding";
|
|
7
7
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { type ImageSourcePropType, StyleSheet, View } from "react-native";
|
|
@@ -12,6 +12,7 @@ import { hasSeenGate, markSeenGate, showcaseGateKey } from "../coachmarks/runtim
|
|
|
12
12
|
import { useResolvedFeatures } from "../features/WireFeaturesProvider";
|
|
13
13
|
import { useOnboardingTheme } from "../theme/ThemeContext";
|
|
14
14
|
import type { OnboardingTheme } from "../theme/types";
|
|
15
|
+
import { resolveShowcaseOnboarding } from "./blazejOnboarding";
|
|
15
16
|
import { showcaseColorsFromTheme, showcasePanelBackground } from "./showcaseColors";
|
|
16
17
|
import type { FeatureShowcaseProps } from "./types";
|
|
17
18
|
|
|
@@ -47,6 +48,13 @@ const mergeThemeOver = (
|
|
|
47
48
|
*
|
|
48
49
|
* When the gate says "seen", it renders nothing and calls `onDone` from an
|
|
49
50
|
* effect (never during render).
|
|
51
|
+
*
|
|
52
|
+
* The underlying package is an OPTIONAL peer, resolved through the guarded lazy
|
|
53
|
+
* require in `blazejOnboarding.ts` rather than a static import, so the showcase
|
|
54
|
+
* subpath builds on a host that never installed it. Absent, this takes the SAME
|
|
55
|
+
* path as the kill switch — render null, call `onDone`, write no gate — so the
|
|
56
|
+
* host's flow always advances and the showcase still plays once if the peer is
|
|
57
|
+
* added later.
|
|
50
58
|
*/
|
|
51
59
|
const _FeatureShowcase: React.FC<FeatureShowcaseProps> = ({
|
|
52
60
|
config,
|
|
@@ -71,14 +79,20 @@ const _FeatureShowcase: React.FC<FeatureShowcaseProps> = ({
|
|
|
71
79
|
const flags = useResolvedFeatures({ flags: features, config: featuresConfig });
|
|
72
80
|
const disabled = !flags.showcase.enabled;
|
|
73
81
|
|
|
82
|
+
// The optional pager peer. Resolved once, on first render, instead of at module scope — so
|
|
83
|
+
// merely importing this file (and thus the showcase subpath) never needs it. Absent → treated
|
|
84
|
+
// exactly like the kill switch below: no render, no gate write, `onDone` from the effect.
|
|
85
|
+
const Onboarding = useMemo(() => resolveShowcaseOnboarding(), []);
|
|
86
|
+
|
|
74
87
|
const gateKey = showcaseGateKey(config.id);
|
|
75
88
|
const seen = useMemo(
|
|
76
89
|
() => hasSeenGate(gateKey, storage, isTesting),
|
|
77
90
|
[gateKey, storage, isTesting],
|
|
78
91
|
);
|
|
79
|
-
//
|
|
80
|
-
// gate write (in `finish`); the
|
|
81
|
-
|
|
92
|
+
// Already-seen, feature-disabled OR the peer missing short-circuits the showcase. Only the seen
|
|
93
|
+
// path is a gate write (in `finish`); the other two never persist, so the showcase replays once
|
|
94
|
+
// the feature is re-enabled or the peer is installed.
|
|
95
|
+
const skip = seen || disabled || !Onboarding;
|
|
82
96
|
|
|
83
97
|
const doneRef = useRef(false);
|
|
84
98
|
const finish = useCallback(() => {
|
|
@@ -177,7 +191,9 @@ const _FeatureShowcase: React.FC<FeatureShowcaseProps> = ({
|
|
|
177
191
|
[t],
|
|
178
192
|
);
|
|
179
193
|
|
|
180
|
-
|
|
194
|
+
// `!Onboarding` is already folded into `skip`; it is repeated here so the narrowing is explicit
|
|
195
|
+
// to the reader and to the compiler at the JSX below.
|
|
196
|
+
if (skip || !Onboarding) return null;
|
|
181
197
|
|
|
182
198
|
const activeGesture = config.slides[activeIndex]?.gesture;
|
|
183
199
|
// Same rule as the coachmark path: tap / double-tap slides show no glyph.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blazejOnboarding — resolve `@blazejkustra/react-native-onboarding`, lazily and optionally.
|
|
3
|
+
*
|
|
4
|
+
* ── WHY THIS DOES NOT BREAK THE NO-NATIVE-DEPENDENCY POLICY ──────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `@blazejkustra/react-native-onboarding` is an OPTIONAL peer of the showcase subpath — it is the
|
|
7
|
+
* slide pager `FeatureShowcase` wraps. Before this file, `FeatureShowcase.tsx` imported its default
|
|
8
|
+
* export at module scope, and `showcase/index.ts` re-exports the component with no wildcard escape,
|
|
9
|
+
* so importing ANYTHING from `@wireai/activation/showcase` (even `selectShowcaseSlides`) dragged
|
|
10
|
+
* the peer in, and a host that skipped the "optional" peer hit a Metro resolution failure. This
|
|
11
|
+
* resolver removes that static edge. Same class and same shape as the 0.14.2 `expo-blur` fix
|
|
12
|
+
* (`coachmarks/expoBlur.ts`), whose header carries the reasoning in full.
|
|
13
|
+
*
|
|
14
|
+
* The TYPES stay statically imported on purpose: a type-only import is erased by every toolchain
|
|
15
|
+
* (babel, esbuild and sucrase all drop it), so it creates no runtime edge and no bundle dependency,
|
|
16
|
+
* and `showcaseColors.ts` already relies on exactly that.
|
|
17
|
+
*
|
|
18
|
+
* ── THE SPECIFIER MUST BE A STRING LITERAL, INSIDE A TRY/CATCH ───────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* Metro collects dependencies statically and matches only a call whose callee is literally the
|
|
21
|
+
* identifier `require` and whose argument is literally a string; the try/catch around it is what
|
|
22
|
+
* marks the dependency `isOptional`. Both halves are load-bearing — see `icons/expoIcons.ts` for
|
|
23
|
+
* the full autopsy of the 0.8.0 shape that aliased the callee and was collected nowhere.
|
|
24
|
+
*
|
|
25
|
+
* ── THE DEGRADE ─────────────────────────────────────────────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* Absent → `FeatureShowcase` renders null and calls `onDone` from an effect, which is the SAME path
|
|
28
|
+
* the feature kill switch already takes. The host's flow always advances (a showcase that cannot
|
|
29
|
+
* render must never strand the user on a blank screen), and the seen-gate is deliberately NOT
|
|
30
|
+
* written, so installing the peer later still plays the showcase once.
|
|
31
|
+
*/
|
|
32
|
+
import type { ComponentType } from "react";
|
|
33
|
+
|
|
34
|
+
import type { OnboardingProps } from "@blazejkustra/react-native-onboarding";
|
|
35
|
+
|
|
36
|
+
// Metro injects a module-scoped `require`; it is ABSENT in a pure-ESM runtime (the kit's own tests
|
|
37
|
+
// run under `node --test` as ESM). Declared locally so this type-checks without ambient Node types;
|
|
38
|
+
// the `typeof` guard keeps the reference ESM-safe.
|
|
39
|
+
declare const require: ((id: string) => unknown) | undefined;
|
|
40
|
+
|
|
41
|
+
/** A `require`-like resolver. Injectable in tests; production uses the guarded literal require. */
|
|
42
|
+
export type OptionalRequire = (moduleName: string) => unknown;
|
|
43
|
+
|
|
44
|
+
/** The pager component `FeatureShowcase` wraps. */
|
|
45
|
+
export type OnboardingComponent = ComponentType<OnboardingProps>;
|
|
46
|
+
|
|
47
|
+
const MODULE_NAME = "@blazejkustra/react-native-onboarding";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The production resolver. The specifier is a LITERAL so Metro collects it (see the header); the
|
|
51
|
+
* `moduleName` parameter exists only to keep the `OptionalRequire` seam shape, so anything other
|
|
52
|
+
* than the one module this file owns resolves to undefined.
|
|
53
|
+
*/
|
|
54
|
+
const runtimeRequire: OptionalRequire = (moduleName) => {
|
|
55
|
+
if (moduleName !== MODULE_NAME) return undefined;
|
|
56
|
+
if (typeof require !== "function") return undefined;
|
|
57
|
+
try {
|
|
58
|
+
return require("@blazejkustra/react-native-onboarding");
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* TEST-ONLY seam. `FeatureShowcase`'s public props are frozen, so it cannot take a `requireModule`
|
|
66
|
+
* the way `WireIcon` does — this lets a component render exercise the PRESENT path without the peer
|
|
67
|
+
* installed. Production never sets it; `runtimeRequire` is the only resolver.
|
|
68
|
+
*/
|
|
69
|
+
let testRequire: OptionalRequire | undefined;
|
|
70
|
+
export const __setShowcaseOnboardingRequireForTests = (fn: OptionalRequire | undefined): void => {
|
|
71
|
+
testRequire = fn;
|
|
72
|
+
cached = undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read the component off the module. The package ships it as a DEFAULT export; a CJS build may put
|
|
77
|
+
* the component directly on `module.exports` instead, so both shapes are accepted.
|
|
78
|
+
*/
|
|
79
|
+
const interop = (mod: unknown): unknown => {
|
|
80
|
+
if (typeof mod === "function") return mod;
|
|
81
|
+
if (!mod || typeof mod !== "object") return undefined;
|
|
82
|
+
const ns = mod as Record<string, unknown>;
|
|
83
|
+
if (ns.default) return ns.default;
|
|
84
|
+
// `module.exports = forwardRef(...)`: the component IS the namespace, and it is an object rather
|
|
85
|
+
// than a function, so it would slip past the check above.
|
|
86
|
+
if ("$$typeof" in ns) return ns;
|
|
87
|
+
return undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Narrow an unknown export to something mountable: a real component (function or class) or a
|
|
92
|
+
* React.memo / forwardRef wrapper (an object carrying `$$typeof`). Reject anything else rather than
|
|
93
|
+
* handing the reconciler a non-component.
|
|
94
|
+
*/
|
|
95
|
+
const asComponent = (value: unknown): OnboardingComponent | undefined => {
|
|
96
|
+
if (typeof value === "function") return value as OnboardingComponent;
|
|
97
|
+
if (value && typeof value === "object" && "$$typeof" in (value as object)) {
|
|
98
|
+
return value as OnboardingComponent;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Module-level memo. `null` = "we looked and it is not there" (distinct from "not looked yet"), so
|
|
105
|
+
* an absent peer costs exactly one failed require per process, not one per showcase mount.
|
|
106
|
+
*/
|
|
107
|
+
let cached: OnboardingComponent | null | undefined;
|
|
108
|
+
|
|
109
|
+
/** Reset the memo. TEST-ONLY seam — production never calls it. */
|
|
110
|
+
export const resetShowcaseOnboardingCache = (): void => {
|
|
111
|
+
cached = undefined;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the pager component, or undefined when the peer is absent/unresolvable. Never throws: an
|
|
116
|
+
* absent showcase must advance the host's flow, never break it.
|
|
117
|
+
*
|
|
118
|
+
* `requireModule` is injectable so tests can exercise BOTH the found and absent paths without
|
|
119
|
+
* installing the peer (same convention as `resolveBlurView` / `resolveIconFamily`).
|
|
120
|
+
*/
|
|
121
|
+
export const resolveShowcaseOnboarding = (
|
|
122
|
+
requireModule: OptionalRequire = testRequire ?? runtimeRequire,
|
|
123
|
+
): OnboardingComponent | undefined => {
|
|
124
|
+
try {
|
|
125
|
+
if (cached === undefined || requireModule !== runtimeRequire) {
|
|
126
|
+
const component = asComponent(interop(requireModule(MODULE_NAME)));
|
|
127
|
+
// Don't poison the module memo from an injected test require.
|
|
128
|
+
if (requireModule === runtimeRequire) cached = component ?? null;
|
|
129
|
+
return component;
|
|
130
|
+
}
|
|
131
|
+
if (cached === null) return undefined;
|
|
132
|
+
return cached;
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
};
|
package/src/showcase/index.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* @wireai/activation/showcase — the pre-onboarding feature showcase ("app intro").
|
|
3
3
|
*
|
|
4
4
|
* Subpath entry, kept OUT of the main barrel so the core kit stays dependency-
|
|
5
|
-
* free
|
|
6
|
-
* `@blazejkustra/react-native-onboarding`
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
5
|
+
* free. Importing this pulls in NO native peer (0.14.3): the pager
|
|
6
|
+
* `@blazejkustra/react-native-onboarding` is reached through the guarded lazy
|
|
7
|
+
* require in `blazejOnboarding.ts`, and the transitive edge through GestureHint
|
|
8
|
+
* to `react-native-reanimated` is guarded the same way in `coachmarks/reanimated.ts`.
|
|
9
|
+
* Without the pager the showcase renders null and calls `onDone` so the host's
|
|
10
|
+
* flow advances; without reanimated the gesture glyph renders static. The app
|
|
11
|
+
* supplies a declarative ShowcaseConfig; the kit bakes in the Wire theme +
|
|
12
|
+
* optional per-slide gesture hand and gates once.
|
|
9
13
|
*
|
|
10
14
|
* import { FeatureShowcase, selectShowcaseSlides } from "@wireai/activation/showcase";
|
|
11
15
|
*/
|