@hook-sdk/template 0.18.1 → 0.20.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.js CHANGED
@@ -74,6 +74,17 @@ var DeepLinksSchema = z.object({
74
74
  passwordReset: z.string().startsWith("/").optional(),
75
75
  emailVerify: z.string().startsWith("/").optional()
76
76
  }).strict();
77
+ var I18nConfigSchema = z.object({
78
+ defaultLocale: z.string().min(2),
79
+ supportedLocales: z.array(z.string().min(2)).min(1),
80
+ resources: z.record(z.string(), z.record(z.string(), z.string()))
81
+ }).strict().refine((v) => v.supportedLocales.includes(v.defaultLocale), {
82
+ message: "i18n.defaultLocale must be a member of i18n.supportedLocales",
83
+ path: ["defaultLocale"]
84
+ });
85
+ var InstallPromptSchema = z.object({
86
+ position: z.enum(["pre-auth", "post-paywall"]).optional()
87
+ }).strict();
77
88
  var AppConfigSchema = z.object({
78
89
  slug: z.string().regex(/^[a-z0-9-]+$/),
79
90
  name: z.string().min(1),
@@ -87,12 +98,18 @@ var AppConfigSchema = z.object({
87
98
  persistedKeys: z.array(PersistedKeySchema),
88
99
  onboarding: OnboardingSchema.optional(),
89
100
  deepLinks: DeepLinksSchema.optional(),
101
+ i18n: I18nConfigSchema.optional(),
90
102
  features_enabled: z.array(z.string()).optional(),
103
+ install_prompt: InstallPromptSchema.optional(),
91
104
  // Build-time injected theme metadata (e.g. icon_url for InstallSplash).
92
105
  // Apps don't author this directly; deploy workflows fill it from
93
106
  // env-resolved bundle host. Permissive shape so apps/workflows can
94
107
  // extend without re-bumping the template schema.
95
- theme: z.object({}).passthrough().optional()
108
+ theme: z.object({}).passthrough().optional(),
109
+ // G133 — per-tenant public app-data keys allowlist. Optional; default
110
+ // backfill em migration 0044 = canonical 6. Apps com keys próprias
111
+ // declaram aqui pra evitar 402 spam pós-signup no SubscriptionGate.
112
+ publicKeys: z.array(z.string().regex(SnakeKeyRE)).optional()
96
113
  }).strict();
97
114
  function parseAppConfig(input) {
98
115
  const r = AppConfigSchema.safeParse(input);
@@ -252,7 +269,7 @@ var FALLBACK_PAYWALL = {
252
269
  };
253
270
  var isMethodAvailable = (availability, method) => availability[method] !== false;
254
271
  function usePaywallState() {
255
- const { subscription, plan } = useHook3();
272
+ const { subscription, plan, authStatus, track: track2 } = useHook3();
256
273
  const configFromCtx = useContext3(AppConfigContext);
257
274
  const paywall = configFromCtx?.paywall ?? FALLBACK_PAYWALL;
258
275
  const isFree = paywall.mode === "free";
@@ -341,6 +358,21 @@ function usePaywallState() {
341
358
  [useDefaultMessages]
342
359
  );
343
360
  const submit = useCallback(async () => {
361
+ if (authStatus === "loading") return void 0;
362
+ if (authStatus !== "authenticated") {
363
+ track2("unauthenticated_submit_attempted", {
364
+ method: selectedMethod,
365
+ cycle,
366
+ cpf_valid: cpfValid
367
+ });
368
+ return void 0;
369
+ }
370
+ track2("payment_attempted", {
371
+ method: selectedMethod,
372
+ cycle,
373
+ cpf_valid: cpfValid,
374
+ selected_amount_cents: cycle === "YEARLY" ? plan.data?.yearlyPriceCents ?? (isFree ? 0 : paywall.prices.yearlyCents) : plan.data?.priceCents ?? (isFree ? 0 : paywall.prices.monthlyCents)
375
+ });
344
376
  setSubmitting(true);
345
377
  setError(null);
346
378
  const methodToUse = selectedMethod;
@@ -401,7 +433,7 @@ function usePaywallState() {
401
433
  setSubmitting(false);
402
434
  return void 0;
403
435
  }
404
- }, [selectedMethod, availability, subscription, cycle, cpf, card, buildError]);
436
+ }, [authStatus, track2, selectedMethod, availability, subscription, cycle, cpf, cpfValid, card, buildError, plan, paywall]);
405
437
  const checkout = useCallback(
406
438
  async (args) => {
407
439
  setSubmitting(true);
@@ -1811,9 +1843,9 @@ var bannerStyle = {
1811
1843
 
1812
1844
  // src/components/InstallGate/InstallGate.tsx
1813
1845
  import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1814
- function InstallGate({ children }) {
1846
+ function InstallGate({ children, position }) {
1815
1847
  const { slug, features_enabled } = useTemplateConfig();
1816
- const enabled = features_enabled.includes("install_prompt");
1848
+ const enabled = features_enabled.includes("pwa-install");
1817
1849
  const installState = useInstallPrompt(slug);
1818
1850
  const shouldBlock = enabled && shouldBlockInstall(installState);
1819
1851
  const trackedRef = useRef2(null);
@@ -1828,9 +1860,10 @@ function InstallGate({ children }) {
1828
1860
  platform: installState.platform,
1829
1861
  browser: installState.iosBrowser ?? installState.androidBrowser ?? null,
1830
1862
  in_app_app: installState.inAppApp,
1831
- variant: installState.variant
1863
+ variant: installState.variant,
1864
+ ...position !== void 0 ? { position } : {}
1832
1865
  });
1833
- }, [shouldBlock, slug, installState.variant, installState.platform, installState.iosBrowser, installState.androidBrowser, installState.inAppApp]);
1866
+ }, [shouldBlock, slug, installState.variant, installState.platform, installState.iosBrowser, installState.androidBrowser, installState.inAppApp, position]);
1834
1867
  if (!enabled) return /* @__PURE__ */ jsx16(Fragment3, { children });
1835
1868
  if (installState.isInstalled) return /* @__PURE__ */ jsx16(Fragment3, { children });
1836
1869
  if (installState.variant === "desktop") {
@@ -1953,23 +1986,66 @@ var ErrorBoundary = class extends Component {
1953
1986
  }
1954
1987
  };
1955
1988
 
1989
+ // src/i18n/I18nProvider.tsx
1990
+ import { useEffect as useEffect7 } from "react";
1991
+ import i18n from "i18next";
1992
+ import { I18nextProvider, initReactI18next } from "react-i18next";
1993
+ import { usePersistedState } from "@hook-sdk/sdk";
1994
+ import { jsx as jsx19 } from "react/jsx-runtime";
1995
+ function ensureInitialized(defaultLocale, supportedLocales, resources, initialLocale) {
1996
+ if (i18n.isInitialized) return;
1997
+ i18n.use(initReactI18next).init({
1998
+ resources: Object.fromEntries(
1999
+ supportedLocales.map((l) => [l, { translation: resources[l] ?? {} }])
2000
+ ),
2001
+ lng: initialLocale,
2002
+ fallbackLng: defaultLocale,
2003
+ interpolation: { escapeValue: false },
2004
+ // useTranslation suspends by default until i18next is "ready". Inline
2005
+ // resources are sync, so suspending creates a guaranteed empty render
2006
+ // tick — confusing in apps and breaks tests that don't use Suspense.
2007
+ react: { useSuspense: false }
2008
+ });
2009
+ }
2010
+ function I18nProvider({
2011
+ defaultLocale,
2012
+ supportedLocales,
2013
+ resources,
2014
+ children
2015
+ }) {
2016
+ const [userLocale] = usePersistedState("user-locale", defaultLocale);
2017
+ ensureInitialized(defaultLocale, supportedLocales, resources, userLocale);
2018
+ useEffect7(() => {
2019
+ if (i18n.isInitialized && i18n.language !== userLocale) {
2020
+ i18n.changeLanguage(userLocale);
2021
+ }
2022
+ }, [userLocale]);
2023
+ return /* @__PURE__ */ jsx19(I18nextProvider, { i18n, children });
2024
+ }
2025
+
1956
2026
  // src/internal/PaymentReturnHandler.tsx
1957
- import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef4, useState as useState5 } from "react";
2027
+ import { useCallback as useCallback3, useEffect as useEffect8, useRef as useRef4, useState as useState5 } from "react";
1958
2028
  import { useHook as useHook5 } from "@hook-sdk/sdk";
1959
- import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
2029
+ import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
1960
2030
  var BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 4e4];
