@hook-sdk/template 0.13.0 → 0.14.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.d.cts CHANGED
@@ -200,9 +200,15 @@ interface UseLoginFormResult {
200
200
  email: string;
201
201
  setEmail: (v: string) => void;
202
202
  emailError: string | null;
203
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
204
+ markEmailTouched: () => void;
203
205
  password: string;
204
206
  setPassword: (v: string) => void;
205
207
  passwordError: string | null;
208
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
209
+ markPasswordTouched: () => void;
210
+ /** True after the user has attempted submit at least once. */
211
+ formSubmitAttempted: boolean;
206
212
  /**
207
213
  * Submete o form. Retorna true se o login deu OK (cookies setados), false
208
214
  * se validação falhou, credenciais inválidas, rate-limit ou erro de rede.
@@ -225,12 +231,20 @@ interface UseSignupFormResult {
225
231
  name: string;
226
232
  setName: (v: string) => void;
227
233
  nameError: string | null;
234
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
235
+ markNameTouched: () => void;
228
236
  email: string;
229
237
  setEmail: (v: string) => void;
230
238
  emailError: string | null;
239
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
240
+ markEmailTouched: () => void;
231
241
  password: string;
232
242
  setPassword: (v: string) => void;
233
243
  passwordError: string | null;
244
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
245
+ markPasswordTouched: () => void;
246
+ /** True after the user has attempted submit at least once. */
247
+ formSubmitAttempted: boolean;
234
248
  /**
235
249
  * Submete o form. Retorna true se o signup deu OK (backend respondeu 2xx),
236
250
  * false se validação falhou, houve erro de rede/servidor, ou email já em uso.
@@ -254,6 +268,10 @@ interface UseForgotFormResult {
254
268
  email: string;
255
269
  setEmail: (v: string) => void;
256
270
  emailError: string | null;
271
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
272
+ markEmailTouched: () => void;
273
+ /** True after the user has attempted submit at least once. */
274
+ formSubmitAttempted: boolean;
257
275
  /**
258
276
  * Submete o form. Retorna true se o backend aceitou a requisição (email
259
277
  * de reset foi enfileirado — sem leak de "existe esse email" por design),
@@ -274,9 +292,15 @@ interface UseResetFormResult {
274
292
  password: string;
275
293
  setPassword: (v: string) => void;
276
294
  passwordError: string | null;
295
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
296
+ markPasswordTouched: () => void;
277
297
  confirm: string;
278
298
  setConfirm: (v: string) => void;
279
299
  confirmError: string | null;
300
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
301
+ markConfirmTouched: () => void;
302
+ /** True after the user has attempted submit at least once. */
303
+ formSubmitAttempted: boolean;
280
304
  submit: () => Promise<void>;
281
305
  submitting: boolean;
282
306
  canSubmit: boolean;
package/dist/index.d.ts CHANGED
@@ -200,9 +200,15 @@ interface UseLoginFormResult {
200
200
  email: string;
201
201
  setEmail: (v: string) => void;
202
202
  emailError: string | null;
203
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
204
+ markEmailTouched: () => void;
203
205
  password: string;
204
206
  setPassword: (v: string) => void;
205
207
  passwordError: string | null;
208
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
209
+ markPasswordTouched: () => void;
210
+ /** True after the user has attempted submit at least once. */
211
+ formSubmitAttempted: boolean;
206
212
  /**
207
213
  * Submete o form. Retorna true se o login deu OK (cookies setados), false
208
214
  * se validação falhou, credenciais inválidas, rate-limit ou erro de rede.
@@ -225,12 +231,20 @@ interface UseSignupFormResult {
225
231
  name: string;
226
232
  setName: (v: string) => void;
227
233
  nameError: string | null;
234
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
235
+ markNameTouched: () => void;
228
236
  email: string;
229
237
  setEmail: (v: string) => void;
230
238
  emailError: string | null;
239
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
240
+ markEmailTouched: () => void;
231
241
  password: string;
232
242
  setPassword: (v: string) => void;
233
243
  passwordError: string | null;
244
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
245
+ markPasswordTouched: () => void;
246
+ /** True after the user has attempted submit at least once. */
247
+ formSubmitAttempted: boolean;
234
248
  /**
235
249
  * Submete o form. Retorna true se o signup deu OK (backend respondeu 2xx),
236
250
  * false se validação falhou, houve erro de rede/servidor, ou email já em uso.
@@ -254,6 +268,10 @@ interface UseForgotFormResult {
254
268
  email: string;
255
269
  setEmail: (v: string) => void;
256
270
  emailError: string | null;
271
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
272
+ markEmailTouched: () => void;
273
+ /** True after the user has attempted submit at least once. */
274
+ formSubmitAttempted: boolean;
257
275
  /**
258
276
  * Submete o form. Retorna true se o backend aceitou a requisição (email
259
277
  * de reset foi enfileirado — sem leak de "existe esse email" por design),
@@ -274,9 +292,15 @@ interface UseResetFormResult {
274
292
  password: string;
275
293
  setPassword: (v: string) => void;
276
294
  passwordError: string | null;
295
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
296
+ markPasswordTouched: () => void;
277
297
  confirm: string;
278
298
  setConfirm: (v: string) => void;
279
299
  confirmError: string | null;
300
+ /** Wave 5 #42: call on input blur so the error becomes visible. */
301
+ markConfirmTouched: () => void;
302
+ /** True after the user has attempted submit at least once. */
303
+ formSubmitAttempted: boolean;
280
304
  submit: () => Promise<void>;
281
305
  submitting: boolean;
282
306
  canSubmit: boolean;
package/dist/index.js CHANGED
@@ -1849,14 +1849,18 @@ import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef3
1849
1849
  import { useHook as useHook4 } from "@hook-sdk/sdk";
1850
1850
  import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1851
1851
  var BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 4e4];
1852
+ var MAX_CYCLES = 3;
1853
+ var SUPPORT_MAILTO = "mailto:suporte@usehook.net?subject=Pagamento%20pendente";
1852
1854
  function PaymentReturnHandler({ children }) {
1853
1855
  const { subscription } = useHook4();
1854
1856
  const subRef = useRef3(subscription);
1855
1857
  subRef.current = subscription;
1856
1858
  const runIdRef = useRef3(0);
1859
+ const cyclesRef = useRef3(0);
1857
1860
  const [state, setState] = useState5("idle");
1858
1861
  const runPoll = useCallback3(() => {
1859
1862
  const runId = ++runIdRef.current;
1863
+ cyclesRef.current += 1;
1860
1864
  setState("confirming");
1861
1865
  let attempts = 0;
1862
1866
  const tick = async () => {
@@ -1872,12 +1876,17 @@ function PaymentReturnHandler({ children }) {
1872
1876
  const cleanUrl = new URL(window.location.href);
1873
1877
  cleanUrl.searchParams.delete("paymentReturn");
1874
1878
  window.history.replaceState({}, "", cleanUrl.toString());
1879
+ cyclesRef.current = 0;
1875
1880
  setState("idle");
1876
1881
  return;
1877
1882
  }
1878
1883
  const delay = BACKOFF_MS[attempts - 1];
1879
1884
  if (delay === void 0) {
1880
- setState("waiting");
1885
+ if (cyclesRef.current >= MAX_CYCLES) {
1886
+ setState("timeout");
1887
+ } else {
1888
+ setState("waiting");
1889
+ }
1881
1890
  return;
1882
1891
  }
1883
1892
  setTimeout(tick, delay);
@@ -1888,11 +1897,18 @@ function PaymentReturnHandler({ children }) {
1888
1897
  if (typeof window === "undefined") return;
1889
1898
  const url = new URL(window.location.href);
1890
1899
  if (url.searchParams.get("paymentReturn") !== "1") return;
1900
+ cyclesRef.current = 0;
1891
1901
  runPoll();
1892
1902
  return () => {
1893
1903
  runIdRef.current++;
1894
1904
  };
1895
1905
  }, [runPoll]);
1906
+ const goHome = useCallback3(() => {
1907
+ const cleanUrl = new URL(window.location.href);
1908
+ cleanUrl.searchParams.delete("paymentReturn");
1909
+ cleanUrl.pathname = "/app/home";
1910
+ window.location.href = cleanUrl.toString();
1911
+ }, []);
1896
1912
  if (state === "confirming") {
1897
1913
  return /* @__PURE__ */ jsx18("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: "Confirmando pagamento\u2026" });
1898
1914
  }
@@ -1902,6 +1918,45 @@ function PaymentReturnHandler({ children }) {
1902
1918
  /* @__PURE__ */ jsx18("button", { type: "button", onClick: runPoll, style: buttonStyle, children: "Atualizar" })
1903
1919
  ] }) });
1904
1920
  }
