@hook-sdk/template 0.18.1 → 0.19.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);
@@ -1811,9 +1828,9 @@ var bannerStyle = {
1811
1828
 
1812
1829
  // src/components/InstallGate/InstallGate.tsx
1813
1830
  import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1814
- function InstallGate({ children }) {
1831
+ function InstallGate({ children, position }) {
1815
1832
  const { slug, features_enabled } = useTemplateConfig();
1816
- const enabled = features_enabled.includes("install_prompt");
1833
+ const enabled = features_enabled.includes("pwa-install");
1817
1834
  const installState = useInstallPrompt(slug);
1818
1835
  const shouldBlock = enabled && shouldBlockInstall(installState);
1819
1836
  const trackedRef = useRef2(null);
@@ -1828,9 +1845,10 @@ function InstallGate({ children }) {
1828
1845
  platform: installState.platform,
1829
1846
  browser: installState.iosBrowser ?? installState.androidBrowser ?? null,
1830
1847
  in_app_app: installState.inAppApp,
1831
- variant: installState.variant
1848
+ variant: installState.variant,
1849
+ ...position !== void 0 ? { position } : {}
1832
1850
  });
1833
- }, [shouldBlock, slug, installState.variant, installState.platform, installState.iosBrowser, installState.androidBrowser, installState.inAppApp]);
1851
+ }, [shouldBlock, slug, installState.variant, installState.platform, installState.iosBrowser, installState.androidBrowser, installState.inAppApp, position]);
1834
1852
  if (!enabled) return /* @__PURE__ */ jsx16(Fragment3, { children });
1835
1853
  if (installState.isInstalled) return /* @__PURE__ */ jsx16(Fragment3, { children });
1836
1854
  if (installState.variant === "desktop") {
@@ -1953,10 +1971,47 @@ var ErrorBoundary = class extends Component {
1953
1971
  }
1954
1972
  };
1955
1973
 
1974
+ // src/i18n/I18nProvider.tsx
1975
+ import { useEffect as useEffect7 } from "react";
1976
+ import i18n from "i18next";
1977
+ import { I18nextProvider, initReactI18next } from "react-i18next";
1978
+ import { usePersistedState } from "@hook-sdk/sdk";
1979
+ import { jsx as jsx19 } from "react/jsx-runtime";
1980
+ function ensureInitialized(defaultLocale, supportedLocales, resources, initialLocale) {
1981
+ if (i18n.isInitialized) return;
1982
+ i18n.use(initReactI18next).init({
1983
+ resources: Object.fromEntries(
1984
+ supportedLocales.map((l) => [l, { translation: resources[l] ?? {} }])
1985
+ ),
1986
+ lng: initialLocale,
1987
+ fallbackLng: defaultLocale,
1988
+ interpolation: { escapeValue: false },
1989
+ // useTranslation suspends by default until i18next is "ready". Inline
1990
+ // resources are sync, so suspending creates a guaranteed empty render
1991
+ // tick — confusing in apps and breaks tests that don't use Suspense.
1992
+ react: { useSuspense: false }
1993
+ });
1994
+ }
1995
+ function I18nProvider({
1996
+ defaultLocale,
1997
+ supportedLocales,
1998
+ resources,
1999
+ children
2000
+ }) {
2001
+ const [userLocale] = usePersistedState("user-locale", defaultLocale);
2002
+ ensureInitialized(defaultLocale, supportedLocales, resources, userLocale);
2003
+ useEffect7(() => {
2004
+ if (i18n.isInitialized && i18n.language !== userLocale) {
2005
+ i18n.changeLanguage(userLocale);
2006
+ }
2007
+ }, [userLocale]);
2008
+ return /* @__PURE__ */ jsx19(I18nextProvider, { i18n, children });
2009
+ }
2010
+
1956
2011
  // src/internal/PaymentReturnHandler.tsx
