@hook-sdk/template 0.18.0 → 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);
@@ -196,7 +213,16 @@ var MAP = {
196
213
  pix_expired: "QR Code do PIX expirou. Gere um novo.",
197
214
  pix_not_paid_yet: "PIX ainda n\xE3o foi pago. Aguardando confirma\xE7\xE3o."
198
215
  };
199
- function asaasErrorMessage(code) {
216
+ function asaasErrorMessage(code, description) {
217
+ if (description) {
218
+ const lower = description.toLowerCase();
219
+ if (lower.includes("cep")) {
220
+ return "Nosso processador de pagamentos n\xE3o reconheceu esse CEP \u2014 a base deles pode estar desatualizada. Tente outro CEP.";
221
+ }
222
+ if (lower.includes("telefone") || lower.includes("contato com ddd")) {
223
+ return "Telefone inv\xE1lido. Confira o n\xFAmero com DDD e tente novamente.";
224
+ }
225
+ }
200
226
  return MAP[code] ?? "Ocorreu um erro inesperado. Tente novamente em instantes.";
201
227
  }
202
228
 
@@ -325,7 +351,9 @@ function usePaywallState() {
325
351
  (code, fallbackMessage) => ({
326
352
  code,
327
353
  message: fallbackMessage,
328
- userMessage: useDefaultMessages ? asaasErrorMessage(code) : fallbackMessage
354
+ // fallbackMessage carries Asaas's PT-BR description ("O CEP informado é inválido.")
355
+ // that distinguishes invalid_holderInfo sub-cases (CEP vs phone vs name).
356
+ userMessage: useDefaultMessages ? asaasErrorMessage(code, fallbackMessage) : fallbackMessage
329
357
  }),
330
358
  [useDefaultMessages]
331
359
  );
@@ -1800,9 +1828,9 @@ var bannerStyle = {
1800
1828
 
1801
1829
  // src/components/InstallGate/InstallGate.tsx
1802
1830
  import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1803
- function InstallGate({ children }) {
1831
+ function InstallGate({ children, position }) {
1804
1832
  const { slug, features_enabled } = useTemplateConfig();
1805
- const enabled = features_enabled.includes("install_prompt");
1833
+ const enabled = features_enabled.includes("pwa-install");
1806
1834
  const installState = useInstallPrompt(slug);
1807
1835
  const shouldBlock = enabled && shouldBlockInstall(installState);
1808
1836
  const trackedRef = useRef2(null);
@@ -1817,9 +1845,10 @@ function InstallGate({ children }) {
1817
1845
  platform: installState.platform,
1818
1846
  browser: installState.iosBrowser ?? installState.androidBrowser ?? null,
1819
1847
  in_app_app: installState.inAppApp,
1820
- variant: installState.variant
1848
+ variant: installState.variant,
1849
+ ...position !== void 0 ? { position } : {}
1821
1850
  });
1822
- }, [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]);
1823
1852
  if (!enabled) return /* @__PURE__ */ jsx16(Fragment3, { children });
1824
1853
  if (installState.isInstalled) return /* @__PURE__ */ jsx16(Fragment3, { children });
1825
1854
  if (installState.variant === "desktop") {
@@ -1942,10 +1971,47 @@ var ErrorBoundary = class extends Component {
1942
1971
  }
1943
1972
  };
1944
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
+
1945
2011
  // src/internal/PaymentReturnHandler.tsx
1946
- 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";
1947
2013
  import { useHook as useHook5 } from "@hook-sdk/sdk";
1948
- 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";
1949
2015
  var BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 4e4];
1950
2016
  var MAX_CYCLES = 3;
1951
2017
  var SUPPORT_MAILTO = "mailto:suporte@usehook.net?subject=Pagamento%20pendente";
@@ -1991,7 +2057,7 @@ function PaymentReturnHandler({ children }) {
1991
2057
  };
1992
2058
  void tick();
1993
2059
  }, []);