1921
+ if (state === "timeout") {
1922
+ return /* @__PURE__ */ jsx18("div", { role: "alert", "aria-live": "assertive", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 360, textAlign: "center", lineHeight: 1.5 }, children: [
1923
+ /* @__PURE__ */ jsx18("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." }),
1924
+ /* @__PURE__ */ jsxs13("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
1925
+ /* @__PURE__ */ jsx18(
1926
+ "button",
1927
+ {
1928
+ type: "button",
1929
+ onClick: () => {
1930
+ cyclesRef.current = 0;
1931
+ runPoll();
1932
+ },
1933
+ style: buttonStyle,
1934
+ "data-testid": "payment-timeout-retry",
1935
+ children: "Tentar de novo"
1936
+ }
1937
+ ),
1938
+ /* @__PURE__ */ jsx18(
1939
+ "button",
1940
+ {
1941
+ type: "button",
1942
+ onClick: goHome,
1943
+ style: secondaryButtonStyle,
1944
+ "data-testid": "payment-timeout-home",
1945
+ children: "Voltar pro app"
1946
+ }
1947
+ ),
1948
+ /* @__PURE__ */ jsx18(
1949
+ "a",
1950
+ {
1951
+ href: SUPPORT_MAILTO,
1952
+ style: linkStyle,
1953
+ "data-testid": "payment-timeout-support",
1954
+ children: "Falar com suporte"
1955
+ }
1956
+ )
1957
+ ] })
1958
+ ] }) });
1959
+ }
1905
1960
  return /* @__PURE__ */ jsx18(Fragment5, { children });