1957
- import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef4, useState as useState5 } from "react";
2012
+ import { useCallback as useCallback3, useEffect as useEffect8, useRef as useRef4, useState as useState5 } from "react";
1958
2013
  import { useHook as useHook5 } from "@hook-sdk/sdk";
1959
- import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
2014
+ import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
1960
2015
  var BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 4e4];
1961
2016
  var MAX_CYCLES = 3;
1962
2017
  var SUPPORT_MAILTO = "mailto:suporte@usehook.net?subject=Pagamento%20pendente";
@@ -2002,7 +2057,7 @@ function PaymentReturnHandler({ children }) {
2002
2057
  };
2003
2058
  void tick();
2004
2059
  }, []);
2005
- useEffect7(() => {
2060
+ useEffect8(() => {
2006
2061
  if (typeof window === "undefined") return;
2007
2062
  const url = new URL(window.location.href);
2008
2063
  if (url.searchParams.get("paymentReturn") !== "1") return;
@@ -2019,19 +2074,19 @@ function PaymentReturnHandler({ children }) {
2019
2074
  window.location.href = cleanUrl.toString();
2020
2075
  }, []);
2021
2076
  if (state === "confirming") {
2022
- return /* @__PURE__ */ jsx19("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: "Confirmando pagamento\u2026" });
2077
+ return /* @__PURE__ */ jsx20("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: "Confirmando pagamento\u2026" });
2023
2078
  }
2024
2079
  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" })
2080
+ return /* @__PURE__ */ jsx20("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 320, textAlign: "center", lineHeight: 1.5 }, children: [
2081
+ /* @__PURE__ */ jsx20("div", { style: { marginBottom: 16 }, children: "Pagamento aceito. Estamos confirmando com o banco \u2014 pode levar alguns minutos." }),
2082
+ /* @__PURE__ */ jsx20("button", { type: "button", onClick: runPoll, style: buttonStyle, children: "Atualizar" })
2028
2083
  ] }) });
2029
2084
  }
2030
2085
  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." }),