1994
- useEffect7(() => {
2060
+ useEffect8(() => {
1995
2061
  if (typeof window === "undefined") return;
1996
2062
  const url = new URL(window.location.href);
1997
2063
  if (url.searchParams.get("paymentReturn") !== "1") return;
@@ -2008,19 +2074,19 @@ function PaymentReturnHandler({ children }) {
2008
2074
  window.location.href = cleanUrl.toString();
2009
2075
  }, []);
2010
2076
  if (state === "confirming") {
2011
- 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" });
2012
2078
  }
2013
2079
  if (state === "waiting") {
2014
- return /* @__PURE__ */ jsx19("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 320, textAlign: "center", lineHeight: 1.5 }, children: [
2015
- /* @__PURE__ */ jsx19("div", { style: { marginBottom: 16 }, children: "Pagamento aceito. Estamos confirmando com o banco \u2014 pode levar alguns minutos." }),
2016
- /* @__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" })
2017
2083
  ] }) });
2018
2084
  }
2019
2085
  if (state === "timeout") {
2020
- return /* @__PURE__ */ jsx19("div", { role: "alert", "aria-live": "assertive", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 360, textAlign: "center", lineHeight: 1.5 }, children: [
2021
- /* @__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." }),
2022
2088
  /* @__PURE__ */ jsxs13("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
2023
- /* @__PURE__ */ jsx19(
2089
+ /* @__PURE__ */ jsx20(
2024
2090
  "button",
2025
2091
  {
2026
2092
  type: "button",
@@ -2033,7 +2099,7 @@ function PaymentReturnHandler({ children }) {
2033
2099
  children: "Tentar de novo"
2034
2100
  }
2035
2101
  ),
2036
- /* @__PURE__ */ jsx19(
2102
+ /* @__PURE__ */ jsx20(
2037
2103
  "button",
2038
2104
  {
2039
2105
  type: "button",
@@ -2043,7 +2109,7 @@ function PaymentReturnHandler({ children }) {
2043
2109
  children: "Voltar pro app"
2044
2110
  }
2045
2111
  ),
2046
- /* @__PURE__ */ jsx19(
2112
+ /* @__PURE__ */ jsx20(
2047
2113
  "a",
2048
2114
  {
2049
2115
  href: SUPPORT_MAILTO,
@@ -2055,7 +2121,7 @@ function PaymentReturnHandler({ children }) {
2055
2121
  ] })
2056
2122
  ] }) });
2057
2123
  }
2058
- return /* @__PURE__ */ jsx19(Fragment5, { children });
2124
+ return /* @__PURE__ */ jsx20(Fragment5, { children });
2059
2125
  }
2060
2126
  var overlayStyle2 = {
2061
2127
  position: "fixed",
@@ -2094,7 +2160,7 @@ var linkStyle = {
2094
2160
  };
2095
2161
 
2096
2162
  // src/AppRoot.tsx
2097
- 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";
2098
2164
  function buildLegacyConfigShim(config) {
2099
2165
  const paywall = config.paywall;
2100
2166
  const isFree = paywall.mode === "free";
@@ -2171,28 +2237,43 @@ function AppRoot(props) {
2171
2237
  const Router = testRouter === "memory" ? MemoryRouter : BrowserRouter;
2172
2238
  const basename = `/app/${config.slug}`;
2173
2239
  const routerProps = testRouter === "memory" ? { basename, initialEntries: testInitialEntries } : { basename };
2174
- 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: [
2175
- /* @__PURE__ */ jsx20(DeepLinkHandler, { deepLinks: config.deepLinks }),
2176
- /* @__PURE__ */ jsx20(SessionExpiredBanner, {}),
2177
- /* @__PURE__ */ jsx20(InstallGate, { children: /* @__PURE__ */ jsx20(
2178
- AuthGated,
2179
- {
2180
- config,
2181
- Login,
2182
- Signup,
2183
- Forgot,
2184
- Reset,
2185
- EmailVerify,
2186
- Paywall,
2187
- Onboarding,
2188
- PreAuthFlow,
2189
- children: /* @__PURE__ */ jsxs14(SubscriptionGate, { Paywall: Paywall ?? FallbackPaywall, children: [
2190
- children,
2191
- /* @__PURE__ */ jsx20(PushPrompt, {})
2192
- ] })
2193
- }
2194
- ) })
2195
- ] }) }) }) }) }) });
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 }) }) }) }) });
2196
2277
  }
