@hook-sdk/template 0.10.0 → 0.12.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/dist/index.cjs +110 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -4
- package/dist/index.d.ts +21 -4
- package/dist/index.js +111 -32
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -153,7 +153,12 @@ var AppConfigSchema = import_zod.z.object({
|
|
|
153
153
|
persistedKeys: import_zod.z.array(PersistedKeySchema),
|
|
154
154
|
onboarding: OnboardingSchema.optional(),
|
|
155
155
|
deepLinks: DeepLinksSchema.optional(),
|
|
156
|
-
features_enabled: import_zod.z.array(import_zod.z.string()).optional()
|
|
156
|
+
features_enabled: import_zod.z.array(import_zod.z.string()).optional(),
|
|
157
|
+
// Build-time injected theme metadata (e.g. icon_url for InstallSplash).
|
|
158
|
+
// Apps don't author this directly; deploy workflows fill it from
|
|
159
|
+
// env-resolved bundle host. Permissive shape so apps/workflows can
|
|
160
|
+
// extend without re-bumping the template schema.
|
|
161
|
+
theme: import_zod.z.object({}).passthrough().optional()
|
|
157
162
|
}).strict();
|
|
158
163
|
function parseAppConfig(input) {
|
|
159
164
|
const r = AppConfigSchema.safeParse(input);
|
|
@@ -1829,8 +1834,15 @@ var ErrorBoundary = class extends import_react9.Component {
|
|
|
1829
1834
|
static getDerivedStateFromError(error) {
|
|
1830
1835
|
return { error };
|
|
1831
1836
|
}
|
|
1832
|
-
componentDidCatch(error) {
|
|
1833
|
-
console.error(
|
|
1837
|
+
componentDidCatch(error, info) {
|
|
1838
|
+
console.error(
|
|
1839
|
+
"[ErrorBoundary] caught:",
|
|
1840
|
+
error?.message || "(no message)",
|
|
1841
|
+
"\nstack:",
|
|
1842
|
+
error?.stack || "(no stack)",
|
|
1843
|
+
"\ncomponentStack:",
|
|
1844
|
+
info?.componentStack || "(no componentStack)"
|
|
1845
|
+
);
|
|
1834
1846
|
}
|
|
1835
1847
|
render() {
|
|
1836
1848
|
if (this.state.error) {
|
|
@@ -2058,6 +2070,8 @@ function FallbackPaywall() {
|
|
|
2058
2070
|
// src/hooks/usePush.ts
|
|
2059
2071
|
var import_react12 = require("react");
|
|
2060
2072
|
var import_sdk5 = require("@hook-sdk/sdk");
|
|
2073
|
+
var DISMISS_STORAGE_KEY = "push:dismissed-until";
|
|
2074
|
+
var DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2061
2075
|
function detectIosNeedsInstall() {
|
|
2062
2076
|
if (typeof navigator === "undefined" || typeof window === "undefined") return false;
|
|
2063
2077
|
const ua = navigator.userAgent || "";
|
|
@@ -2068,6 +2082,21 @@ function detectIosNeedsInstall() {
|
|
|
2068
2082
|
const legacyStandalone = typeof navigator.standalone === "boolean" ? navigator.standalone : false;
|
|
2069
2083
|
return !(standalone || legacyStandalone);
|
|
2070
2084
|
}
|
|
2085
|
+
function readDismissedUntil() {
|
|
2086
|
+
if (typeof localStorage === "undefined") return null;
|
|
2087
|
+
try {
|
|
2088
|
+
const raw = localStorage.getItem(DISMISS_STORAGE_KEY);
|
|
2089
|
+
if (raw === null) return null;
|
|
2090
|
+
const n = Number.parseInt(raw, 10);
|
|
2091
|
+
return Number.isFinite(n) ? n : null;
|
|
2092
|
+
} catch {
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
function isDismissedNow() {
|
|
2097
|
+
const until = readDismissedUntil();
|
|
2098
|
+
return until !== null && until > Date.now();
|
|
2099
|
+
}
|
|
2071
2100
|
function deriveState(push) {
|
|
2072
2101
|
if (!push.isAvailable()) {
|
|
2073
2102
|
if (detectIosNeedsInstall()) return { kind: "ios_needs_install" };
|
|
@@ -2080,6 +2109,7 @@ function deriveState(push) {
|
|
|
2080
2109
|
if (detectIosNeedsInstall()) return { kind: "ios_needs_install" };
|
|
2081
2110
|
return { kind: "unsupported" };
|
|
2082
2111
|
}
|
|
2112
|
+
if (isDismissedNow()) return { kind: "dismissed" };
|
|
2083
2113
|
return { kind: "prompt" };
|
|
2084
2114
|
}
|
|
2085
2115
|
function usePush() {
|
|
@@ -2109,14 +2139,41 @@ function usePush() {
|
|
|
2109
2139
|
throw e;
|
|
2110
2140
|
}
|
|
2111
2141
|
}, [push]);
|
|
2112
|
-
|
|
2142
|
+
const dismiss = (0, import_react12.useCallback)(() => {
|
|
2143
|
+
if (typeof localStorage !== "undefined") {
|
|
2144
|
+
try {
|
|
2145
|
+
localStorage.setItem(DISMISS_STORAGE_KEY, String(Date.now() + DISMISS_TTL_MS));
|
|
2146
|
+
} catch {
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
setState({ kind: "dismissed" });
|
|
2150
|
+
}, []);
|
|
2151
|
+
return { state, subscribe, unsubscribe, dismiss };
|
|
2113
2152
|
}
|
|
2114
2153
|
|
|
2115
2154
|
// src/components/PushPrompt.tsx
|
|
2116
2155
|
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
2156
|
+
function platformRecoveryCopy(texts) {
|
|
2157
|
+
if (typeof navigator === "undefined") return null;
|
|
2158
|
+
const ua = navigator.userAgent || "";
|
|
2159
|
+
const platform = detectPlatform(ua);
|
|
2160
|
+
switch (platform) {
|
|
2161
|
+
case "ios-safari":
|
|
2162
|
+
case "ios-other":
|
|
2163
|
+
return texts.deniedRecoveryIos ?? null;
|
|
2164
|
+
case "android":
|
|
2165
|
+
return texts.deniedRecoveryAndroid ?? null;
|
|
2166
|
+
case "desktop":
|
|
2167
|
+
return texts.deniedRecoveryDesktop ?? null;
|
|
2168
|
+
case "in-app":
|
|
2169
|
+
return texts.deniedRecoveryInApp ?? null;
|
|
2170
|
+
default:
|
|
2171
|
+
return null;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2117
2174
|
function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, className }) {
|
|
2118
2175
|
const { state, subscribe } = usePush();
|
|
2119
|
-
if (state.kind === "subscribed") return null;
|
|
2176
|
+
if (state.kind === "subscribed" || state.kind === "dismissed") return null;
|
|
2120
2177
|
if (state.kind === "ios_needs_install") {
|
|
2121
2178
|
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className, role: "region", "aria-label": texts.iosInstallTitle, children: [
|
|
2122
2179
|
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: texts.iosInstallTitle }),
|
|
@@ -2125,9 +2182,11 @@ function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, clas
|
|
|
2125
2182
|
] });
|
|
2126
2183
|
}
|
|
2127
2184
|
if (state.kind === "denied") {
|
|
2185
|
+
const recovery = platformRecoveryCopy(texts);
|
|
2128
2186
|
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className, role: "region", "aria-label": texts.deniedTitle, children: [
|
|
2129
2187
|
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: texts.deniedTitle }),
|
|
2130
|
-
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { children: texts.deniedBody })
|
|
2188
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { children: texts.deniedBody }),
|
|
2189
|
+
recovery && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { "data-testid": "denied-recovery", children: recovery })
|
|
2131
2190
|
] });
|
|
2132
2191
|
}
|
|
2133
2192
|
if (state.kind === "unsupported") {
|
|
@@ -2604,62 +2663,82 @@ function useOnboardingStep() {
|
|
|
2604
2663
|
// src/OnboardingFlow.tsx
|
|
2605
2664
|
var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
2606
2665
|
var isFilled = (v) => v != null && v !== "";
|
|
2666
|
+
var CURRENT_STEP_FIELD = "currentStep";
|
|
2667
|
+
function readPersistedStepIdx(draft) {
|
|
2668
|
+
const raw = draft[CURRENT_STEP_FIELD];
|
|
2669
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw >= 0 ? raw : 0;
|
|
2670
|
+
}
|
|
2607
2671
|
function OnboardingFlow({
|
|
2608
2672
|
steps,
|
|
2609
2673
|
screens,
|
|
2610
2674
|
onComplete,
|
|
2611
2675
|
persistKey
|
|
2612
2676
|
}) {
|
|
2613
|
-
const [draft, setDraft] = (0, import_sdk16.usePersistedState)(persistKey, {});
|
|
2614
|
-
const [idx, setIdx] = (0, import_react21.useState)(0);
|
|
2677
|
+
const [draft, setDraft, status] = (0, import_sdk16.usePersistedState)(persistKey, {});
|
|
2615
2678
|
const draftRef = (0, import_react21.useRef)(draft);
|
|
2616
2679
|
draftRef.current = draft;
|
|
2617
|
-
const
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
}
|
|
2629
|
-
const valid = (0, import_react21.useMemo)(
|
|
2630
|
-
() => (step.validates ?? []).every((field) => isFilled(draft[field])),
|
|
2631
|
-
[draft, step]
|
|
2680
|
+
const idx = readPersistedStepIdx(draft);
|
|
2681
|
+
const clampedIdx = Math.min(Math.max(idx, 0), Math.max(steps.length - 1, 0));
|
|
2682
|
+
const setIdx = (0, import_react21.useCallback)(
|
|
2683
|
+
(n) => {
|
|
2684
|
+
setDraft((prev) => {
|
|
2685
|
+
const prevIdx = readPersistedStepIdx(prev);
|
|
2686
|
+
const nextIdx = typeof n === "function" ? n(prevIdx) : n;
|
|
2687
|
+
return { ...prev, [CURRENT_STEP_FIELD]: nextIdx };
|
|
2688
|
+
});
|
|
2689
|
+
},
|
|
2690
|
+
[setDraft]
|
|
2632
2691
|
);
|
|
2633
2692
|
const setValue = (0, import_react21.useCallback)(
|
|
2634
2693
|
(patch) => {
|
|
2635
2694
|
draftRef.current = { ...draftRef.current, ...patch };
|
|
2636
|
-
setDraft((
|
|
2695
|
+
setDraft((prev) => ({ ...prev, ...patch }));
|
|
2637
2696
|
},
|
|
2638
2697
|
[setDraft]
|
|
2639
2698
|
);
|
|
2699
|
+
const step = steps[clampedIdx];
|
|
2700
|
+
const valid = (0, import_react21.useMemo)(
|
|
2701
|
+
() => step ? (step.validates ?? []).every((field) => isFilled(draft[field])) : false,
|
|
2702
|
+
[draft, step]
|
|
2703
|
+
);
|
|
2640
2704
|
const next = (0, import_react21.useCallback)(() => {
|
|
2705
|
+
if (!step) return;
|
|
2641
2706
|
const current = draftRef.current;
|
|
2642
2707
|
const validNow = (step.validates ?? []).every((field) => isFilled(current[field]));
|
|
2643
2708
|
if (!validNow) return;
|
|
2644
|
-
if (
|
|
2709
|
+
if (clampedIdx + 1 >= steps.length) {
|
|
2645
2710
|
onComplete(current);
|
|
2646
2711
|
} else {
|
|
2647
|
-
setIdx(
|
|
2712
|
+
setIdx(clampedIdx + 1);
|
|
2648
2713
|
}
|
|
2649
|
-
}, [
|
|
2650
|
-
const
|
|
2714
|
+
}, [clampedIdx, onComplete, step, steps.length, setIdx]);
|
|
2715
|
+
const prevStep = (0, import_react21.useCallback)(() => setIdx((i) => Math.max(0, i - 1)), [setIdx]);
|
|
2651
2716
|
const ctx = (0, import_react21.useMemo)(
|
|
2652
2717
|
() => ({
|
|
2653
|
-
stepIndex:
|
|
2718
|
+
stepIndex: clampedIdx,
|
|
2654
2719
|
totalSteps: steps.length,
|
|
2655
2720
|
value: draft,
|
|
2656
2721
|
setValue,
|
|
2657
2722
|
valid,
|
|
2658
2723
|
next,
|
|
2659
|
-
prev
|
|
2724
|
+
prev: prevStep
|
|
2660
2725
|
}),
|
|
2661
|
-
[
|
|
2726
|
+
[clampedIdx, steps.length, draft, setValue, valid, next, prevStep]
|
|
2662
2727
|
);
|
|
2728
|
+
if (status.loading) {
|
|
2729
|
+
return null;
|
|
2730
|
+
}
|
|
2731
|
+
if (!step) {
|
|
2732
|
+
throw new Error(
|
|
2733
|
+
`[hook-template] OnboardingFlow: step index ${clampedIdx} out of range (steps.length=${steps.length})`
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
const Screen = screens[step.screen];
|
|
2737
|
+
if (!Screen) {
|
|
2738
|
+
throw new Error(
|
|
2739
|
+
`[hook-template] OnboardingFlow: missing screen component for step '${step.id}' (expected key '${step.screen}' in screens prop)`
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2663
2742
|
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Screen, {}) });
|
|
2664
2743
|
}
|
|
2665
2744
|
|