2086
+ return /* @__PURE__ */ jsx20("div", { role: "alert", "aria-live": "assertive", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 360, textAlign: "center", lineHeight: 1.5 }, children: [
2087
+ /* @__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
2088
  /* @__PURE__ */ jsxs13("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
2034
- /* @__PURE__ */ jsx19(
2089
+ /* @__PURE__ */ jsx20(
2035
2090
  "button",
2036
2091
  {
2037
2092
  type: "button",
@@ -2044,7 +2099,7 @@ function PaymentReturnHandler({ children }) {
2044
2099
  children: "Tentar de novo"
2045
2100
  }
2046
2101
  ),
2047
- /* @__PURE__ */ jsx19(
2102
+ /* @__PURE__ */ jsx20(
2048
2103
  "button",
2049
2104
  {
2050
2105
  type: "button",
@@ -2054,7 +2109,7 @@ function PaymentReturnHandler({ children }) {
2054
2109
  children: "Voltar pro app"
2055
2110
  }
2056
2111
  ),
2057
- /* @__PURE__ */ jsx19(
2112
+ /* @__PURE__ */ jsx20(
2058
2113
  "a",
2059
2114
  {
2060
2115
  href: SUPPORT_MAILTO,
@@ -2066,7 +2121,7 @@ function PaymentReturnHandler({ children }) {
2066
2121
  ] })
2067
2122
  ] }) });
2068
2123
  }
2069
- return /* @__PURE__ */ jsx19(Fragment5, { children });
2124
+ return /* @__PURE__ */ jsx20(Fragment5, { children });
2070
2125
  }
2071
2126
  var overlayStyle2 = {
2072
2127
  position: "fixed",
@@ -2105,7 +2160,7 @@ var linkStyle = {
2105
2160
  };
2106
2161
 
2107
2162
  // src/AppRoot.tsx
2108
- import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
2163
+ import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
2109
2164
  function buildLegacyConfigShim(config) {
2110
2165
  const paywall = config.paywall;
2111
2166
  const isFree = paywall.mode === "free";
@@ -2182,28 +2237,43 @@ function AppRoot(props) {
2182
2237
  const Router = testRouter === "memory" ? MemoryRouter : BrowserRouter;
2183
2238
  const basename = `/app/${config.slug}`;
2184
2239
  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
- ] }) }) }) }) }) });
2240
+ const position = config.install_prompt?.position ?? "post-paywall";
2241
+ const subscriptionGated = /* @__PURE__ */ jsx21(SubscriptionGate, { Paywall: Paywall ?? FallbackPaywall, children: position === "post-paywall" ? /* @__PURE__ */ jsxs14(InstallGate, { position: "post-paywall", children: [
2242
+ children,
2243
+ /* @__PURE__ */ jsx21(PushPrompt, {})
2244
+ ] }) : /* @__PURE__ */ jsxs14(Fragment6, { children: [
2245
+ children,
2246
+ /* @__PURE__ */ jsx21(PushPrompt, {})
2247
+ ] }) });
2248
+ const authGated = /* @__PURE__ */ jsx21(
2249
+ AuthGated,
2250
+ {
2251
+ config,
2252
+ Login,
2253
+ Signup,
2254
+ Forgot,
2255
+ Reset,
2256
+ EmailVerify,
2257
+ Paywall,
2258
+ Onboarding,
2259
+ PreAuthFlow,
2260
+ children: subscriptionGated
2261
+ }
2262
+ );
2263
+ const routedTree = /* @__PURE__ */ jsxs14(Router, { ...routerProps, children: [
2264
+ /* @__PURE__ */ jsx21(DeepLinkHandler, { deepLinks: config.deepLinks }),
2265
+ /* @__PURE__ */ jsx21(SessionExpiredBanner, {}),
2266
+ position === "pre-auth" ? /* @__PURE__ */ jsx21(InstallGate, { position: "pre-auth", children: authGated }) : authGated
2267
+ ] });
2268
+ 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(
2269
+ I18nProvider,
2270
+ {
2271
+ defaultLocale: config.i18n.defaultLocale,
2272
+ supportedLocales: config.i18n.supportedLocales,
2273
+ resources: config.i18n.resources,
2274
+ children: routedTree
2275
+ }
2276
+ ) : routedTree }) }) }) }) });
2207
2277
  }
2208
2278
  function AuthGated({
2209
2279
  children,
@@ -2220,31 +2290,31 @@ function AuthGated({
2220
2290
  if (authStatus !== "authenticated") {
2221
2291
  if (config.onboarding?.trigger === "pre_signup_custom" && PreAuthFlow) {
2222
2292
  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, {}) })
2293
+ /* @__PURE__ */ jsx21(Route, { path: "/signin", element: /* @__PURE__ */ jsx21(Login, {}) }),
2294
+ /* @__PURE__ */ jsx21(Route, { path: "/signup", element: /* @__PURE__ */ jsx21(Signup, {}) }),
2295
+ /* @__PURE__ */ jsx21(Route, { path: "/forgot", element: /* @__PURE__ */ jsx21(Forgot, {}) }),
2296
+ /* @__PURE__ */ jsx21(Route, { path: "/reset", element: /* @__PURE__ */ jsx21(Reset, {}) }),
2297
+ EmailVerify ? /* @__PURE__ */ jsx21(Route, { path: "/verify", element: /* @__PURE__ */ jsx21(EmailVerify, {}) }) : null,
2298
+ /* @__PURE__ */ jsx21(Route, { path: "/*", element: /* @__PURE__ */ jsx21(PreAuthFlow, {}) })
2229
2299
  ] });
2230
2300
  }
2231
2301
  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 }) })