1961
2031
  var MAX_CYCLES = 3;
1962
2032
  var SUPPORT_MAILTO = "mailto:suporte@usehook.net?subject=Pagamento%20pendente";
1963
2033
  function PaymentReturnHandler({ children }) {
1964
- const { subscription } = useHook5();
2034
+ const { subscription, track: track2 } = useHook5();
1965
2035
  const subRef = useRef4(subscription);
1966
2036
  subRef.current = subscription;
1967
2037
  const runIdRef = useRef4(0);
1968
2038
  const cyclesRef = useRef4(0);
2039
+ const startMsRef = useRef4(0);
1969
2040
  const [state, setState] = useState5("idle");
1970
2041
  const runPoll = useCallback3(() => {
1971
2042
  const runId = ++runIdRef.current;
2043
+ const isFirstRun = cyclesRef.current === 0;
1972
2044
  cyclesRef.current += 1;
2045
+ if (isFirstRun) {
2046
+ startMsRef.current = Date.now();
2047
+ track2("payment_confirmation_started", {});
2048
+ }
1973
2049
  setState("confirming");
1974
2050
  let attempts = 0;
1975
2051
  const tick = async () => {
@@ -1982,6 +2058,11 @@ function PaymentReturnHandler({ children }) {
1982
2058
  if (runIdRef.current !== runId) return;
1983
2059
  const status = subRef.current.status();
1984
2060
  if (status === "active" || status === "trialing") {
2061
+ track2("payment_confirmation_succeeded", {
2062
+ cycle_count: cyclesRef.current,
2063
+ attempt_count: attempts,
2064
+ duration_ms: Date.now() - startMsRef.current
2065
+ });
1985
2066
  const cleanUrl = new URL(window.location.href);
1986
2067
  cleanUrl.searchParams.delete("paymentReturn");
1987
2068
  window.history.replaceState({}, "", cleanUrl.toString());
@@ -1992,6 +2073,9 @@ function PaymentReturnHandler({ children }) {
1992
2073
  const delay = BACKOFF_MS[attempts - 1];
1993
2074
  if (delay === void 0) {
1994
2075
  if (cyclesRef.current >= MAX_CYCLES) {
2076
+ track2("payment_confirmation_timed_out", {
2077
+ total_duration_ms: Date.now() - startMsRef.current
2078
+ });
1995
2079
  setState("timeout");
1996
2080
  } else {
1997
2081
  setState("waiting");
@@ -2001,8 +2085,8 @@ function PaymentReturnHandler({ children }) {
2001
2085
  setTimeout(tick, delay);
2002
2086
  };
2003
2087
  void tick();
2004
- }, []);
2005
- useEffect7(() => {
2088
+ }, [track2]);
2089
+ useEffect8(() => {
2006
2090
  if (typeof window === "undefined") return;
2007
2091
  const url = new URL(window.location.href);
2008
2092
  if (url.searchParams.get("paymentReturn") !== "1") return;
@@ -2019,19 +2103,19 @@ function PaymentReturnHandler({ children }) {
2019
2103
  window.location.href = cleanUrl.toString();
2020
2104
  }, []);
2021
2105
  if (state === "confirming") {
2022
- return /* @__PURE__ */ jsx19("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: "Confirmando pagamento\u2026" });
2106
+ return /* @__PURE__ */ jsx20("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: "Confirmando pagamento\u2026" });
2023
2107
  }
2024
2108
  if (state === "waiting") {
2025
- return /* @__PURE__ */ jsx19("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 320, textAlign: "center", lineHeight: 1.5 }, children: [
2026
- /* @__PURE__ */ jsx19("div", { style: { marginBottom: 16 }, children: "Pagamento aceito. Estamos confirmando com o banco \u2014 pode levar alguns minutos." }),
2027
- /* @__PURE__ */ jsx19("button", { type: "button", onClick: runPoll, style: buttonStyle, children: "Atualizar" })
2109
+ return /* @__PURE__ */ jsx20("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 320, textAlign: "center", lineHeight: 1.5 }, children: [
2110
+ /* @__PURE__ */ jsx20("div", { style: { marginBottom: 16 }, children: "Pagamento aceito. Estamos confirmando com o banco \u2014 pode levar alguns minutos." }),
2111
+ /* @__PURE__ */ jsx20("button", { type: "button", onClick: runPoll, style: buttonStyle, children: "Atualizar" })
2028
2112
  ] }) });
2029
2113
  }
2030
2114
  if (state === "timeout") {
2031
- return /* @__PURE__ */ jsx19("div", { role: "alert", "aria-live": "assertive", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 360, textAlign: "center", lineHeight: 1.5 }, children: [
2032
- /* @__PURE__ */ jsx19("div", { style: { marginBottom: 16 }, children: "Ainda n\xE3o conseguimos confirmar seu pagamento com o banco. Voc\xEA pode tentar de novo, voltar pro app, ou falar com a gente." }),
2115
+ return /* @__PURE__ */ jsx20("div", { role: "alert", "aria-live": "assertive", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 360, textAlign: "center", lineHeight: 1.5 }, children: [
2116
+ /* @__PURE__ */ jsx20("div", { style: { marginBottom: 16 }, children: "Ainda n\xE3o conseguimos confirmar seu pagamento com o banco. Voc\xEA pode tentar de novo, voltar pro app, ou falar com a gente." }),
2033
2117
  /* @__PURE__ */ jsxs13("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
2034
- /* @__PURE__ */ jsx19(
2118
+ /* @__PURE__ */ jsx20(
2035
2119
  "button",
2036
2120
  {
2037
2121
  type: "button",
@@ -2044,7 +2128,7 @@ function PaymentReturnHandler({ children }) {
2044
2128
  children: "Tentar de novo"
2045
2129
  }
2046
2130
  ),
2047
- /* @__PURE__ */ jsx19(
2131
+ /* @__PURE__ */ jsx20(
2048
2132
  "button",
2049
2133
  {
2050
2134
  type: "button",
@@ -2054,7 +2138,7 @@ function PaymentReturnHandler({ children }) {
2054
2138
  children: "Voltar pro app"
2055
2139
  }
2056
2140
  ),
2057
- /* @__PURE__ */ jsx19(
2141
+ /* @__PURE__ */ jsx20(
2058
2142
  "a",
2059
2143
  {
2060
2144
  href: SUPPORT_MAILTO,
@@ -2066,7 +2150,7 @@ function PaymentReturnHandler({ children }) {
2066
2150
  ] })
2067
2151
  ] }) });
2068
2152
  }
2069
- return /* @__PURE__ */ jsx19(Fragment5, { children });
2153
+ return /* @__PURE__ */ jsx20(Fragment5, { children });
2070
2154
  }
2071
2155
  var overlayStyle2 = {
2072
2156
  position: "fixed",
@@ -2105,7 +2189,7 @@ var linkStyle = {
2105
2189
  };
2106
2190
 
2107
2191
  // src/AppRoot.tsx
2108
- import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
2192
+ import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
2109
2193
  function buildLegacyConfigShim(config) {
2110
2194
  const paywall = config.paywall;
2111
2195
  const isFree = paywall.mode === "free";
@@ -2182,28 +2266,43 @@ function AppRoot(props) {
2182
2266
  const Router = testRouter === "memory" ? MemoryRouter : BrowserRouter;
2183
2267
  const basename = `/app/${config.slug}`;
2184
2268
  const routerProps = testRouter === "memory" ? { basename, initialEntries: testInitialEntries } : { basename };
2185
- return /* @__PURE__ */ jsx20(ErrorBoundary, { children: /* @__PURE__ */ jsx20(AppConfigProvider, { config, children: /* @__PURE__ */ jsx20(TemplateConfigProvider, { config: legacyShim, children: /* @__PURE__ */ jsx20(ThemeProvider, { children: /* @__PURE__ */ jsx20(PersistenceRegistry, { config: config.persistedKeys, children: /* @__PURE__ */ jsxs14(Router, { ...routerProps, children: [
2186
- /* @__PURE__ */ jsx20(DeepLinkHandler, { deepLinks: config.deepLinks }),
2187
- /* @__PURE__ */ jsx20(SessionExpiredBanner, {}),
2188
- /* @__PURE__ */ jsx20(InstallGate, { children: /* @__PURE__ */ jsx20(
2189
- AuthGated,
2190
- {
2191
- config,
2192
- Login,
2193
- Signup,
2194
- Forgot,
2195
- Reset,
2196
- EmailVerify,
2197
- Paywall,
2198
- Onboarding,
2199
- PreAuthFlow,
2200
- children: /* @__PURE__ */ jsxs14(SubscriptionGate, { Paywall: Paywall ?? FallbackPaywall, children: [
2201
- children,
2202
- /* @__PURE__ */ jsx20(PushPrompt, {})
2203
- ] })
2204
- }
2205
- ) })
2206
- ] }) }) }) }) }) });
2269
+ const position = config.install_prompt?.position ?? "post-paywall";
2270
+ const subscriptionGated = /* @__PURE__ */ jsx21(SubscriptionGate, { Paywall: Paywall ?? FallbackPaywall, children: position === "post-paywall" ? /* @__PURE__ */ jsxs14(InstallGate, { position: "post-paywall", children: [
2271
+ children,
2272
+ /* @__PURE__ */ jsx21(PushPrompt, {})
2273
+ ] }) : /* @__PURE__ */ jsxs14(Fragment6, { children: [
2274
+ children,
2275
+ /* @__PURE__ */ jsx21(PushPrompt, {})
2276
+ ] }) });
2277
+ const authGated = /* @__PURE__ */ jsx21(
2278
+ AuthGated,
2279
+ {
2280
+ config,
2281
+ Login,
2282
+ Signup,
2283
+ Forgot,
2284
+ Reset,
2285
+ EmailVerify,
2286
+ Paywall,
2287
+ Onboarding,
2288
+ PreAuthFlow,
2289
+ children: subscriptionGated
2290
+ }
2291
+ );
2292
+ const routedTree = /* @__PURE__ */ jsxs14(Router, { ...routerProps, children: [
2293
+ /* @__PURE__ */ jsx21(DeepLinkHandler, { deepLinks: config.deepLinks }),
2294
+ /* @__PURE__ */ jsx21(SessionExpiredBanner, {}),
2295
+ position === "pre-auth" ? /* @__PURE__ */ jsx21(InstallGate, { position: "pre-auth", children: authGated }) : authGated
2296
+ ] });
2297
+ return /* @__PURE__ */ jsx21(ErrorBoundary, { children: /* @__PURE__ */ jsx21(AppConfigProvider, { config, children: /* @__PURE__ */ jsx21(TemplateConfigProvider, { config: legacyShim, children: /* @__PURE__ */ jsx21(ThemeProvider, { children: /* @__PURE__ */ jsx21(PersistenceRegistry, { config: config.persistedKeys, children: config.i18n ? /* @__PURE__ */ jsx21(
2298
+ I18nProvider,
2299
+ {
2300
+ defaultLocale: config.i18n.defaultLocale,
2301
+ supportedLocales: config.i18n.supportedLocales,
2302
+ resources: config.i18n.resources,
2303
+ children: routedTree
2304
+ }
2305
+ ) : routedTree }) }) }) }) });
2207
2306
  }
2208
2307
  function AuthGated({
2209
2308
  children,
@@ -2220,31 +2319,31 @@ function AuthGated({
2220
2319
  if (authStatus !== "authenticated") {
2221
2320
  if (config.onboarding?.trigger === "pre_signup_custom" && PreAuthFlow) {
2222
2321
  return /* @__PURE__ */ jsxs14(Routes, { children: [
2223
- /* @__PURE__ */ jsx20(Route, { path: "/signin", element: /* @__PURE__ */ jsx20(Login, {}) }),
2224
- /* @__PURE__ */ jsx20(Route, { path: "/signup", element: /* @__PURE__ */ jsx20(Signup, {}) }),
2225
- /* @__PURE__ */ jsx20(Route, { path: "/forgot", element: /* @__PURE__ */ jsx20(Forgot, {}) }),
2226
- /* @__PURE__ */ jsx20(Route, { path: "/reset", element: /* @__PURE__ */ jsx20(Reset, {}) }),
2227
- EmailVerify ? /* @__PURE__ */ jsx20(Route, { path: "/verify", element: /* @__PURE__ */ jsx20(EmailVerify, {}) }) : null,
2228
- /* @__PURE__ */ jsx20(Route, { path: "/*", element: /* @__PURE__ */ jsx20(PreAuthFlow, {}) })
2322
+ /* @__PURE__ */ jsx21(Route, { path: "/signin", element: /* @__PURE__ */ jsx21(Login, {}) }),
2323
+ /* @__PURE__ */ jsx21(Route, { path: "/signup", element: /* @__PURE__ */ jsx21(Signup, {}) }),
2324
+ /* @__PURE__ */ jsx21(Route, { path: "/forgot", element: /* @__PURE__ */ jsx21(Forgot, {}) }),
2325
+ /* @__PURE__ */ jsx21(Route, { path: "/reset", element: /* @__PURE__ */ jsx21(Reset, {}) }),
2326
+ EmailVerify ? /* @__PURE__ */ jsx21(Route, { path: "/verify", element: /* @__PURE__ */ jsx21(EmailVerify, {}) }) : null,
2327
+ /* @__PURE__ */ jsx21(Route, { path: "/*", element: /* @__PURE__ */ jsx21(PreAuthFlow, {}) })
2229
2328
  ] });
2230
2329
  }
2231
2330
  return /* @__PURE__ */ jsxs14(Routes, { children: [
2232
- /* @__PURE__ */ jsx20(Route, { path: "/", element: /* @__PURE__ */ jsx20(Login, {}) }),
2233
- /* @__PURE__ */ jsx20(Route, { path: "/signup", element: /* @__PURE__ */ jsx20(Signup, {}) }),
2234
- /* @__PURE__ */ jsx20(Route, { path: "/forgot", element: /* @__PURE__ */ jsx20(Forgot, {}) }),
2235
- /* @__PURE__ */ jsx20(Route, { path: "/reset", element: /* @__PURE__ */ jsx20(Reset, {}) }),
2236
- EmailVerify ? /* @__PURE__ */ jsx20(Route, { path: "/verify", element: /* @__PURE__ */ jsx20(EmailVerify, {}) }) : null,
2237
- /* @__PURE__ */ jsx20(Route, { path: "*", element: /* @__PURE__ */ jsx20(Navigate, { to: "/", replace: true }) })
2331
+ /* @__PURE__ */ jsx21(Route, { path: "/", element: /* @__PURE__ */ jsx21(Login, {}) }),
2332
+ /* @__PURE__ */ jsx21(Route, { path: "/signup", element: /* @__PURE__ */ jsx21(Signup, {}) }),
2333
+ /* @__PURE__ */ jsx21(Route, { path: "/forgot", element: /* @__PURE__ */ jsx21(Forgot, {}) }),
2334
+ /* @__PURE__ */ jsx21(Route, { path: "/reset", element: /* @__PURE__ */ jsx21(Reset, {}) }),
2335
+ EmailVerify ? /* @__PURE__ */ jsx21(Route, { path: "/verify", element: /* @__PURE__ */ jsx21(EmailVerify, {}) }) : null,
2336
+ /* @__PURE__ */ jsx21(Route, { path: "*", element: /* @__PURE__ */ jsx21(Navigate, { to: "/", replace: true }) })
2238
2337
  ] });
2239
2338
  }
2240
- return /* @__PURE__ */ jsx20(Fragment6, { children });
2339
+ return /* @__PURE__ */ jsx21(Fragment6, { children });
2241
2340
  }
2242
2341
  function FallbackPaywall() {
2243
2342
  return null;
2244
2343
  }
2245
2344
 
2246
2345
  // src/hooks/usePush.ts
2247
- import { useCallback as useCallback4, useEffect as useEffect8, useState as useState6 } from "react";
2346
+ import { useCallback as useCallback4, useEffect as useEffect9, useState as useState6 } from "react";
2248
2347
  import { useHook as useHook7 } from "@hook-sdk/sdk";
2249
2348
  var DISMISS_STORAGE_KEY = "push:dismissed-until";
2250
2349
  var DISMISS_TTL_MS2 = 7 * 24 * 60 * 60 * 1e3;
@@ -2291,7 +2390,7 @@ function deriveState(push) {
2291
2390
  function usePush() {
2292
2391
  const { push } = useHook7();
2293
2392
  const [state, setState] = useState6(() => deriveState(push));
2294
- useEffect8(() => {
2393
+ useEffect9(() => {
2295
2394
  setState(deriveState(push));
2296
2395
  }, [push]);
2297
2396
  const subscribe = useCallback4(async () => {
@@ -2328,51 +2427,27 @@ function usePush() {
2328
2427
  }
2329
2428
 
2330
2429
  // src/components/PushPrompt.tsx
2331
- import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
2332
- function platformRecoveryCopy(texts) {
2333
- if (typeof navigator === "undefined") return null;
2334
- const ua = navigator.userAgent || "";
2335
- const platform = detectPlatform(ua);
2336
- switch (platform) {
2337
- case "ios-safari":
2338
- case "ios-other":
2339
- return texts.deniedRecoveryIos ?? null;
2340
- case "android":
2341
- return texts.deniedRecoveryAndroid ?? null;
2342
- case "desktop":
2343
- return texts.deniedRecoveryDesktop ?? null;
2344
- case "in-app":
2345
- return texts.deniedRecoveryInApp ?? null;
2346
- default:
2347
- return null;
2348
- }
2349
- }
2430
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
2350
2431
  function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, className }) {
2351
2432
  const { state, subscribe } = usePush();
2352
- if (state.kind === "subscribed" || state.kind === "dismissed") return null;
2433
+ if (state.kind === "denied" || state.kind === "dismissed" || state.kind === "subscribed") {
2434
+ return null;
2435
+ }
2353
2436
  if (state.kind === "ios_needs_install") {
2354
2437
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", "aria-label": texts.iosInstallTitle, children: [
2355
- /* @__PURE__ */ jsx21("h3", { children: texts.iosInstallTitle }),
2356
- /* @__PURE__ */ jsx21("p", { children: texts.iosInstallBody }),
2357
- onInstallRequested && texts.iosInstallCta && /* @__PURE__ */ jsx21("button", { onClick: onInstallRequested, children: texts.iosInstallCta })
2358
- ] });
2359
- }
2360
- if (state.kind === "denied") {
2361
- const recovery = platformRecoveryCopy(texts);
2362
- return /* @__PURE__ */ jsxs15("div", { className, role: "region", "aria-label": texts.deniedTitle, children: [
2363
- /* @__PURE__ */ jsx21("h3", { children: texts.deniedTitle }),
2364
- /* @__PURE__ */ jsx21("p", { children: texts.deniedBody }),
2365
- recovery && /* @__PURE__ */ jsx21("p", { "data-testid": "denied-recovery", children: recovery })
2438
+ /* @__PURE__ */ jsx22("h3", { children: texts.iosInstallTitle }),
2439
+ /* @__PURE__ */ jsx22("p", { children: texts.iosInstallBody }),
2440
+ onInstallRequested && texts.iosInstallCta && /* @__PURE__ */ jsx22("button", { onClick: onInstallRequested, children: texts.iosInstallCta })
2366
2441
  ] });
2367
2442
  }
2368
2443
  if (state.kind === "unsupported") {
2369
- return /* @__PURE__ */ jsx21("div", { className, role: "region", children: /* @__PURE__ */ jsx21("p", { children: texts.unsupportedBody }) });
2444
+ return /* @__PURE__ */ jsx22("div", { className, role: "region", children: /* @__PURE__ */ jsx22("p", { children: texts.unsupportedBody }) });
2370
2445
  }
2371
2446
  if (state.kind === "error") {
2372
- return /* @__PURE__ */ jsx21("div", { className, role: "region", "aria-label": "error", children: /* @__PURE__ */ jsx21("p", { children: state.message }) });
2447
+ return /* @__PURE__ */ jsx22("div", { className, role: "region", "aria-label": "error", children: /* @__PURE__ */ jsx22("p", { children: state.message }) });
2373
2448
  }
2374
2449
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", children: [
2375
- /* @__PURE__ */ jsx21(
2450
+ /* @__PURE__ */ jsx22(
2376
2451
  "button",
2377
2452
  {
2378
2453
  type: "button",
@@ -2386,23 +2461,49 @@ function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, clas
2386
2461
  children: texts.cta
2387
2462
  }
2388
2463
  ),
2389
- onDeclined && /* @__PURE__ */ jsx21("button", { type: "button", onClick: onDeclined, children: texts.declineCta })
2464
+ onDeclined && /* @__PURE__ */ jsx22("button", { type: "button", onClick: onDeclined, children: texts.declineCta })
2465
+ ] });
2466
+ }
2467
+
2468
+ // src/components/LanguageSwitcher.tsx
2469
+ import { usePersistedState as usePersistedState2 } from "@hook-sdk/sdk";
2470
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
2471
+ function LanguageSwitcher({ id, className, label = "Language" }) {
2472
+ const config = useAppConfig();
2473
+ const i18nConfig = config.i18n;
2474
+ const [userLocale, setUserLocale] = usePersistedState2(
2475
+ "user-locale",
2476
+ i18nConfig?.defaultLocale ?? "en-US"
2477
+ );
2478
+ if (!i18nConfig) return null;
2479
+ return /* @__PURE__ */ jsxs16("label", { className, children: [
2480
+ label ? /* @__PURE__ */ jsx23("span", { children: label }) : null,
2481
+ /* @__PURE__ */ jsx23(
2482
+ "select",
2483
+ {
2484
+ id,
2485
+ value: userLocale,
2486
+ onChange: (e) => setUserLocale(e.target.value),
2487
+ "data-testid": "language-switcher",
2488
+ children: i18nConfig.supportedLocales.map((loc) => /* @__PURE__ */ jsx23("option", { value: loc, children: loc }, loc))
2489
+ }
2490
+ )
2390
2491
  ] });
2391
2492
  }
2392
2493
 
2393
2494
  // src/defaults/LoadingState.tsx
2394
- import { jsx as jsx22 } from "react/jsx-runtime";
2495
+ import { jsx as jsx24 } from "react/jsx-runtime";
2395
2496
  function LoadingState({ message }) {
2396
- return /* @__PURE__ */ jsx22("div", { role: "status", "aria-live": "polite", style: { padding: 24, textAlign: "center" }, children: /* @__PURE__ */ jsx22("span", { children: message ?? "Carregando..." }) });
2497
+ return /* @__PURE__ */ jsx24("div", { role: "status", "aria-live": "polite", style: { padding: 24, textAlign: "center" }, children: /* @__PURE__ */ jsx24("span", { children: message ?? "Carregando..." }) });
2397
2498
  }
2398
2499
 
2399
2500
  // src/defaults/EmptyState.tsx
2400
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
2501
+ import { jsx as jsx25, jsxs as jsxs17 } from "react/jsx-runtime";
2401
2502
  function EmptyState({ title, description, action }) {
2402
- return /* @__PURE__ */ jsxs16("div", { role: "status", style: { padding: 32, textAlign: "center" }, children: [
2403
- /* @__PURE__ */ jsx23("h2", { style: { marginBottom: 8 }, children: title }),
2404
- description && /* @__PURE__ */ jsx23("p", { style: { opacity: 0.7 }, children: description }),
2405
- action && /* @__PURE__ */ jsx23("div", { style: { marginTop: 16 }, children: action })
2503
+ return /* @__PURE__ */ jsxs17("div", { role: "status", style: { padding: 32, textAlign: "center" }, children: [
2504
+ /* @__PURE__ */ jsx25("h2", { style: { marginBottom: 8 }, children: title }),
2505
+ description && /* @__PURE__ */ jsx25("p", { style: { opacity: 0.7 }, children: description }),
2506
+ action && /* @__PURE__ */ jsx25("div", { style: { marginTop: 16 }, children: action })
2406
2507
  ] });
2407
2508
  }
2408
2509
 
@@ -2619,7 +2720,7 @@ function useForgotForm() {
2619
2720
  }
2620
2721
 
2621
2722
  // src/hooks/useResetForm.ts
2622
- import { useCallback as useCallback8, useEffect as useEffect9, useMemo as useMemo7, useState as useState10 } from "react";
2723
+ import { useCallback as useCallback8, useEffect as useEffect10, useMemo as useMemo7, useState as useState10 } from "react";
2623
2724
  import { useHook as useHook11 } from "@hook-sdk/sdk";
2624
2725
  var MIN_PASSWORD3 = 12;
2625
2726
  function useResetForm() {
@@ -2633,7 +2734,7 @@ function useResetForm() {
2633
2734
  const [touchedPassword, setTouchedPassword] = useState10(false);
2634
2735
  const [touchedConfirm, setTouchedConfirm] = useState10(false);
2635
2736
  const [formSubmitAttempted, setFormSubmitAttempted] = useState10(false);
2636
- useEffect9(() => {
2737
+ useEffect10(() => {
2637
2738
  if (typeof window === "undefined") return;
2638
2739
  const params = new URLSearchParams(window.location.search);
2639
2740
  const t = params.get("token");
@@ -2727,12 +2828,12 @@ function discountPercent(anchorCents, realCents) {
2727
2828
  }
2728
2829
 
2729
2830
  // src/hooks/useAuthPrimitives.ts
2730
- import { useEffect as useEffect10 } from "react";
2831
+ import { useEffect as useEffect11 } from "react";
2731
2832
  import { useHook as useHook13 } from "@hook-sdk/sdk";
2732
2833
  var warned = false;
2733
2834
  function useAuthPrimitives() {
2734
2835
  const { auth } = useHook13();
2735
- useEffect10(() => {
2836
+ useEffect11(() => {
2736
2837
  if (!warned && process.env.NODE_ENV !== "production") {
2737
2838
  warned = true;
2738
2839
  console.warn(
@@ -2777,7 +2878,7 @@ function useSubscription() {
2777
2878
  }
2778
2879
 
2779
2880
  // src/hooks/useReminders.ts
2780
- import { useCallback as useCallback9, useEffect as useEffect11, useState as useState11 } from "react";
2881
+ import { useCallback as useCallback9, useEffect as useEffect12, useState as useState11 } from "react";
2781
2882
  import { useHook as useHook16 } from "@hook-sdk/sdk";
2782
2883
  function useReminders() {
2783
2884
  const { push } = useHook16();
@@ -2793,7 +2894,7 @@ function useReminders() {
2793
2894
  setLoading(false);
2794
2895
  }
2795
2896
  }, [r]);
2796
- useEffect11(() => {
2897
+ useEffect12(() => {
2797
2898
  void reload();
2798
2899
  }, [reload]);
2799
2900
  const setReminder = useCallback9(async (input) => {
@@ -2832,20 +2933,20 @@ function useToast() {
2832
2933
 
2833
2934
  // src/RouteBoundary.tsx
2834
2935
  import { Routes as Routes2, Route as Route2 } from "react-router-dom";
2835
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
2936
+ import { jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
2836
2937
  function RouteBoundary({ children }) {
2837
- return /* @__PURE__ */ jsxs17(Routes2, { children: [
2938
+ return /* @__PURE__ */ jsxs18(Routes2, { children: [
2838
2939
  children,
2839
- /* @__PURE__ */ jsx24(Route2, { path: "*", element: /* @__PURE__ */ jsx24(DefaultNotFound, {}) })
2940
+ /* @__PURE__ */ jsx26(Route2, { path: "*", element: /* @__PURE__ */ jsx26(DefaultNotFound, {}) })
2840
2941
  ] });
2841
2942
  }
2842
2943
  function DefaultNotFound() {
2843
- return /* @__PURE__ */ jsx24("div", { role: "alert", children: "P\xE1gina n\xE3o encontrada" });
2944
+ return /* @__PURE__ */ jsx26("div", { role: "alert", children: "P\xE1gina n\xE3o encontrada" });
2844
2945
  }
2845
2946
 
2846
2947
  // src/PreAuthShell.tsx
2847
2948
  import { BrowserRouter as BrowserRouter2, MemoryRouter as MemoryRouter2, Routes as Routes3 } from "react-router-dom";
2848
- import { jsx as jsx25 } from "react/jsx-runtime";
2949
+ import { jsx as jsx27 } from "react/jsx-runtime";
2849
2950
  function PreAuthShell({
2850
2951
  basename,
2851
2952
  testRouter,
@@ -2853,14 +2954,14 @@ function PreAuthShell({
2853
2954
  children
2854
2955
  }) {
2855
2956
  if (testRouter === "memory") {
2856
- return /* @__PURE__ */ jsx25(MemoryRouter2, { basename, initialEntries: testInitialEntries, children: /* @__PURE__ */ jsx25(Routes3, { children }) });
2957
+ return /* @__PURE__ */ jsx27(MemoryRouter2, { basename, initialEntries: testInitialEntries, children: /* @__PURE__ */ jsx27(Routes3, { children }) });
2857
2958
  }
2858
- return /* @__PURE__ */ jsx25(BrowserRouter2, { basename, children: /* @__PURE__ */ jsx25(Routes3, { children }) });
2959
+ return /* @__PURE__ */ jsx27(BrowserRouter2, { basename, children: /* @__PURE__ */ jsx27(Routes3, { children }) });
2859
2960
  }
2860
2961
 
2861
2962
  // src/OnboardingFlow.tsx
2862
- import { useCallback as useCallback11, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef5 } from "react";
2863
- import { usePersistedState, useHook as useHook17 } from "@hook-sdk/sdk";
2963
+ import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef5 } from "react";
2964
+ import { usePersistedState as usePersistedState3, useHook as useHook17 } from "@hook-sdk/sdk";
2864
2965
 
2865
2966
  // src/hooks/useOnboardingStep.ts
2866
2967
  import { createContext as createContext3, useContext as useContext4 } from "react";
@@ -2876,7 +2977,7 @@ function useOnboardingStep() {
2876
2977
  }
2877
2978
 
2878
2979
  // src/OnboardingFlow.tsx
2879
- import { jsx as jsx26 } from "react/jsx-runtime";
2980
+ import { jsx as jsx28 } from "react/jsx-runtime";
2880
2981
  var isFilled = (v) => v != null && v !== "";
2881
2982
  var CURRENT_STEP_FIELD = "currentStep";
2882
2983
  function readPersistedStepIdx(draft) {
@@ -2889,7 +2990,7 @@ function OnboardingFlow({
2889
2990
  onComplete,
2890
2991
  persistKey
2891
2992
  }) {
2892
- const [draft, setDraft, status] = usePersistedState(persistKey, {});
2993
+ const [draft, setDraft, status] = usePersistedState3(persistKey, {});
2893
2994
  const draftRef = useRef5(draft);
2894
2995
  draftRef.current = draft;
2895
2996
  const idx = readPersistedStepIdx(draft);
@@ -2914,7 +3015,7 @@ function OnboardingFlow({
2914
3015
  const step = steps[clampedIdx];
2915
3016
  const hookCtx = useHook17();
2916
3017
  const track2 = typeof hookCtx.track === "function" ? hookCtx.track : void 0;
2917
- useEffect12(() => {
3018
+ useEffect13(() => {
2918
3019
  if (status.loading) return;
2919
3020
  if (!step) return;
2920
3021
  if (!track2) return;
@@ -2966,7 +3067,7 @@ function OnboardingFlow({
2966
3067
  `[hook-template] OnboardingFlow: missing screen component for step '${step.id}' (expected key '${step.screen}' in screens prop)`
2967
3068
  );
2968
3069
  }
2969
- return /* @__PURE__ */ jsx26(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx26(Screen, {}) });
3070
+ return /* @__PURE__ */ jsx28(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx28(Screen, {}) });
2970
3071
  }
2971
3072
 
2972
3073
  // src/hooks/useFeature.ts
@@ -2981,8 +3082,10 @@ export {
2981
3082
  DeepLinkHandler,
2982
3083
  EmptyState,
2983
3084
  ErrorBoundary,
3085
+ I18nProvider,
2984
3086
  InstallGate,
2985
3087
  InstallSplash,
3088
+ LanguageSwitcher,
2986
3089
  LoadingState,
2987
3090
  OnboardingFlow,
2988
3091
  PaymentReturnHandler,