2197
2278
  function AuthGated({
2198
2279
  children,
@@ -2209,31 +2290,31 @@ function AuthGated({
2209
2290
  if (authStatus !== "authenticated") {
2210
2291
  if (config.onboarding?.trigger === "pre_signup_custom" && PreAuthFlow) {
2211
2292
  return /* @__PURE__ */ jsxs14(Routes, { children: [
2212
- /* @__PURE__ */ jsx20(Route, { path: "/signin", element: /* @__PURE__ */ jsx20(Login, {}) }),
2213
- /* @__PURE__ */ jsx20(Route, { path: "/signup", element: /* @__PURE__ */ jsx20(Signup, {}) }),
2214
- /* @__PURE__ */ jsx20(Route, { path: "/forgot", element: /* @__PURE__ */ jsx20(Forgot, {}) }),
2215
- /* @__PURE__ */ jsx20(Route, { path: "/reset", element: /* @__PURE__ */ jsx20(Reset, {}) }),
2216
- EmailVerify ? /* @__PURE__ */ jsx20(Route, { path: "/verify", element: /* @__PURE__ */ jsx20(EmailVerify, {}) }) : null,
2217
- /* @__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, {}) })
2218
2299
  ] });
2219
2300
  }
2220
2301
  return /* @__PURE__ */ jsxs14(Routes, { children: [
2221
- /* @__PURE__ */ jsx20(Route, { path: "/", element: /* @__PURE__ */ jsx20(Login, {}) }),
2222
- /* @__PURE__ */ jsx20(Route, { path: "/signup", element: /* @__PURE__ */ jsx20(Signup, {}) }),
2223
- /* @__PURE__ */ jsx20(Route, { path: "/forgot", element: /* @__PURE__ */ jsx20(Forgot, {}) }),
2224
- /* @__PURE__ */ jsx20(Route, { path: "/reset", element: /* @__PURE__ */ jsx20(Reset, {}) }),
2225
- EmailVerify ? /* @__PURE__ */ jsx20(Route, { path: "/verify", element: /* @__PURE__ */ jsx20(EmailVerify, {}) }) : null,
2226
- /* @__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 }) })
2227
2308
  ] });
2228
2309
  }
2229
- return /* @__PURE__ */ jsx20(Fragment6, { children });
2310
+ return /* @__PURE__ */ jsx21(Fragment6, { children });
2230
2311
  }
2231
2312
  function FallbackPaywall() {
2232
2313
  return null;
2233
2314
  }
2234
2315
 
2235
2316
  // src/hooks/usePush.ts
2236
- 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";
2237
2318
  import { useHook as useHook7 } from "@hook-sdk/sdk";
2238
2319
  var DISMISS_STORAGE_KEY = "push:dismissed-until";
2239
2320
  var DISMISS_TTL_MS2 = 7 * 24 * 60 * 60 * 1e3;
@@ -2280,7 +2361,7 @@ function deriveState(push) {
2280
2361
  function usePush() {
2281
2362
  const { push } = useHook7();
2282
2363
  const [state, setState] = useState6(() => deriveState(push));
2283
- useEffect8(() => {
2364
+ useEffect9(() => {
2284
2365
  setState(deriveState(push));
2285
2366
  }, [push]);
2286
2367
  const subscribe = useCallback4(async () => {
@@ -2317,7 +2398,7 @@ function usePush() {
2317
2398
  }
2318
2399
 
2319
2400
  // src/components/PushPrompt.tsx
2320
- import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
2401
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
2321
2402
  function platformRecoveryCopy(texts) {
2322
2403
  if (typeof navigator === "undefined") return null;
2323
2404
  const ua = navigator.userAgent || "";
@@ -2341,27 +2422,27 @@ function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, clas
2341
2422
  if (state.kind === "subscribed" || state.kind === "dismissed") return null;
2342
2423
  if (state.kind === "ios_needs_install") {
2343
2424
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", "aria-label": texts.iosInstallTitle, children: [
2344
- /* @__PURE__ */ jsx21("h3", { children: texts.iosInstallTitle }),
2345
- /* @__PURE__ */ jsx21("p", { children: texts.iosInstallBody }),
2346
- 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 })
2347
2428
  ] });
2348
2429
  }
2349
2430
  if (state.kind === "denied") {
2350
2431
  const recovery = platformRecoveryCopy(texts);
2351
2432
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", "aria-label": texts.deniedTitle, children: [
2352
- /* @__PURE__ */ jsx21("h3", { children: texts.deniedTitle }),
2353
- /* @__PURE__ */ jsx21("p", { children: texts.deniedBody }),
2354
- 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 })
2355
2436
  ] });
2356
2437
  }
2357
2438
  if (state.kind === "unsupported") {
2358
- 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 }) });
2359
2440
  }
2360
2441
  if (state.kind === "error") {
2361
- 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 }) });
2362
2443
  }