2302
+ /* @__PURE__ */ jsx21(Route, { path: "/", element: /* @__PURE__ */ jsx21(Login, {}) }),
2303
+ /* @__PURE__ */ jsx21(Route, { path: "/signup", element: /* @__PURE__ */ jsx21(Signup, {}) }),
2304
+ /* @__PURE__ */ jsx21(Route, { path: "/forgot", element: /* @__PURE__ */ jsx21(Forgot, {}) }),
2305
+ /* @__PURE__ */ jsx21(Route, { path: "/reset", element: /* @__PURE__ */ jsx21(Reset, {}) }),
2306
+ EmailVerify ? /* @__PURE__ */ jsx21(Route, { path: "/verify", element: /* @__PURE__ */ jsx21(EmailVerify, {}) }) : null,
2307
+ /* @__PURE__ */ jsx21(Route, { path: "*", element: /* @__PURE__ */ jsx21(Navigate, { to: "/", replace: true }) })
2238
2308
  ] });
2239
2309
  }
2240
- return /* @__PURE__ */ jsx20(Fragment6, { children });
2310
+ return /* @__PURE__ */ jsx21(Fragment6, { children });
2241
2311
  }
2242
2312
  function FallbackPaywall() {
2243
2313
  return null;
2244
2314
  }
2245
2315
 
2246
2316
  // src/hooks/usePush.ts
2247
- import { useCallback as useCallback4, useEffect as useEffect8, useState as useState6 } from "react";
2317
+ import { useCallback as useCallback4, useEffect as useEffect9, useState as useState6 } from "react";
2248
2318
  import { useHook as useHook7 } from "@hook-sdk/sdk";
2249
2319
  var DISMISS_STORAGE_KEY = "push:dismissed-until";
2250
2320
  var DISMISS_TTL_MS2 = 7 * 24 * 60 * 60 * 1e3;
@@ -2291,7 +2361,7 @@ function deriveState(push) {
2291
2361
  function usePush() {
2292
2362
  const { push } = useHook7();
2293
2363
  const [state, setState] = useState6(() => deriveState(push));
2294
- useEffect8(() => {
2364
+ useEffect9(() => {
2295
2365
  setState(deriveState(push));
2296
2366
  }, [push]);
2297
2367
  const subscribe = useCallback4(async () => {
@@ -2328,7 +2398,7 @@ function usePush() {
2328
2398
  }
2329
2399
 
2330
2400
  // src/components/PushPrompt.tsx
2331
- import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
2401
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
2332
2402
  function platformRecoveryCopy(texts) {
2333
2403
  if (typeof navigator === "undefined") return null;
2334
2404
  const ua = navigator.userAgent || "";
@@ -2352,27 +2422,27 @@ function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, clas
2352
2422
  if (state.kind === "subscribed" || state.kind === "dismissed") return null;
2353
2423
  if (state.kind === "ios_needs_install") {
2354
2424
  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 })
2425
+ /* @__PURE__ */ jsx22("h3", { children: texts.iosInstallTitle }),
2426
+ /* @__PURE__ */ jsx22("p", { children: texts.iosInstallBody }),
2427
+ onInstallRequested && texts.iosInstallCta && /* @__PURE__ */ jsx22("button", { onClick: onInstallRequested, children: texts.iosInstallCta })
2358
2428
  ] });
2359
2429
  }
2360
2430
  if (state.kind === "denied") {
2361
2431
  const recovery = platformRecoveryCopy(texts);
2362
2432
  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 })
2433
+ /* @__PURE__ */ jsx22("h3", { children: texts.deniedTitle }),
2434
+ /* @__PURE__ */ jsx22("p", { children: texts.deniedBody }),
2435
+ recovery && /* @__PURE__ */ jsx22("p", { "data-testid": "denied-recovery", children: recovery })
2366
2436
  ] });
2367
2437
  }
2368
2438
  if (state.kind === "unsupported") {
2369
- return /* @__PURE__ */ jsx21("div", { className, role: "region", children: /* @__PURE__ */ jsx21("p", { children: texts.unsupportedBody }) });
2439
+ return /* @__PURE__ */ jsx22("div", { className, role: "region", children: /* @__PURE__ */ jsx22("p", { children: texts.unsupportedBody }) });
2370
2440
  }