1906
1961
  }
1907
1962
  var overlayStyle2 = {
@@ -1926,6 +1981,19 @@ var buttonStyle = {
1926
1981
  fontWeight: 600,
1927
1982
  cursor: "pointer"
1928
1983
  };
1984
+ var secondaryButtonStyle = {
1985
+ ...buttonStyle,
1986
+ background: "transparent",
1987
+ color: "white",
1988
+ border: "1px solid rgba(255,255,255,0.5)"
1989
+ };
1990
+ var linkStyle = {
1991
+ color: "white",
1992
+ textDecoration: "underline",
1993
+ fontSize: "0.9rem",
1994
+ marginTop: 4,
1995
+ textAlign: "center"
1996
+ };
1929
1997
 
1930
1998
  // src/AppRoot.tsx
1931
1999
  import { Fragment as Fragment6, jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
@@ -2262,18 +2330,24 @@ function useLoginForm() {
2262
2330
  const [password, setPassword] = useState7("");
2263
2331
  const [submitting, setSubmitting] = useState7(false);
2264
2332
  const [error, setError] = useState7(null);
2265
- const emailError = useMemo4(() => {
2333
+ const [touchedEmail, setTouchedEmail] = useState7(false);
2334
+ const [touchedPassword, setTouchedPassword] = useState7(false);
2335
+ const [formSubmitAttempted, setFormSubmitAttempted] = useState7(false);
2336
+ const validateEmail = useMemo4(() => {
2266
2337
  if (email.length === 0) return null;
2267
2338
  if (!EMAIL_RE.test(email)) return "Formato de e-mail inv\xE1lido.";
2268
2339
  return null;
2269
2340
  }, [email]);
2270
- const passwordError = useMemo4(() => {
2341
+ const validatePassword = useMemo4(() => {
2271
2342
  if (password.length === 0) return null;
2272
2343
  if (password.length < MIN_PASSWORD) return `M\xEDnimo de ${MIN_PASSWORD} caracteres.`;
2273
2344
  return null;
2274
2345
  }, [password]);
2275
- const canSubmit = email.length > 0 && password.length >= MIN_PASSWORD && emailError === null && passwordError === null && !submitting;
2346
+ const emailError = touchedEmail || formSubmitAttempted ? validateEmail : null;
2347
+ const passwordError = touchedPassword || formSubmitAttempted ? validatePassword : null;
2348
+ const canSubmit = email.length > 0 && password.length >= MIN_PASSWORD && validateEmail === null && validatePassword === null && !submitting;
2276
2349
  const submit = useCallback5(async () => {
2350
+ setFormSubmitAttempted(true);
2277
2351
  if (!canSubmit) return false;
2278
2352
  setSubmitting(true);
2279
2353
  setError(null);
@@ -2291,9 +2365,12 @@ function useLoginForm() {
2291
2365
  email,
2292
2366
  setEmail,
2293
2367
  emailError,
2368
+ markEmailTouched: () => setTouchedEmail(true),
2294
2369
  password,
2295
2370
  setPassword,
2296
2371
  passwordError,
2372
+ markPasswordTouched: () => setTouchedPassword(true),
2373
+ formSubmitAttempted,
2297
2374
  submit,
2298
2375
  submitting,
2299
2376
  canSubmit,
@@ -2314,23 +2391,31 @@ function useSignupForm() {
2314
2391
  const [password, setPassword] = useState8("");
2315
2392
  const [submitting, setSubmitting] = useState8(false);
2316
2393
  const [error, setError] = useState8(null);
2317
- const nameError = useMemo5(() => {
2394
+ const [touchedName, setTouchedName] = useState8(false);
2395
+ const [touchedEmail, setTouchedEmail] = useState8(false);
2396
+ const [touchedPassword, setTouchedPassword] = useState8(false);
2397
+ const [formSubmitAttempted, setFormSubmitAttempted] = useState8(false);
2398
+ const validateName = useMemo5(() => {
2318
2399
  if (name.length === 0) return null;
2319
2400
  if (name.trim().length < 2) return "Nome muito curto.";
2320
2401
  return null;
2321
2402
  }, [name]);
2322
- const emailError = useMemo5(() => {
2403
+ const validateEmail = useMemo5(() => {
2323
2404
  if (email.length === 0) return null;
2324
2405
  if (!EMAIL_RE2.test(email)) return "Formato de e-mail inv\xE1lido.";
2325
2406
  return null;
2326
2407
  }, [email]);
2327
- const passwordError = useMemo5(() => {
2408
+ const validatePassword = useMemo5(() => {
2328
2409
  if (password.length === 0) return null;
2329
2410
  if (password.length < MIN_PASSWORD2) return `M\xEDnimo de ${MIN_PASSWORD2} caracteres.`;
2330
2411
  return null;
2331
2412
  }, [password]);
2332
- const canSubmit = name.trim().length >= 2 && email.length > 0 && password.length >= MIN_PASSWORD2 && nameError === null && emailError === null && passwordError === null && !submitting;
2413
+ const nameError = touchedName || formSubmitAttempted ? validateName : null;
2414
+ const emailError = touchedEmail || formSubmitAttempted ? validateEmail : null;
2415
+ const passwordError = touchedPassword || formSubmitAttempted ? validatePassword : null;
2416
+ const canSubmit = name.trim().length >= 2 && email.length > 0 && password.length >= MIN_PASSWORD2 && validateName === null && validateEmail === null && validatePassword === null && !submitting;
2333
2417
  const submit = useCallback6(async () => {
2418
+ setFormSubmitAttempted(true);
2334
2419
  if (!canSubmit) return false;
2335
2420
  setSubmitting(true);
2336
2421
  setError(null);
@@ -2348,12 +2433,16 @@ function useSignupForm() {
2348
2433
  name,
2349
2434
  setName,
2350
2435
  nameError,
2436
+ markNameTouched: () => setTouchedName(true),
2351
2437
  email,
2352
2438
  setEmail,
2353
2439
  emailError,
2440
+ markEmailTouched: () => setTouchedEmail(true),
2354
2441
  password,
2355
2442
  setPassword,
2356
2443
  passwordError,
2444
+ markPasswordTouched: () => setTouchedPassword(true),
2445
+ formSubmitAttempted,
2357
2446
  submit,
2358
2447
  submitting,
2359
2448
  canSubmit,
@@ -2372,13 +2461,17 @@ function useForgotForm() {
2372
2461
  const [submitting, setSubmitting] = useState9(false);
2373
2462
  const [sent, setSent] = useState9(false);
2374
2463
  const [error, setError] = useState9(null);
2375
- const emailError = useMemo6(() => {
2464
+ const [touchedEmail, setTouchedEmail] = useState9(false);
2465
+ const [formSubmitAttempted, setFormSubmitAttempted] = useState9(false);
2466
+ const validateEmail = useMemo6(() => {
2376
2467
  if (email.length === 0) return null;
2377
2468
  if (!EMAIL_RE3.test(email)) return "Formato de e-mail inv\xE1lido.";
2378
2469
  return null;
2379
2470
  }, [email]);
2380
- const canSubmit = email.length > 0 && emailError === null && !submitting;
2471
+ const emailError = touchedEmail || formSubmitAttempted ? validateEmail : null;
2472
+ const canSubmit = email.length > 0 && validateEmail === null && !submitting;
2381
2473
  const submit = useCallback7(async () => {
2474
+ setFormSubmitAttempted(true);
2382
2475
  if (!canSubmit) return false;
2383
2476
  setSubmitting(true);
2384
2477
  setError(null);
@@ -2397,6 +2490,8 @@ function useForgotForm() {
2397
2490
  email,
2398
2491
  setEmail,
2399
2492
  emailError,
2493
+ markEmailTouched: () => setTouchedEmail(true),
2494
+ formSubmitAttempted,
2400
2495
  submit,
2401
2496
  submitting,
2402
2497
  canSubmit,
@@ -2417,24 +2512,30 @@ function useResetForm() {
2417
2512
  const [submitting, setSubmitting] = useState10(false);
2418
2513
  const [done, setDone] = useState10(false);
2419
2514
  const [error, setError] = useState10(null);
2515
+ const [touchedPassword, setTouchedPassword] = useState10(false);
2516
+ const [touchedConfirm, setTouchedConfirm] = useState10(false);
2517
+ const [formSubmitAttempted, setFormSubmitAttempted] = useState10(false);
2420
2518
  useEffect8(() => {
2421
2519
  if (typeof window === "undefined") return;
2422
2520
  const params = new URLSearchParams(window.location.search);
2423
2521
  const t = params.get("token");
2424
2522
  setToken(t && t.length > 0 ? t : null);
2425
2523
  }, []);
2426
- const passwordError = useMemo7(() => {
2524
+ const validatePassword = useMemo7(() => {
2427
2525
  if (password.length === 0) return null;
2428
2526
  if (password.length < MIN_PASSWORD3) return `M\xEDnimo de ${MIN_PASSWORD3} caracteres.`;
2429
2527
  return null;
2430
2528
  }, [password]);
2431
- const confirmError = useMemo7(() => {
2529
+ const validateConfirm = useMemo7(() => {
2432
2530
  if (confirm.length === 0) return null;
2433
2531
  if (confirm !== password) return "Senhas n\xE3o coincidem.";
2434
2532
  return null;
2435
2533
  }, [confirm, password]);
2436
- const canSubmit = token !== null && password.length >= MIN_PASSWORD3 && confirm === password && passwordError === null && confirmError === null && !submitting && !done;
2534
+ const passwordError = touchedPassword || formSubmitAttempted ? validatePassword : null;
2535
+ const confirmError = touchedConfirm || formSubmitAttempted ? validateConfirm : null;
2536
+ const canSubmit = token !== null && password.length >= MIN_PASSWORD3 && confirm === password && validatePassword === null && validateConfirm === null && !submitting && !done;
2437
2537
  const submit = useCallback8(async () => {
2538
+ setFormSubmitAttempted(true);
2438
2539
  if (!canSubmit || token === null) return;
2439
2540
  setSubmitting(true);
2440
2541
  setError(null);
@@ -2458,9 +2559,12 @@ function useResetForm() {
2458
2559
  password,
2459
2560
  setPassword,
2460
2561
  passwordError,
2562
+ markPasswordTouched: () => setTouchedPassword(true),
2461
2563
  confirm,
2462
2564
  setConfirm,
2463
2565
  confirmError,
2566
+ markConfirmTouched: () => setTouchedConfirm(true),
2567
+ formSubmitAttempted,
2464
2568
  submit,
2465
2569
  submitting,
2466
2570
  canSubmit,