2363
2444
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", children: [
2364
- /* @__PURE__ */ jsx21(
2445
+ /* @__PURE__ */ jsx22(
2365
2446
  "button",
2366
2447
  {
2367
2448
  type: "button",
@@ -2375,23 +2456,49 @@ function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, clas
2375
2456
  children: texts.cta
2376
2457
  }
2377
2458
  ),
2378
- 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
+ )
2379
2486
  ] });
2380
2487
  }
2381
2488
 
2382
2489
  // src/defaults/LoadingState.tsx
2383
- import { jsx as jsx22 } from "react/jsx-runtime";
2490
+ import { jsx as jsx24 } from "react/jsx-runtime";
2384
2491
  function LoadingState({ message }) {
2385
- 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..." }) });
2386
2493
  }
2387
2494
 
2388
2495
  // src/defaults/EmptyState.tsx
2389
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
2496
+ import { jsx as jsx25, jsxs as jsxs17 } from "react/jsx-runtime";
2390
2497
  function EmptyState({ title, description, action }) {
2391
- return /* @__PURE__ */ jsxs16("div", { role: "status", style: { padding: 32, textAlign: "center" }, children: [
2392
- /* @__PURE__ */ jsx23("h2", { style: { marginBottom: 8 }, children: title }),
2393
- description && /* @__PURE__ */ jsx23("p", { style: { opacity: 0.7 }, children: description }),
2394
- 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 })
2395
2502
  ] });
2396
2503
  }
2397
2504
 
@@ -2608,7 +2715,7 @@ function useForgotForm() {
2608
2715
  }
2609
2716
 
2610
2717
  // src/hooks/useResetForm.ts
2611
- 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";
2612
2719
  import { useHook as useHook11 } from "@hook-sdk/sdk";
2613
2720
  var MIN_PASSWORD3 = 12;