2371
2441
  if (state.kind === "error") {
2372
- return /* @__PURE__ */ jsx21("div", { className, role: "region", "aria-label": "error", children: /* @__PURE__ */ jsx21("p", { children: state.message }) });
2442
+ return /* @__PURE__ */ jsx22("div", { className, role: "region", "aria-label": "error", children: /* @__PURE__ */ jsx22("p", { children: state.message }) });
2373
2443
  }
2374
2444
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", children: [
2375
- /* @__PURE__ */ jsx21(
2445
+ /* @__PURE__ */ jsx22(
2376
2446
  "button",
2377
2447
  {
2378
2448
  type: "button",
@@ -2386,23 +2456,49 @@ function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, clas
2386
2456
  children: texts.cta
2387
2457
  }
2388
2458
  ),
2389
- onDeclined && /* @__PURE__ */ jsx21("button", { type: "button", onClick: onDeclined, children: texts.declineCta })
2459
+ onDeclined && /* @__PURE__ */ jsx22("button", { type: "button", onClick: onDeclined, children: texts.declineCta })
2460
+ ] });
2461
+ }
2462
+
2463
+ // src/components/LanguageSwitcher.tsx
2464
+ import { usePersistedState as usePersistedState2 } from "@hook-sdk/sdk";
2465
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
2466
+ function LanguageSwitcher({ id, className, label = "Language" }) {
2467
+ const config = useAppConfig();
2468
+ const i18nConfig = config.i18n;
2469
+ const [userLocale, setUserLocale] = usePersistedState2(
2470
+ "user-locale",
2471
+ i18nConfig?.defaultLocale ?? "en-US"
2472
+ );
2473
+ if (!i18nConfig) return null;
2474
+ return /* @__PURE__ */ jsxs16("label", { className, children: [
2475
+ label ? /* @__PURE__ */ jsx23("span", { children: label }) : null,
2476
+ /* @__PURE__ */ jsx23(
2477
+ "select",
2478
+ {
2479
+ id,
2480
+ value: userLocale,
2481
+ onChange: (e) => setUserLocale(e.target.value),
2482
+ "data-testid": "language-switcher",
2483
+ children: i18nConfig.supportedLocales.map((loc) => /* @__PURE__ */ jsx23("option", { value: loc, children: loc }, loc))
2484
+ }
2485
+ )
2390
2486
  ] });
2391
2487
  }
2392
2488
 
2393
2489
  // src/defaults/LoadingState.tsx
2394
- import { jsx as jsx22 } from "react/jsx-runtime";
2490
+ import { jsx as jsx24 } from "react/jsx-runtime";
2395
2491
  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..." }) });
2492
+ return /* @__PURE__ */ jsx24("div", { role: "status", "aria-live": "polite", style: { padding: 24, textAlign: "center" }, children: /* @__PURE__ */ jsx24("span", { children: message ?? "Carregando..." }) });
2397
2493
  }
2398
2494
 
2399
2495
  // src/defaults/EmptyState.tsx
2400
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
2496
+ import { jsx as jsx25, jsxs as jsxs17 } from "react/jsx-runtime";
2401
2497
  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 })
2498
+ return /* @__PURE__ */ jsxs17("div", { role: "status", style: { padding: 32, textAlign: "center" }, children: [
2499
+ /* @__PURE__ */ jsx25("h2", { style: { marginBottom: 8 }, children: title }),
2500
+ description && /* @__PURE__ */ jsx25("p", { style: { opacity: 0.7 }, children: description }),
2501
+ action && /* @__PURE__ */ jsx25("div", { style: { marginTop: 16 }, children: action })
2406
2502
  ] });
2407
2503
  }
2408
2504
 
@@ -2619,7 +2715,7 @@ function useForgotForm() {
2619
2715
  }
2620
2716
 
2621
2717
  // src/hooks/useResetForm.ts
2622
- import { useCallback as useCallback8, useEffect as useEffect9, useMemo as useMemo7, useState as useState10 } from "react";
2718
+ import { useCallback as useCallback8, useEffect as useEffect10, useMemo as useMemo7, useState as useState10 } from "react";
2623
2719
  import { useHook as useHook11 } from "@hook-sdk/sdk";
