@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 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("[ErrorBoundary] caught", 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
- return { state, subscribe, unsubscribe };
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 step = steps[idx];
2618
- if (!step) {
2619
- throw new Error(
2620
- `[hook-template] OnboardingFlow: step index ${idx} out of range (steps.length=${steps.length})`
2621
- );
2622
- }
2623
- const Screen = screens[step.screen];
2624
- if (!Screen) {
2625
- throw new Error(
2626
- `[hook-template] OnboardingFlow: missing screen component for step '${step.id}' (expected key '${step.screen}' in screens prop)`
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((prev2) => ({ ...prev2, ...patch }));
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 (idx + 1 >= steps.length) {
2709
+ if (clampedIdx + 1 >= steps.length) {
2645
2710
  onComplete(current);
2646
2711
  } else {
2647
- setIdx(idx + 1);
2712
+ setIdx(clampedIdx + 1);
2648
2713
  }
2649
- }, [idx, onComplete, step, steps.length]);
2650
- const prev = (0, import_react21.useCallback)(() => setIdx((i) => Math.max(0, i - 1)), []);
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: idx,
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
- [idx, steps.length, draft, setValue, valid, next, prev]
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