2614
2721
  function useResetForm() {
@@ -2622,7 +2729,7 @@ function useResetForm() {
2622
2729
  const [touchedPassword, setTouchedPassword] = useState10(false);
2623
2730
  const [touchedConfirm, setTouchedConfirm] = useState10(false);
2624
2731
  const [formSubmitAttempted, setFormSubmitAttempted] = useState10(false);
2625
- useEffect9(() => {
2732
+ useEffect10(() => {
2626
2733
  if (typeof window === "undefined") return;
2627
2734
  const params = new URLSearchParams(window.location.search);
2628
2735
  const t = params.get("token");
@@ -2716,12 +2823,12 @@ function discountPercent(anchorCents, realCents) {
2716
2823
  }
2717
2824
 
2718
2825
  // src/hooks/useAuthPrimitives.ts
2719
- import { useEffect as useEffect10 } from "react";
2826
+ import { useEffect as useEffect11 } from "react";
2720
2827
  import { useHook as useHook13 } from "@hook-sdk/sdk";
2721
2828
  var warned = false;
2722
2829
  function useAuthPrimitives() {
2723
2830
  const { auth } = useHook13();
2724
- useEffect10(() => {
2831
+ useEffect11(() => {
2725
2832
  if (!warned && process.env.NODE_ENV !== "production") {
2726
2833
  warned = true;
2727
2834
  console.warn(
@@ -2766,7 +2873,7 @@ function useSubscription() {
2766
2873
  }
2767
2874
 
2768
2875
  // src/hooks/useReminders.ts
2769
- 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";
2770
2877
  import { useHook as useHook16 } from "@hook-sdk/sdk";
2771
2878
  function useReminders() {
2772
2879
  const { push } = useHook16();
@@ -2782,7 +2889,7 @@ function useReminders() {
2782
2889
  setLoading(false);
2783
2890
  }
2784
2891
  }, [r]);
2785
- useEffect11(() => {
2892
+ useEffect12(() => {
2786
2893
  void reload();
2787
2894
  }, [reload]);
2788
2895
  const setReminder = useCallback9(async (input) => {
@@ -2821,20 +2928,20 @@ function useToast() {
2821
2928
 
2822
2929
  // src/RouteBoundary.tsx
2823
2930
  import { Routes as Routes2, Route as Route2 } from "react-router-dom";
2824
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
2931
+ import { jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
2825
2932
  function RouteBoundary({ children }) {
2826
- return /* @__PURE__ */ jsxs17(Routes2, { children: [
2933
+ return /* @__PURE__ */ jsxs18(Routes2, { children: [
2827
2934
  children,
2828
- /* @__PURE__ */ jsx24(Route2, { path: "*", element: /* @__PURE__ */ jsx24(DefaultNotFound, {}) })
2935
+ /* @__PURE__ */ jsx26(Route2, { path: "*", element: /* @__PURE__ */ jsx26(DefaultNotFound, {}) })
2829
2936
  ] });
2830
2937
  }
2831
2938
  function DefaultNotFound() {
2832
- 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" });
2833
2940
  }
2834
2941
 
2835
2942
  // src/PreAuthShell.tsx
2836
2943
  import { BrowserRouter as BrowserRouter2, MemoryRouter as MemoryRouter2, Routes as Routes3 } from "react-router-dom";
2837
- import { jsx as jsx25 } from "react/jsx-runtime";
2944
+ import { jsx as jsx27 } from "react/jsx-runtime";
2838
2945
  function PreAuthShell({
2839
2946
  basename,
2840
2947
  testRouter,
@@ -2842,14 +2949,14 @@ function PreAuthShell({
2842
2949
  children
2843
2950
  }) {
2844
2951
  if (testRouter === "memory") {
2845
- 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 }) });
2846
2953
  }
2847
- return /* @__PURE__ */ jsx25(BrowserRouter2, { basename, children: /* @__PURE__ */ jsx25(Routes3, { children }) });
2954
+ return /* @__PURE__ */ jsx27(BrowserRouter2, { basename, children: /* @__PURE__ */ jsx27(Routes3, { children }) });
2848
2955
  }
2849
2956
 
2850
2957
  // src/OnboardingFlow.tsx
2851
- import { useCallback as useCallback11, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef5 } from "react";
2852
- 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";
2853
2960
 
2854
2961
  // src/hooks/useOnboardingStep.ts
2855
2962
  import { createContext as createContext3, useContext as useContext4 } from "react";
@@ -2865,7 +2972,7 @@ function useOnboardingStep() {
2865
2972
  }
2866
2973
 
2867
2974
  // src/OnboardingFlow.tsx
2868
- import { jsx as jsx26 } from "react/jsx-runtime";
2975
+ import { jsx as jsx28 } from "react/jsx-runtime";
2869
2976
  var isFilled = (v) => v != null && v !== "";
2870
2977
  var CURRENT_STEP_FIELD = "currentStep";
2871
2978
  function readPersistedStepIdx(draft) {
@@ -2878,7 +2985,7 @@ function OnboardingFlow({
2878
2985
  onComplete,
2879
2986
  persistKey
2880
2987
  }) {
2881
- const [draft, setDraft, status] = usePersistedState(persistKey, {});
2988
+ const [draft, setDraft, status] = usePersistedState3(persistKey, {});
2882
2989
  const draftRef = useRef5(draft);
2883
2990
  draftRef.current = draft;
2884
2991
  const idx = readPersistedStepIdx(draft);
@@ -2903,7 +3010,7 @@ function OnboardingFlow({
2903
3010
  const step = steps[clampedIdx];
2904
3011
  const hookCtx = useHook17();
2905
3012
  const track2 = typeof hookCtx.track === "function" ? hookCtx.track : void 0;
2906
- useEffect12(() => {
3013
+ useEffect13(() => {
2907
3014
  if (status.loading) return;
2908
3015
  if (!step) return;
2909
3016
  if (!track2) return;
@@ -2955,7 +3062,7 @@ function OnboardingFlow({
2955
3062
  `[hook-template] OnboardingFlow: missing screen component for step '${step.id}' (expected key '${step.screen}' in screens prop)`
2956
3063
  );
2957
3064
  }
2958
- return /* @__PURE__ */ jsx26(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx26(Screen, {}) });
3065
+ return /* @__PURE__ */ jsx28(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx28(Screen, {}) });
2959
3066
  }
2960
3067
 
2961
3068
  // src/hooks/useFeature.ts
@@ -2970,8 +3077,10 @@ export {
2970
3077
  DeepLinkHandler,
2971
3078
  EmptyState,
2972
3079
  ErrorBoundary,
3080
+ I18nProvider,
2973
3081
  InstallGate,
2974
3082
  InstallSplash,
3083
+ LanguageSwitcher,
2975
3084
  LoadingState,
2976
3085
  OnboardingFlow,
2977
3086
  PaymentReturnHandler,