2624
2720
  var MIN_PASSWORD3 = 12;
2625
2721
  function useResetForm() {
@@ -2633,7 +2729,7 @@ function useResetForm() {
2633
2729
  const [touchedPassword, setTouchedPassword] = useState10(false);
2634
2730
  const [touchedConfirm, setTouchedConfirm] = useState10(false);
2635
2731
  const [formSubmitAttempted, setFormSubmitAttempted] = useState10(false);
2636
- useEffect9(() => {
2732
+ useEffect10(() => {
2637
2733
  if (typeof window === "undefined") return;
2638
2734
  const params = new URLSearchParams(window.location.search);
2639
2735
  const t = params.get("token");
@@ -2727,12 +2823,12 @@ function discountPercent(anchorCents, realCents) {
2727
2823
  }
2728
2824
 
2729
2825
  // src/hooks/useAuthPrimitives.ts
2730
- import { useEffect as useEffect10 } from "react";
2826
+ import { useEffect as useEffect11 } from "react";
2731
2827
  import { useHook as useHook13 } from "@hook-sdk/sdk";
2732
2828
  var warned = false;
2733
2829
  function useAuthPrimitives() {
2734
2830
  const { auth } = useHook13();
2735
- useEffect10(() => {
2831
+ useEffect11(() => {
2736
2832
  if (!warned && process.env.NODE_ENV !== "production") {
2737
2833
  warned = true;
2738
2834
  console.warn(
@@ -2777,7 +2873,7 @@ function useSubscription() {
2777
2873
  }
2778
2874
 
2779
2875
  // src/hooks/useReminders.ts
2780
- import { useCallback as useCallback9, useEffect as useEffect11, useState as useState11 } from "react";
2876
+ import { useCallback as useCallback9, useEffect as useEffect12, useState as useState11 } from "react";
2781
2877
  import { useHook as useHook16 } from "@hook-sdk/sdk";
2782
2878
  function useReminders() {
2783
2879
  const { push } = useHook16();
@@ -2793,7 +2889,7 @@ function useReminders() {
2793
2889
  setLoading(false);
2794
2890
  }
2795
2891
  }, [r]);
2796
- useEffect11(() => {
2892
+ useEffect12(() => {
2797
2893
  void reload();
2798
2894
  }, [reload]);
2799
2895
  const setReminder = useCallback9(async (input) => {
@@ -2832,20 +2928,20 @@ function useToast() {
2832
2928
 
2833
2929
  // src/RouteBoundary.tsx
2834
2930
  import { Routes as Routes2, Route as Route2 } from "react-router-dom";
2835
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
2931
+ import { jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
2836
2932
  function RouteBoundary({ children }) {
2837
- return /* @__PURE__ */ jsxs17(Routes2, { children: [
2933
+ return /* @__PURE__ */ jsxs18(Routes2, { children: [
2838
2934
  children,
2839
- /* @__PURE__ */ jsx24(Route2, { path: "*", element: /* @__PURE__ */ jsx24(DefaultNotFound, {}) })
2935
+ /* @__PURE__ */ jsx26(Route2, { path: "*", element: /* @__PURE__ */ jsx26(DefaultNotFound, {}) })
2840
2936
  ] });
2841
2937
  }
2842
2938
  function DefaultNotFound() {
2843
- return /* @__PURE__ */ jsx24("div", { role: "alert", children: "P\xE1gina n\xE3o encontrada" });
2939
+ return /* @__PURE__ */ jsx26("div", { role: "alert", children: "P\xE1gina n\xE3o encontrada" });
2844
2940
  }
2845
2941
 
2846
2942
  // src/PreAuthShell.tsx
2847
2943
  import { BrowserRouter as BrowserRouter2, MemoryRouter as MemoryRouter2, Routes as Routes3 } from "react-router-dom";
2848
- import { jsx as jsx25 } from "react/jsx-runtime";
2944
+ import { jsx as jsx27 } from "react/jsx-runtime";
2849
2945
  function PreAuthShell({
2850
2946
  basename,
2851
2947
  testRouter,
@@ -2853,14 +2949,14 @@ function PreAuthShell({
2853
2949
  children
2854
2950
  }) {
2855
2951
  if (testRouter === "memory") {
2856
- return /* @__PURE__ */ jsx25(MemoryRouter2, { basename, initialEntries: testInitialEntries, children: /* @__PURE__ */ jsx25(Routes3, { children }) });
2952
+ return /* @__PURE__ */ jsx27(MemoryRouter2, { basename, initialEntries: testInitialEntries, children: /* @__PURE__ */ jsx27(Routes3, { children }) });
2857
2953
  }
2858
- return /* @__PURE__ */ jsx25(BrowserRouter2, { basename, children: /* @__PURE__ */ jsx25(Routes3, { children }) });
2954
+ return /* @__PURE__ */ jsx27(BrowserRouter2, { basename, children: /* @__PURE__ */ jsx27(Routes3, { children }) });
2859
2955
  }
2860
2956
 
2861
2957
  // 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";
2958
+ import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef5 } from "react";
2959
+ import { usePersistedState as usePersistedState3, useHook as useHook17 } from "@hook-sdk/sdk";
2864
2960
 
2865
2961
  // src/hooks/useOnboardingStep.ts
2866
2962
  import { createContext as createContext3, useContext as useContext4 } from "react";
@@ -2876,7 +2972,7 @@ function useOnboardingStep() {
2876
2972
  }
2877
2973
 
2878
2974
  // src/OnboardingFlow.tsx
2879
- import { jsx as jsx26 } from "react/jsx-runtime";
2975
+ import { jsx as jsx28 } from "react/jsx-runtime";
2880
2976
  var isFilled = (v) => v != null && v !== "";
2881
2977
  var CURRENT_STEP_FIELD = "currentStep";
2882
2978
  function readPersistedStepIdx(draft) {
@@ -2889,7 +2985,7 @@ function OnboardingFlow({
2889
2985
  onComplete,
2890
2986
  persistKey
2891
2987
  }) {
2892
- const [draft, setDraft, status] = usePersistedState(persistKey, {});
2988
+ const [draft, setDraft, status] = usePersistedState3(persistKey, {});
2893
2989
  const draftRef = useRef5(draft);
2894
2990
  draftRef.current = draft;
2895
2991
  const idx = readPersistedStepIdx(draft);
@@ -2914,7 +3010,7 @@ function OnboardingFlow({
2914
3010
  const step = steps[clampedIdx];
2915
3011
  const hookCtx = useHook17();
2916
3012
  const track2 = typeof hookCtx.track === "function" ? hookCtx.track : void 0;
2917
- useEffect12(() => {
3013
+ useEffect13(() => {
2918
3014
  if (status.loading) return;
2919
3015
  if (!step) return;
2920
3016
  if (!track2) return;
@@ -2966,7 +3062,7 @@ function OnboardingFlow({
2966
3062
  `[hook-template] OnboardingFlow: missing screen component for step '${step.id}' (expected key '${step.screen}' in screens prop)`
2967
3063
  );
2968
3064
  }
2969
- return /* @__PURE__ */ jsx26(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx26(Screen, {}) });
3065
+ return /* @__PURE__ */ jsx28(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx28(Screen, {}) });
2970
3066
  }
2971
3067
 
2972
3068
  // src/hooks/useFeature.ts
@@ -2981,8 +3077,10 @@ export {
2981
3077
  DeepLinkHandler,
2982
3078
  EmptyState,
2983
3079
  ErrorBoundary,
3080
+ I18nProvider,
2984
3081
  InstallGate,
2985
3082
  InstallSplash,
3083
+ LanguageSwitcher,
2986
3084
  LoadingState,
2987
3085
  OnboardingFlow,
2988
3086
  PaymentReturnHandler,