@hook-sdk/template 0.13.0 → 0.14.1

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
@@ -77,7 +77,11 @@ var DeepLinksSchema = z.object({
77
77
  var AppConfigSchema = z.object({
78
78
  slug: z.string().regex(/^[a-z0-9-]+$/),
79
79
  name: z.string().min(1),
80
- branding: z.object({ primaryColor: z.string(), logoUrl: z.string().url() }),
80
+ branding: z.object({
81
+ primaryColor: z.string(),
82
+ logoUrl: z.string().url(),
83
+ iconUrl: z.string().url().optional()
84
+ }),
81
85
  authFlow: AuthFlowSchema,
82
86
  paywall: PaywallSchema,
83
87
  persistedKeys: z.array(PersistedKeySchema),
@@ -497,6 +501,7 @@ import { useEffect as useEffect4, useRef } from "react";
497
501
 
498
502
  // src/hooks/useInstallPrompt.ts
499
503
  import { useCallback as useCallback2, useEffect as useEffect3, useState as useState2 } from "react";
504
+ var ANDROID_PROMPT_WAIT_MS = 3e3;
500
505
  var IOS_RE = /iPad|iPhone|iPod/;
501
506
  var IOS_NON_SAFARI_RE = /CriOS|FxiOS|EdgiOS/;
502
507
  var ANDROID_RE = /Android/;
@@ -566,11 +571,12 @@ function track(event, props) {
566
571
  if (typeof window === "undefined") return;
567
572
  window.posthog?.capture?.(event, props);
568
573
  }
569
- function pickVariant(state) {
574
+ function pickVariant(state, promptWaitElapsed) {
570
575
  if (state.isInstalled) return "none";
571
576
  switch (state.platform) {
572
577
  case "android":
573
- return state.isInstallable ? "android-native" : "android-manual";
578
+ if (state.isInstallable) return "android-native";
579
+ return promptWaitElapsed ? "android-manual" : "android-pending";
574
580
  case "ios-safari":
575
581
  return "ios-safari";
576
582
  case "ios-other":
@@ -640,6 +646,15 @@ function useInstallPrompt(slug) {
640
646
  const [isDismissedSession, setIsDismissedSession] = useState2(() => readSessionSkip(slug));
641
647
  const [isDismissedPermanent, setIsDismissedPermanent] = useState2(() => readPermanentDismiss(slug).dismissed);
642
648
  const [skipCount, setSkipCount] = useState2(() => readSkipCount(slug));
649
+ const [promptWaitElapsed, setPromptWaitElapsed] = useState2(() => {
650
+ if (typeof window === "undefined") return true;
651
+ return window.__pwaInstallPrompt != null;
652
+ });
653
+ useEffect3(() => {
654
+ if (promptWaitElapsed) return;
655
+ const id = setTimeout(() => setPromptWaitElapsed(true), ANDROID_PROMPT_WAIT_MS);
656
+ return () => clearTimeout(id);
657
+ }, [promptWaitElapsed]);
643
658
  useEffect3(() => {
644
659
  if (typeof window === "undefined") return;
645
660
  if (window.__pwaInstallPrompt) {
@@ -677,17 +692,20 @@ function useInstallPrompt(slug) {
677
692
  mq.addEventListener?.("change", handler);
678
693
  return () => mq.removeEventListener?.("change", handler);
679
694
  }, [slug]);
680
- const variant = pickVariant({
681
- platform,
682
- iosBrowser,
683
- androidBrowser,
684
- inAppApp,
685
- isInstallable,
686
- isInstalled,
687
- isDismissedSession,
688
- isDismissedPermanent,
689
- skipCount
690
- });
695
+ const variant = pickVariant(
696
+ {
697
+ platform,
698
+ iosBrowser,
699
+ androidBrowser,
700
+ inAppApp,
701
+ isInstallable,
702
+ isInstalled,
703
+ isDismissedSession,
704
+ isDismissedPermanent,
705
+ skipCount
706
+ },
707
+ promptWaitElapsed
708
+ );
691
709
  const promptInstall = useCallback2(async () => {
692
710
  if (typeof window === "undefined") return false;
693
711
  const prompt = window.__pwaInstallPrompt;
@@ -1223,8 +1241,44 @@ function Step({ n, icon, children }) {
1223
1241
  );
1224
1242
  }
1225
1243
 
1244
+ // src/components/InstallGate/variants/AndroidPendingVariant.tsx
1245
+ import { jsx as jsx10 } from "react/jsx-runtime";
1246
+ function AndroidPendingVariant() {
1247
+ const copy = INSTALL_COPY.android.native;
1248
+ return /* @__PURE__ */ jsx10(InstallSplash, { title: copy.title, children: /* @__PURE__ */ jsx10(
1249
+ "div",
1250
+ {
1251
+ style: {
1252
+ display: "flex",
1253
+ flexDirection: "column",
1254
+ alignItems: "center",
1255
+ gap: 16,
1256
+ padding: "24px 0"
1257
+ },
1258
+ children: /* @__PURE__ */ jsx10(Spinner, {})
1259
+ }
1260
+ ) });
1261
+ }
1262
+ function Spinner() {
1263
+ return /* @__PURE__ */ jsx10(
1264
+ "div",
1265
+ {
1266
+ "aria-hidden": true,
1267
+ style: {
1268
+ width: 28,
1269
+ height: 28,
1270
+ borderRadius: "50%",
1271
+ border: "3px solid #e5e5e7",
1272
+ borderTopColor: "var(--hook-color-primary)",
1273
+ animation: "hook-install-spin 0.8s linear infinite"
1274
+ },
1275
+ children: /* @__PURE__ */ jsx10("style", { children: `@keyframes hook-install-spin { to { transform: rotate(360deg); } }` })
1276
+ }
1277
+ );
1278
+ }
1279
+
1226
1280
  // src/components/InstallGate/Step.tsx
1227
- import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
1281
+ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1228
1282
  function Step2({
1229
1283
  n,
1230
1284
  title,
@@ -1242,7 +1296,7 @@ function Step2({
1242
1296
  textAlign: "left"
1243
1297
  },
1244
1298
  children: [
1245
- /* @__PURE__ */ jsx10(
1299
+ /* @__PURE__ */ jsx11(
1246
1300
  "div",
1247
1301
  {
1248
1302
  style: {
@@ -1262,8 +1316,8 @@ function Step2({
1262
1316
  }
1263
1317
  ),
1264
1318
  /* @__PURE__ */ jsxs5("div", { style: { flex: 1 }, children: [
1265
- /* @__PURE__ */ jsx10("p", { style: { margin: 0, fontSize: 15, fontWeight: 500, color: "#111", lineHeight: 1.3 }, children: title }),
1266
- subtitle && /* @__PURE__ */ jsx10("p", { style: { margin: "4px 0 0 0", fontSize: 13, color: "#777" }, children: subtitle }),
1319
+ /* @__PURE__ */ jsx11("p", { style: { margin: 0, fontSize: 15, fontWeight: 500, color: "#111", lineHeight: 1.3 }, children: title }),
1320
+ subtitle && /* @__PURE__ */ jsx11("p", { style: { margin: "4px 0 0 0", fontSize: 13, color: "#777" }, children: subtitle }),
1267
1321
  visual
1268
1322
  ] })
1269
1323
  ]
@@ -1272,7 +1326,7 @@ function Step2({
1272
1326
  }
1273
1327
 
1274
1328
  // src/components/InstallGate/variants/IOSafariVariant.tsx
1275
- import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
1329
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
1276
1330
  function IOSafariVariant({
1277
1331
  state,
1278
1332
  actions
@@ -1280,13 +1334,13 @@ function IOSafariVariant({
1280
1334
  const copy = INSTALL_COPY.iosSafari;
1281
1335
  const showPermanent = shouldShowPermanentOption(state);
1282
1336
  return /* @__PURE__ */ jsxs6(InstallSplash, { title: copy.title, subtitle: copy.subtitle, children: [
1283
- /* @__PURE__ */ jsx11(
1337
+ /* @__PURE__ */ jsx12(
1284
1338
  Step2,
1285
1339
  {
1286
1340
  n: 1,
1287
1341
  title: copy.step1.title,
1288
1342
  subtitle: copy.step1.subtitle,
1289
- visual: /* @__PURE__ */ jsx11(
1343
+ visual: /* @__PURE__ */ jsx12(
1290
1344
  "div",
1291
1345
  {
1292
1346
  style: {
@@ -1298,12 +1352,12 @@ function IOSafariVariant({
1298
1352
  padding: "12px 0",
1299
1353
  marginTop: 8
1300
1354
  },
1301
- children: /* @__PURE__ */ jsx11(ShareIconIOS, { size: 32, style: { color: "var(--hook-color-primary)" } })
1355
+ children: /* @__PURE__ */ jsx12(ShareIconIOS, { size: 32, style: { color: "var(--hook-color-primary)" } })
1302
1356
  }
1303
1357
  )
1304
1358
  }
1305
1359
  ),
1306
- /* @__PURE__ */ jsx11(
1360
+ /* @__PURE__ */ jsx12(
1307
1361
  Step2,
1308
1362
  {
1309
1363
  n: 2,
@@ -1321,19 +1375,19 @@ function IOSafariVariant({
1321
1375
  marginTop: 8
1322
1376
  },
1323
1377
  children: [
1324
- /* @__PURE__ */ jsx11(SquarePlusIcon, { size: 22, style: { color: "#555" } }),
1325
- /* @__PURE__ */ jsx11("span", { style: { fontSize: 14, color: "#333" }, children: copy.step2.iconLabel })
1378
+ /* @__PURE__ */ jsx12(SquarePlusIcon, { size: 22, style: { color: "#555" } }),
1379
+ /* @__PURE__ */ jsx12("span", { style: { fontSize: 14, color: "#333" }, children: copy.step2.iconLabel })
1326
1380
  ]
1327
1381
  }
1328
1382
  )
1329
1383
  }
1330
1384
  ),
1331
- /* @__PURE__ */ jsx11(
1385
+ /* @__PURE__ */ jsx12(
1332
1386
  Step2,
1333
1387
  {
1334
1388
  n: 3,
1335
1389
  title: copy.step3.title,
1336
- visual: /* @__PURE__ */ jsx11(
1390
+ visual: /* @__PURE__ */ jsx12(
1337
1391
  "div",
1338
1392
  {
1339
1393
  style: {
@@ -1344,7 +1398,7 @@ function IOSafariVariant({
1344
1398
  padding: "10px 14px",
1345
1399
  marginTop: 8
1346
1400
  },
1347
- children: /* @__PURE__ */ jsx11(
1401
+ children: /* @__PURE__ */ jsx12(
1348
1402
  "span",
1349
1403
  {
1350
1404
  style: {
@@ -1359,7 +1413,7 @@ function IOSafariVariant({
1359
1413
  )
1360
1414
  }
1361
1415
  ),
1362
- /* @__PURE__ */ jsx11(
1416
+ /* @__PURE__ */ jsx12(
1363
1417
  "button",
1364
1418
  {
1365
1419
  "data-testid": "install-prompt-skip-session",
@@ -1369,7 +1423,7 @@ function IOSafariVariant({
1369
1423
  children: copy.skip
1370
1424
  }
1371
1425
  ),
1372
- showPermanent && /* @__PURE__ */ jsx11(
1426
+ showPermanent && /* @__PURE__ */ jsx12(
1373
1427
  "button",
1374
1428
  {
1375
1429
  "data-testid": "install-prompt-skip-permanent",
@@ -1383,7 +1437,7 @@ function IOSafariVariant({
1383
1437
  }
1384
1438
 
1385
1439
  // src/components/InstallGate/variants/IOSOtherVariant.tsx
1386
- import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
1440
+ import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
1387
1441
  function IOSOtherVariant({
1388
1442
  state,
1389
1443
  actions
@@ -1391,13 +1445,13 @@ function IOSOtherVariant({
1391
1445
  const copy = INSTALL_COPY.iosOther;
1392
1446
  const showPermanent = shouldShowPermanentOption(state);
1393
1447
  return /* @__PURE__ */ jsxs7(InstallSplash, { title: copy.title, subtitle: copy.subtitle, children: [
1394
- /* @__PURE__ */ jsx12(
1448
+ /* @__PURE__ */ jsx13(
1395
1449
  Step2,
1396
1450
  {
1397
1451
  n: 1,
1398
1452
  title: copy.step1.title,
1399
1453
  subtitle: copy.step1.subtitle,
1400
- visual: /* @__PURE__ */ jsx12(
1454
+ visual: /* @__PURE__ */ jsx13(
1401
1455
  "div",
1402
1456
  {
1403
1457
  style: {
@@ -1409,12 +1463,12 @@ function IOSOtherVariant({
1409
1463
  padding: "12px 0",
1410
1464
  marginTop: 8
1411
1465
  },
1412
- children: /* @__PURE__ */ jsx12(ShareIconIOS, { size: 32, style: { color: "var(--hook-color-primary)" } })
1466
+ children: /* @__PURE__ */ jsx13(ShareIconIOS, { size: 32, style: { color: "var(--hook-color-primary)" } })
1413
1467
  }
1414
1468
  )
1415
1469
  }
1416
1470
  ),
1417
- /* @__PURE__ */ jsx12(
1471
+ /* @__PURE__ */ jsx13(
1418
1472
  Step2,
1419
1473
  {
1420
1474
  n: 2,
@@ -1432,19 +1486,19 @@ function IOSOtherVariant({
1432
1486
  marginTop: 8
1433
1487
  },
1434
1488
  children: [
1435
- /* @__PURE__ */ jsx12(SquarePlusIcon, { size: 22, style: { color: "#555" } }),
1436
- /* @__PURE__ */ jsx12("span", { style: { fontSize: 14, color: "#333" }, children: copy.step2.iconLabel })
1489
+ /* @__PURE__ */ jsx13(SquarePlusIcon, { size: 22, style: { color: "#555" } }),
1490
+ /* @__PURE__ */ jsx13("span", { style: { fontSize: 14, color: "#333" }, children: copy.step2.iconLabel })
1437
1491
  ]
1438
1492
  }
1439
1493
  )
1440
1494
  }
1441
1495
  ),
1442
- /* @__PURE__ */ jsx12(
1496
+ /* @__PURE__ */ jsx13(
1443
1497
  Step2,
1444
1498
  {
1445
1499
  n: 3,
1446
1500
  title: copy.step3.title,
1447
- visual: /* @__PURE__ */ jsx12(
1501
+ visual: /* @__PURE__ */ jsx13(
1448
1502
  "div",
1449
1503
  {
1450
1504
  style: {
@@ -1455,7 +1509,7 @@ function IOSOtherVariant({
1455
1509
  padding: "10px 14px",
1456
1510
  marginTop: 8
1457
1511
  },
1458
- children: /* @__PURE__ */ jsx12(
1512
+ children: /* @__PURE__ */ jsx13(
1459
1513
  "span",
1460
1514
  {
1461
1515
  style: {
@@ -1470,7 +1524,7 @@ function IOSOtherVariant({
1470
1524
  )
1471
1525
  }
1472
1526
  ),
1473
- /* @__PURE__ */ jsx12(
1527
+ /* @__PURE__ */ jsx13(
1474
1528
  "button",
1475
1529
  {
1476
1530
  "data-testid": "install-prompt-skip-session",
@@ -1480,7 +1534,7 @@ function IOSOtherVariant({
1480
1534
  children: copy.skip
1481
1535
  }
1482
1536
  ),
1483
- showPermanent && /* @__PURE__ */ jsx12(
1537
+ showPermanent && /* @__PURE__ */ jsx13(
1484
1538
  "button",
1485
1539
  {
1486
1540
  "data-testid": "install-prompt-skip-permanent",
@@ -1495,7 +1549,7 @@ function IOSOtherVariant({
1495
1549
 
1496
1550
  // src/components/InstallGate/variants/InAppBrowserVariant.tsx
1497
1551
  import { useState as useState3 } from "react";
1498
- import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1552
+ import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
1499
1553
  function InAppBrowserVariant({
1500
1554
  state,
1501
1555
  actions
@@ -1512,9 +1566,9 @@ function InAppBrowserVariant({
1512
1566
  };
1513
1567
  const DotsIcon = app === "facebook" || app === "telegram" ? MenuDotsVerticalIcon : MenuDotsHorizontalIcon;
1514
1568
  return /* @__PURE__ */ jsxs8(InstallSplash, { title: appCopy.title, children: [
1515
- /* @__PURE__ */ jsx13(Step3, { n: 1, icon: /* @__PURE__ */ jsx13(DotsIcon, { size: 20 }), children: appCopy.step1 }),
1516
- /* @__PURE__ */ jsx13(Step3, { n: 2, icon: /* @__PURE__ */ jsx13(ExternalLinkIcon, { size: 18 }), children: appCopy.step2 }),
1517
- /* @__PURE__ */ jsx13(
1569
+ /* @__PURE__ */ jsx14(Step3, { n: 1, icon: /* @__PURE__ */ jsx14(DotsIcon, { size: 20 }), children: appCopy.step1 }),
1570
+ /* @__PURE__ */ jsx14(Step3, { n: 2, icon: /* @__PURE__ */ jsx14(ExternalLinkIcon, { size: 18 }), children: appCopy.step2 }),
1571
+ /* @__PURE__ */ jsx14(
1518
1572
  "button",
1519
1573
  {
1520
1574
  "data-testid": "install-prompt-cta-inapp-copy",
@@ -1524,7 +1578,7 @@ function InAppBrowserVariant({
1524
1578
  children: copied ? copy.copiedToast : copy.copy
1525
1579
  }
1526
1580
  ),
1527
- /* @__PURE__ */ jsx13(
1581
+ /* @__PURE__ */ jsx14(
1528
1582
  "button",
1529
1583
  {
1530
1584
  "data-testid": "install-prompt-skip-session",
@@ -1534,7 +1588,7 @@ function InAppBrowserVariant({
1534
1588
  children: copy.skip
1535
1589
  }
1536
1590
  ),
1537
- showPermanent && /* @__PURE__ */ jsx13(
1591
+ showPermanent && /* @__PURE__ */ jsx14(
1538
1592
  "button",
1539
1593
  {
1540
1594
  "data-testid": "install-prompt-skip-permanent",
@@ -1565,7 +1619,7 @@ function Step3({
1565
1619
  textAlign: "left"
1566
1620
  },
1567
1621
  children: [
1568
- /* @__PURE__ */ jsx13(
1622
+ /* @__PURE__ */ jsx14(
1569
1623
  "div",
1570
1624
  {
1571
1625
  style: {
@@ -1584,15 +1638,15 @@ function Step3({
1584
1638
  children: n
1585
1639
  }
1586
1640
  ),
1587
- /* @__PURE__ */ jsx13("div", { style: { flex: 1, fontSize: 14, color: "#333" }, children }),
1588
- /* @__PURE__ */ jsx13("div", { style: { color: "#888", flexShrink: 0 }, children: icon })
1641
+ /* @__PURE__ */ jsx14("div", { style: { flex: 1, fontSize: 14, color: "#333" }, children }),
1642
+ /* @__PURE__ */ jsx14("div", { style: { color: "#888", flexShrink: 0 }, children: icon })
1589
1643
  ]
1590
1644
  }
1591
1645
  );
1592
1646
  }
1593
1647
 
1594
1648
  // src/components/InstallGate/variants/DesktopVariant.tsx
1595
- import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1649
+ import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
1596
1650
  function DesktopVariant({
1597
1651
  state,
1598
1652
  actions
@@ -1608,14 +1662,14 @@ function DesktopVariant({
1608
1662
  "aria-label": copy.title,
1609
1663
  style: bannerStyle,
1610
1664
  children: [
1611
- iconUrl ? /* @__PURE__ */ jsx14(
1665
+ iconUrl ? /* @__PURE__ */ jsx15(
1612
1666
  "img",
1613
1667
  {
1614
1668
  src: iconUrl,
1615
1669
  alt: "",
1616
1670
  style: { width: 40, height: 40, borderRadius: 10, objectFit: "cover", flexShrink: 0 }
1617
1671
  }
1618
- ) : /* @__PURE__ */ jsx14(
1672
+ ) : /* @__PURE__ */ jsx15(
1619
1673
  "div",
1620
1674
  {
1621
1675
  style: {
@@ -1635,8 +1689,8 @@ function DesktopVariant({
1635
1689
  }
1636
1690
  ),
1637
1691
  /* @__PURE__ */ jsxs9("div", { style: { flex: 1, minWidth: 0 }, children: [
1638
- /* @__PURE__ */ jsx14("div", { style: { fontSize: 14, fontWeight: 600, color: "#111" }, children: copy.title }),
1639
- /* @__PURE__ */ jsx14("div", { style: { fontSize: 12, color: "#666" }, children: copy.subtitle })
1692
+ /* @__PURE__ */ jsx15("div", { style: { fontSize: 14, fontWeight: 600, color: "#111" }, children: copy.title }),
1693
+ /* @__PURE__ */ jsx15("div", { style: { fontSize: 12, color: "#666" }, children: copy.subtitle })
1640
1694
  ] }),
1641
1695
  /* @__PURE__ */ jsxs9(
1642
1696
  "button",
@@ -1659,12 +1713,12 @@ function DesktopVariant({
1659
1713
  flexShrink: 0
1660
1714
  },
1661
1715
  children: [
1662
- /* @__PURE__ */ jsx14(DownloadIcon, { size: 14 }),
1716
+ /* @__PURE__ */ jsx15(DownloadIcon, { size: 14 }),
1663
1717
  copy.cta
1664
1718
  ]
1665
1719
  }
1666
1720
  ),
1667
- /* @__PURE__ */ jsx14(
1721
+ /* @__PURE__ */ jsx15(
1668
1722
  "button",
1669
1723
  {
1670
1724
  "data-testid": "install-prompt-desktop-close",
@@ -1679,7 +1733,7 @@ function DesktopVariant({
1679
1733
  padding: 4,
1680
1734
  flexShrink: 0
1681
1735
  },
1682
- children: /* @__PURE__ */ jsx14(XIcon, { size: 16 })
1736
+ children: /* @__PURE__ */ jsx15(XIcon, { size: 16 })
1683
1737
  }
1684
1738
  )
1685
1739
  ]
@@ -1703,7 +1757,7 @@ var bannerStyle = {
1703
1757
  };
1704
1758
 
1705
1759
  // src/components/InstallGate/InstallGate.tsx
1706
- import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1760
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1707
1761
  function InstallGate({ children }) {
1708
1762
  const { slug, features_enabled } = useTemplateConfig();
1709
1763
  const enabled = features_enabled.includes("install_prompt");
@@ -1724,30 +1778,32 @@ function InstallGate({ children }) {
1724
1778
  variant: installState.variant
1725
1779
  });
1726
1780
  }, [shouldBlock, slug, installState.variant, installState.platform, installState.iosBrowser, installState.androidBrowser, installState.inAppApp]);
1727
- if (!enabled) return /* @__PURE__ */ jsx15(Fragment3, { children });
1728
- if (installState.isInstalled) return /* @__PURE__ */ jsx15(Fragment3, { children });
1781
+ if (!enabled) return /* @__PURE__ */ jsx16(Fragment3, { children });
1782
+ if (installState.isInstalled) return /* @__PURE__ */ jsx16(Fragment3, { children });
1729
1783
  if (installState.variant === "desktop") {
1730
1784
  const showBanner = !installState.isDismissedSession && !installState.isDismissedPermanent;
1731
1785
  return /* @__PURE__ */ jsxs10(Fragment3, { children: [
1732
1786
  children,
1733
- showBanner && /* @__PURE__ */ jsx15(DesktopVariant, { state: installState, actions: installState })
1787
+ showBanner && /* @__PURE__ */ jsx16(DesktopVariant, { state: installState, actions: installState })
1734
1788
  ] });
1735
1789
  }
1736
- if (!shouldBlock) return /* @__PURE__ */ jsx15(Fragment3, { children });
1790
+ if (!shouldBlock) return /* @__PURE__ */ jsx16(Fragment3, { children });
1737
1791
  switch (installState.variant) {
1738
1792
  case "android-native":
1739
- return /* @__PURE__ */ jsx15(AndroidNativeVariant, { state: installState, actions: installState });
1793
+ return /* @__PURE__ */ jsx16(AndroidNativeVariant, { state: installState, actions: installState });
1740
1794
  case "android-manual":
1741
- return /* @__PURE__ */ jsx15(AndroidManualVariant, { state: installState, actions: installState });
1795
+ return /* @__PURE__ */ jsx16(AndroidManualVariant, { state: installState, actions: installState });
1796
+ case "android-pending":
1797
+ return /* @__PURE__ */ jsx16(AndroidPendingVariant, {});
1742
1798
  case "ios-safari":
1743
- return /* @__PURE__ */ jsx15(IOSafariVariant, { state: installState, actions: installState });
1799
+ return /* @__PURE__ */ jsx16(IOSafariVariant, { state: installState, actions: installState });
1744
1800
  case "ios-other":
1745
- return /* @__PURE__ */ jsx15(IOSOtherVariant, { state: installState, actions: installState });
1801
+ return /* @__PURE__ */ jsx16(IOSOtherVariant, { state: installState, actions: installState });
1746
1802
  case "in-app":
1747
- return /* @__PURE__ */ jsx15(InAppBrowserVariant, { state: installState, actions: installState });
1803
+ return /* @__PURE__ */ jsx16(InAppBrowserVariant, { state: installState, actions: installState });
1748
1804
  case "none":
1749
1805
  default:
1750
- return /* @__PURE__ */ jsx15(Fragment3, { children });
1806
+ return /* @__PURE__ */ jsx16(Fragment3, { children });
1751
1807
  }
1752
1808
  }
1753
1809
 
@@ -1759,7 +1815,7 @@ function PushPrompt() {
1759
1815
  // src/internal/SessionExpiredBanner.tsx
1760
1816
  import { useEffect as useEffect5, useRef as useRef2, useState as useState4 } from "react";
1761
1817
  import { useHook as useHook3 } from "@hook-sdk/sdk";
1762
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
1818
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
1763
1819
  var DISMISS_KEY = "hook:session-expired-dismissed-until";
1764
1820
  var DISMISS_TTL_MS = 60 * 60 * 1e3;
1765
1821
  function SessionExpiredBanner() {
@@ -1788,11 +1844,11 @@ function SessionExpiredBanner() {
1788
1844
  }
1789
1845
  return /* @__PURE__ */ jsxs11("div", { role: "alert", className: "fixed top-0 inset-x-0 bg-red-600 text-white px-4 py-2 flex items-center justify-between gap-3 text-sm shadow", style: { zIndex: 10001 }, children: [
1790
1846
  /* @__PURE__ */ jsxs11("span", { children: [
1791
- /* @__PURE__ */ jsx16("strong", { children: "Sua sess\xE3o expirou." }),
1847
+ /* @__PURE__ */ jsx17("strong", { children: "Sua sess\xE3o expirou." }),
1792
1848
  " Fa\xE7a login novamente para continuar."
1793
1849
  ] }),
1794
1850
  /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
1795
- /* @__PURE__ */ jsx16(
1851
+ /* @__PURE__ */ jsx17(
1796
1852
  "button",
1797
1853
  {
1798
1854
  type: "button",
@@ -1801,7 +1857,7 @@ function SessionExpiredBanner() {
1801
1857
  children: "Fazer login"
1802
1858
  }
1803
1859
  ),
1804
- /* @__PURE__ */ jsx16(
1860
+ /* @__PURE__ */ jsx17(
1805
1861
  "button",
1806
1862
  {
1807
1863
  type: "button",
@@ -1817,7 +1873,7 @@ function SessionExpiredBanner() {
1817
1873
 
1818
1874
  // src/defaults/ErrorBoundary.tsx
1819
1875
  import { Component } from "react";
1820
- import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1876
+ import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
1821
1877
  var ErrorBoundary = class extends Component {
1822
1878
  state = { error: null };
1823
1879
  static getDerivedStateFromError(error) {
@@ -1836,27 +1892,31 @@ var ErrorBoundary = class extends Component {
1836
1892
  render() {
1837
1893
  if (this.state.error) {
1838
1894
  return this.props.fallback ?? /* @__PURE__ */ jsxs12("div", { role: "alert", style: { padding: 24, textAlign: "center" }, children: [
1839
- /* @__PURE__ */ jsx17("h2", { children: "Algo deu errado" }),
1840
- /* @__PURE__ */ jsx17("p", { style: { opacity: 0.7 }, children: "Recarregue a p\xE1gina pra tentar de novo." })
1895
+ /* @__PURE__ */ jsx18("h2", { children: "Algo deu errado" }),
1896
+ /* @__PURE__ */ jsx18("p", { style: { opacity: 0.7 }, children: "Recarregue a p\xE1gina pra tentar de novo." })
1841
1897
  ] });
1842
1898
  }
1843
- return /* @__PURE__ */ jsx17(Fragment4, { children: this.props.children });
1899
+ return /* @__PURE__ */ jsx18(Fragment4, { children: this.props.children });
1844
1900
  }
1845
1901
  };
1846
1902
 
1847
1903
  // src/internal/PaymentReturnHandler.tsx
1848
1904
  import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef3, useState as useState5 } from "react";
1849
1905
  import { useHook as useHook4 } from "@hook-sdk/sdk";
1850
- import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1906
+ import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
1851
1907
  var BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 4e4];
1908
+ var MAX_CYCLES = 3;
1909
+ var SUPPORT_MAILTO = "mailto:suporte@usehook.net?subject=Pagamento%20pendente";
1852
1910
  function PaymentReturnHandler({ children }) {
1853
1911
  const { subscription } = useHook4();
1854
1912
  const subRef = useRef3(subscription);
1855
1913
  subRef.current = subscription;
1856
1914
  const runIdRef = useRef3(0);
1915
+ const cyclesRef = useRef3(0);
1857
1916
  const [state, setState] = useState5("idle");
1858
1917
  const runPoll = useCallback3(() => {
1859
1918
  const runId = ++runIdRef.current;
1919
+ cyclesRef.current += 1;
1860
1920
  setState("confirming");
1861
1921
  let attempts = 0;
1862
1922
  const tick = async () => {
@@ -1872,12 +1932,17 @@ function PaymentReturnHandler({ children }) {
1872
1932
  const cleanUrl = new URL(window.location.href);
1873
1933
  cleanUrl.searchParams.delete("paymentReturn");
1874
1934
  window.history.replaceState({}, "", cleanUrl.toString());
1935
+ cyclesRef.current = 0;
1875
1936
  setState("idle");
1876
1937
  return;
1877
1938
  }
1878
1939
  const delay = BACKOFF_MS[attempts - 1];
1879
1940
  if (delay === void 0) {
1880
- setState("waiting");
1941
+ if (cyclesRef.current >= MAX_CYCLES) {
1942
+ setState("timeout");
1943
+ } else {
1944
+ setState("waiting");
1945
+ }
1881
1946
  return;
1882
1947
  }
1883
1948
  setTimeout(tick, delay);
@@ -1888,21 +1953,67 @@ function PaymentReturnHandler({ children }) {
1888
1953
  if (typeof window === "undefined") return;
1889
1954
  const url = new URL(window.location.href);
1890
1955
  if (url.searchParams.get("paymentReturn") !== "1") return;
1956
+ cyclesRef.current = 0;
1891
1957
  runPoll();
1892
1958
  return () => {
1893
1959
  runIdRef.current++;
1894
1960
  };
1895
1961
  }, [runPoll]);
1962
+ const goHome = useCallback3(() => {
1963
+ const cleanUrl = new URL(window.location.href);
1964
+ cleanUrl.searchParams.delete("paymentReturn");
1965
+ cleanUrl.pathname = "/app/home";
1966
+ window.location.href = cleanUrl.toString();
1967
+ }, []);
1896
1968
  if (state === "confirming") {
1897
- return /* @__PURE__ */ jsx18("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: "Confirmando pagamento\u2026" });
1969
+ return /* @__PURE__ */ jsx19("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: "Confirmando pagamento\u2026" });
1898
1970
  }
1899
1971
  if (state === "waiting") {
1900
- return /* @__PURE__ */ jsx18("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 320, textAlign: "center", lineHeight: 1.5 }, children: [
1901
- /* @__PURE__ */ jsx18("div", { style: { marginBottom: 16 }, children: "Pagamento aceito. Estamos confirmando com o banco \u2014 pode levar alguns minutos." }),
1902
- /* @__PURE__ */ jsx18("button", { type: "button", onClick: runPoll, style: buttonStyle, children: "Atualizar" })
1972
+ return /* @__PURE__ */ jsx19("div", { role: "status", "aria-live": "polite", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 320, textAlign: "center", lineHeight: 1.5 }, children: [
1973
+ /* @__PURE__ */ jsx19("div", { style: { marginBottom: 16 }, children: "Pagamento aceito. Estamos confirmando com o banco \u2014 pode levar alguns minutos." }),
1974
+ /* @__PURE__ */ jsx19("button", { type: "button", onClick: runPoll, style: buttonStyle, children: "Atualizar" })
1903
1975
  ] }) });
1904
1976
  }
1905
- return /* @__PURE__ */ jsx18(Fragment5, { children });
1977
+ if (state === "timeout") {
1978
+ return /* @__PURE__ */ jsx19("div", { role: "alert", "aria-live": "assertive", style: overlayStyle2, children: /* @__PURE__ */ jsxs13("div", { style: { maxWidth: 360, textAlign: "center", lineHeight: 1.5 }, children: [
1979
+ /* @__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." }),
1980
+ /* @__PURE__ */ jsxs13("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
1981
+ /* @__PURE__ */ jsx19(
1982
+ "button",
1983
+ {
1984
+ type: "button",
1985
+ onClick: () => {
1986
+ cyclesRef.current = 0;
1987
+ runPoll();
1988
+ },
1989
+ style: buttonStyle,
1990
+ "data-testid": "payment-timeout-retry",
1991
+ children: "Tentar de novo"
1992
+ }
1993
+ ),
1994
+ /* @__PURE__ */ jsx19(
1995
+ "button",
1996
+ {
1997
+ type: "button",
1998
+ onClick: goHome,
1999
+ style: secondaryButtonStyle,
2000
+ "data-testid": "payment-timeout-home",
2001
+ children: "Voltar pro app"
2002
+ }
2003
+ ),
2004
+ /* @__PURE__ */ jsx19(
2005
+ "a",
2006
+ {
2007
+ href: SUPPORT_MAILTO,
2008
+ style: linkStyle,
2009
+ "data-testid": "payment-timeout-support",
2010
+ children: "Falar com suporte"
2011
+ }
2012
+ )
2013
+ ] })
2014
+ ] }) });
2015
+ }
2016
+ return /* @__PURE__ */ jsx19(Fragment5, { children });
1906
2017
  }
1907
2018
  var overlayStyle2 = {
1908
2019
  position: "fixed",
@@ -1926,9 +2037,22 @@ var buttonStyle = {
1926
2037
  fontWeight: 600,
1927
2038
  cursor: "pointer"
1928
2039
  };
2040
+ var secondaryButtonStyle = {
2041
+ ...buttonStyle,
2042
+ background: "transparent",
2043
+ color: "white",
2044
+ border: "1px solid rgba(255,255,255,0.5)"
2045
+ };
2046
+ var linkStyle = {
2047
+ color: "white",
2048
+ textDecoration: "underline",
2049
+ fontSize: "0.9rem",
2050
+ marginTop: 4,
2051
+ textAlign: "center"
2052
+ };
1929
2053
 
1930
2054
  // src/AppRoot.tsx
1931
- import { Fragment as Fragment6, jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
2055
+ import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
1932
2056
  function buildLegacyConfigShim(config) {
1933
2057
  const paywall = config.paywall;
1934
2058
  const isFree = paywall.mode === "free";
@@ -1938,7 +2062,16 @@ function buildLegacyConfigShim(config) {
1938
2062
  slug: config.slug,
1939
2063
  name: config.name,
1940
2064
  email_alias: config.slug,
1941
- theme: { primary_color: config.branding.primaryColor },
2065
+ // Map branding into the legacy theme shape so InstallSplash (and
2066
+ // anything else reading theme.icon_url / theme.logo_url) can surface
2067
+ // the app icon instead of the generic "first letter of name" fallback.
2068
+ // Falls back to logoUrl when iconUrl is unset — apps that haven't
2069
+ // adopted iconUrl keep their previous behavior unchanged.
2070
+ theme: {
2071
+ primary_color: config.branding.primaryColor,
2072
+ icon_url: config.branding.iconUrl ?? config.branding.logoUrl,
2073
+ logo_url: config.branding.logoUrl
2074
+ },
1942
2075
  features_enabled: config.features_enabled ?? [],
1943
2076
  dependencies_allowlist: ["react", "react-dom"],
1944
2077
  subscription: {
@@ -1996,10 +2129,10 @@ function AppRoot(props) {
1996
2129
  const Router = testRouter === "memory" ? MemoryRouter : BrowserRouter;
1997
2130
  const basename = `/app/${config.slug}`;
1998
2131
  const routerProps = testRouter === "memory" ? { basename, initialEntries: testInitialEntries } : { basename };
1999
- return /* @__PURE__ */ jsx19(ErrorBoundary, { children: /* @__PURE__ */ jsx19(AppConfigProvider, { config, children: /* @__PURE__ */ jsx19(TemplateConfigProvider, { config: legacyShim, children: /* @__PURE__ */ jsx19(ThemeProvider, { children: /* @__PURE__ */ jsx19(PersistenceRegistry, { config: config.persistedKeys, children: /* @__PURE__ */ jsxs14(Router, { ...routerProps, children: [
2000
- /* @__PURE__ */ jsx19(DeepLinkHandler, { deepLinks: config.deepLinks }),
2001
- /* @__PURE__ */ jsx19(SessionExpiredBanner, {}),
2002
- /* @__PURE__ */ jsx19(InstallGate, { children: /* @__PURE__ */ jsx19(
2132
+ 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: [
2133
+ /* @__PURE__ */ jsx20(DeepLinkHandler, { deepLinks: config.deepLinks }),
2134
+ /* @__PURE__ */ jsx20(SessionExpiredBanner, {}),
2135
+ /* @__PURE__ */ jsx20(InstallGate, { children: /* @__PURE__ */ jsx20(
2003
2136
  AuthGated,
2004
2137
  {
2005
2138
  config,
@@ -2013,7 +2146,7 @@ function AppRoot(props) {
2013
2146
  PreAuthFlow,
2014
2147
  children: /* @__PURE__ */ jsxs14(SubscriptionGate, { Paywall: Paywall ?? FallbackPaywall, children: [
2015
2148
  children,
2016
- /* @__PURE__ */ jsx19(PushPrompt, {})
2149
+ /* @__PURE__ */ jsx20(PushPrompt, {})
2017
2150
  ] })
2018
2151
  }
2019
2152
  ) })
@@ -2034,24 +2167,24 @@ function AuthGated({
2034
2167
  if (authStatus !== "authenticated") {
2035
2168
  if (config.onboarding?.trigger === "pre_signup_custom" && PreAuthFlow) {
2036
2169
  return /* @__PURE__ */ jsxs14(Routes, { children: [
2037
- /* @__PURE__ */ jsx19(Route, { path: "/signin", element: /* @__PURE__ */ jsx19(Login, {}) }),
2038
- /* @__PURE__ */ jsx19(Route, { path: "/signup", element: /* @__PURE__ */ jsx19(Signup, {}) }),
2039
- /* @__PURE__ */ jsx19(Route, { path: "/forgot", element: /* @__PURE__ */ jsx19(Forgot, {}) }),
2040
- /* @__PURE__ */ jsx19(Route, { path: "/reset", element: /* @__PURE__ */ jsx19(Reset, {}) }),
2041
- EmailVerify ? /* @__PURE__ */ jsx19(Route, { path: "/verify", element: /* @__PURE__ */ jsx19(EmailVerify, {}) }) : null,
2042
- /* @__PURE__ */ jsx19(Route, { path: "/*", element: /* @__PURE__ */ jsx19(PreAuthFlow, {}) })
2170
+ /* @__PURE__ */ jsx20(Route, { path: "/signin", element: /* @__PURE__ */ jsx20(Login, {}) }),
2171
+ /* @__PURE__ */ jsx20(Route, { path: "/signup", element: /* @__PURE__ */ jsx20(Signup, {}) }),
2172
+ /* @__PURE__ */ jsx20(Route, { path: "/forgot", element: /* @__PURE__ */ jsx20(Forgot, {}) }),
2173
+ /* @__PURE__ */ jsx20(Route, { path: "/reset", element: /* @__PURE__ */ jsx20(Reset, {}) }),
2174
+ EmailVerify ? /* @__PURE__ */ jsx20(Route, { path: "/verify", element: /* @__PURE__ */ jsx20(EmailVerify, {}) }) : null,
2175
+ /* @__PURE__ */ jsx20(Route, { path: "/*", element: /* @__PURE__ */ jsx20(PreAuthFlow, {}) })
2043
2176
  ] });
2044
2177
  }
2045
2178
  return /* @__PURE__ */ jsxs14(Routes, { children: [
2046
- /* @__PURE__ */ jsx19(Route, { path: "/", element: /* @__PURE__ */ jsx19(Login, {}) }),
2047
- /* @__PURE__ */ jsx19(Route, { path: "/signup", element: /* @__PURE__ */ jsx19(Signup, {}) }),
2048
- /* @__PURE__ */ jsx19(Route, { path: "/forgot", element: /* @__PURE__ */ jsx19(Forgot, {}) }),
2049
- /* @__PURE__ */ jsx19(Route, { path: "/reset", element: /* @__PURE__ */ jsx19(Reset, {}) }),
2050
- EmailVerify ? /* @__PURE__ */ jsx19(Route, { path: "/verify", element: /* @__PURE__ */ jsx19(EmailVerify, {}) }) : null,
2051
- /* @__PURE__ */ jsx19(Route, { path: "*", element: /* @__PURE__ */ jsx19(Navigate, { to: "/", replace: true }) })
2179
+ /* @__PURE__ */ jsx20(Route, { path: "/", element: /* @__PURE__ */ jsx20(Login, {}) }),
2180
+ /* @__PURE__ */ jsx20(Route, { path: "/signup", element: /* @__PURE__ */ jsx20(Signup, {}) }),
2181
+ /* @__PURE__ */ jsx20(Route, { path: "/forgot", element: /* @__PURE__ */ jsx20(Forgot, {}) }),
2182
+ /* @__PURE__ */ jsx20(Route, { path: "/reset", element: /* @__PURE__ */ jsx20(Reset, {}) }),
2183
+ EmailVerify ? /* @__PURE__ */ jsx20(Route, { path: "/verify", element: /* @__PURE__ */ jsx20(EmailVerify, {}) }) : null,
2184
+ /* @__PURE__ */ jsx20(Route, { path: "*", element: /* @__PURE__ */ jsx20(Navigate, { to: "/", replace: true }) })
2052
2185
  ] });
2053
2186
  }
2054
- return /* @__PURE__ */ jsx19(Fragment6, { children });
2187
+ return /* @__PURE__ */ jsx20(Fragment6, { children });
2055
2188
  }
2056
2189
  function FallbackPaywall() {
2057
2190
  return null;
@@ -2142,7 +2275,7 @@ function usePush() {
2142
2275
  }
2143
2276
 
2144
2277
  // src/components/PushPrompt.tsx
2145
- import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
2278
+ import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
2146
2279
  function platformRecoveryCopy(texts) {
2147
2280
  if (typeof navigator === "undefined") return null;
2148
2281
  const ua = navigator.userAgent || "";
@@ -2166,27 +2299,27 @@ function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, clas
2166
2299
  if (state.kind === "subscribed" || state.kind === "dismissed") return null;
2167
2300
  if (state.kind === "ios_needs_install") {
2168
2301
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", "aria-label": texts.iosInstallTitle, children: [
2169
- /* @__PURE__ */ jsx20("h3", { children: texts.iosInstallTitle }),
2170
- /* @__PURE__ */ jsx20("p", { children: texts.iosInstallBody }),
2171
- onInstallRequested && texts.iosInstallCta && /* @__PURE__ */ jsx20("button", { onClick: onInstallRequested, children: texts.iosInstallCta })
2302
+ /* @__PURE__ */ jsx21("h3", { children: texts.iosInstallTitle }),
2303
+ /* @__PURE__ */ jsx21("p", { children: texts.iosInstallBody }),
2304
+ onInstallRequested && texts.iosInstallCta && /* @__PURE__ */ jsx21("button", { onClick: onInstallRequested, children: texts.iosInstallCta })
2172
2305
  ] });
2173
2306
  }
2174
2307
  if (state.kind === "denied") {
2175
2308
  const recovery = platformRecoveryCopy(texts);
2176
2309
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", "aria-label": texts.deniedTitle, children: [
2177
- /* @__PURE__ */ jsx20("h3", { children: texts.deniedTitle }),
2178
- /* @__PURE__ */ jsx20("p", { children: texts.deniedBody }),
2179
- recovery && /* @__PURE__ */ jsx20("p", { "data-testid": "denied-recovery", children: recovery })
2310
+ /* @__PURE__ */ jsx21("h3", { children: texts.deniedTitle }),
2311
+ /* @__PURE__ */ jsx21("p", { children: texts.deniedBody }),
2312
+ recovery && /* @__PURE__ */ jsx21("p", { "data-testid": "denied-recovery", children: recovery })
2180
2313
  ] });
2181
2314
  }
2182
2315
  if (state.kind === "unsupported") {
2183
- return /* @__PURE__ */ jsx20("div", { className, role: "region", children: /* @__PURE__ */ jsx20("p", { children: texts.unsupportedBody }) });
2316
+ return /* @__PURE__ */ jsx21("div", { className, role: "region", children: /* @__PURE__ */ jsx21("p", { children: texts.unsupportedBody }) });
2184
2317
  }
2185
2318
  if (state.kind === "error") {
2186
- return /* @__PURE__ */ jsx20("div", { className, role: "region", "aria-label": "error", children: /* @__PURE__ */ jsx20("p", { children: state.message }) });
2319
+ return /* @__PURE__ */ jsx21("div", { className, role: "region", "aria-label": "error", children: /* @__PURE__ */ jsx21("p", { children: state.message }) });
2187
2320
  }
2188
2321
  return /* @__PURE__ */ jsxs15("div", { className, role: "region", children: [
2189
- /* @__PURE__ */ jsx20(
2322
+ /* @__PURE__ */ jsx21(
2190
2323
  "button",
2191
2324
  {
2192
2325
  type: "button",
@@ -2200,23 +2333,23 @@ function PushPrompt2({ texts, onSubscribed, onDeclined, onInstallRequested, clas
2200
2333
  children: texts.cta
2201
2334
  }
2202
2335
  ),
2203
- onDeclined && /* @__PURE__ */ jsx20("button", { type: "button", onClick: onDeclined, children: texts.declineCta })
2336
+ onDeclined && /* @__PURE__ */ jsx21("button", { type: "button", onClick: onDeclined, children: texts.declineCta })
2204
2337
  ] });
2205
2338
  }
2206
2339
 
2207
2340
  // src/defaults/LoadingState.tsx
2208
- import { jsx as jsx21 } from "react/jsx-runtime";
2341
+ import { jsx as jsx22 } from "react/jsx-runtime";
2209
2342
  function LoadingState({ message }) {
2210
- return /* @__PURE__ */ jsx21("div", { role: "status", "aria-live": "polite", style: { padding: 24, textAlign: "center" }, children: /* @__PURE__ */ jsx21("span", { children: message ?? "Carregando..." }) });
2343
+ return /* @__PURE__ */ jsx22("div", { role: "status", "aria-live": "polite", style: { padding: 24, textAlign: "center" }, children: /* @__PURE__ */ jsx22("span", { children: message ?? "Carregando..." }) });
2211
2344
  }
2212
2345
 
2213
2346
  // src/defaults/EmptyState.tsx
2214
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
2347
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
2215
2348
  function EmptyState({ title, description, action }) {
2216
2349
  return /* @__PURE__ */ jsxs16("div", { role: "status", style: { padding: 32, textAlign: "center" }, children: [
2217
- /* @__PURE__ */ jsx22("h2", { style: { marginBottom: 8 }, children: title }),
2218
- description && /* @__PURE__ */ jsx22("p", { style: { opacity: 0.7 }, children: description }),
2219
- action && /* @__PURE__ */ jsx22("div", { style: { marginTop: 16 }, children: action })
2350
+ /* @__PURE__ */ jsx23("h2", { style: { marginBottom: 8 }, children: title }),
2351
+ description && /* @__PURE__ */ jsx23("p", { style: { opacity: 0.7 }, children: description }),
2352
+ action && /* @__PURE__ */ jsx23("div", { style: { marginTop: 16 }, children: action })
2220
2353
  ] });
2221
2354
  }
2222
2355
 
@@ -2262,18 +2395,24 @@ function useLoginForm() {
2262
2395
  const [password, setPassword] = useState7("");
2263
2396
  const [submitting, setSubmitting] = useState7(false);
2264
2397
  const [error, setError] = useState7(null);
2265
- const emailError = useMemo4(() => {
2398
+ const [touchedEmail, setTouchedEmail] = useState7(false);
2399
+ const [touchedPassword, setTouchedPassword] = useState7(false);
2400
+ const [formSubmitAttempted, setFormSubmitAttempted] = useState7(false);
2401
+ const validateEmail = useMemo4(() => {
2266
2402
  if (email.length === 0) return null;
2267
2403
  if (!EMAIL_RE.test(email)) return "Formato de e-mail inv\xE1lido.";
2268
2404
  return null;
2269
2405
  }, [email]);
2270
- const passwordError = useMemo4(() => {
2406
+ const validatePassword = useMemo4(() => {
2271
2407
  if (password.length === 0) return null;
2272
2408
  if (password.length < MIN_PASSWORD) return `M\xEDnimo de ${MIN_PASSWORD} caracteres.`;
2273
2409
  return null;
2274
2410
  }, [password]);
2275
- const canSubmit = email.length > 0 && password.length >= MIN_PASSWORD && emailError === null && passwordError === null && !submitting;
2411
+ const emailError = touchedEmail || formSubmitAttempted ? validateEmail : null;
2412
+ const passwordError = touchedPassword || formSubmitAttempted ? validatePassword : null;
2413
+ const canSubmit = email.length > 0 && password.length >= MIN_PASSWORD && validateEmail === null && validatePassword === null && !submitting;
2276
2414
  const submit = useCallback5(async () => {
2415
+ setFormSubmitAttempted(true);
2277
2416
  if (!canSubmit) return false;
2278
2417
  setSubmitting(true);
2279
2418
  setError(null);
@@ -2291,9 +2430,12 @@ function useLoginForm() {
2291
2430
  email,
2292
2431
  setEmail,
2293
2432
  emailError,
2433
+ markEmailTouched: () => setTouchedEmail(true),
2294
2434
  password,
2295
2435
  setPassword,
2296
2436
  passwordError,
2437
+ markPasswordTouched: () => setTouchedPassword(true),
2438
+ formSubmitAttempted,
2297
2439
  submit,
2298
2440
  submitting,
2299
2441
  canSubmit,
@@ -2314,23 +2456,31 @@ function useSignupForm() {
2314
2456
  const [password, setPassword] = useState8("");
2315
2457
  const [submitting, setSubmitting] = useState8(false);
2316
2458
  const [error, setError] = useState8(null);
2317
- const nameError = useMemo5(() => {
2459
+ const [touchedName, setTouchedName] = useState8(false);
2460
+ const [touchedEmail, setTouchedEmail] = useState8(false);
2461
+ const [touchedPassword, setTouchedPassword] = useState8(false);
2462
+ const [formSubmitAttempted, setFormSubmitAttempted] = useState8(false);
2463
+ const validateName = useMemo5(() => {
2318
2464
  if (name.length === 0) return null;
2319
2465
  if (name.trim().length < 2) return "Nome muito curto.";
2320
2466
  return null;
2321
2467
  }, [name]);
2322
- const emailError = useMemo5(() => {
2468
+ const validateEmail = useMemo5(() => {
2323
2469
  if (email.length === 0) return null;
2324
2470
  if (!EMAIL_RE2.test(email)) return "Formato de e-mail inv\xE1lido.";
2325
2471
  return null;
2326
2472
  }, [email]);
2327
- const passwordError = useMemo5(() => {
2473
+ const validatePassword = useMemo5(() => {
2328
2474
  if (password.length === 0) return null;
2329
2475
  if (password.length < MIN_PASSWORD2) return `M\xEDnimo de ${MIN_PASSWORD2} caracteres.`;
2330
2476
  return null;
2331
2477
  }, [password]);
2332
- const canSubmit = name.trim().length >= 2 && email.length > 0 && password.length >= MIN_PASSWORD2 && nameError === null && emailError === null && passwordError === null && !submitting;
2478
+ const nameError = touchedName || formSubmitAttempted ? validateName : null;
2479
+ const emailError = touchedEmail || formSubmitAttempted ? validateEmail : null;
2480
+ const passwordError = touchedPassword || formSubmitAttempted ? validatePassword : null;
2481
+ const canSubmit = name.trim().length >= 2 && email.length > 0 && password.length >= MIN_PASSWORD2 && validateName === null && validateEmail === null && validatePassword === null && !submitting;
2333
2482
  const submit = useCallback6(async () => {
2483
+ setFormSubmitAttempted(true);
2334
2484
  if (!canSubmit) return false;
2335
2485
  setSubmitting(true);
2336
2486
  setError(null);
@@ -2348,12 +2498,16 @@ function useSignupForm() {
2348
2498
  name,
2349
2499
  setName,
2350
2500
  nameError,
2501
+ markNameTouched: () => setTouchedName(true),
2351
2502
  email,
2352
2503
  setEmail,
2353
2504
  emailError,
2505
+ markEmailTouched: () => setTouchedEmail(true),
2354
2506
  password,
2355
2507
  setPassword,
2356
2508
  passwordError,
2509
+ markPasswordTouched: () => setTouchedPassword(true),
2510
+ formSubmitAttempted,
2357
2511
  submit,
2358
2512
  submitting,
2359
2513
  canSubmit,
@@ -2372,13 +2526,17 @@ function useForgotForm() {
2372
2526
  const [submitting, setSubmitting] = useState9(false);
2373
2527
  const [sent, setSent] = useState9(false);
2374
2528
  const [error, setError] = useState9(null);
2375
- const emailError = useMemo6(() => {
2529
+ const [touchedEmail, setTouchedEmail] = useState9(false);
2530
+ const [formSubmitAttempted, setFormSubmitAttempted] = useState9(false);
2531
+ const validateEmail = useMemo6(() => {
2376
2532
  if (email.length === 0) return null;
2377
2533
  if (!EMAIL_RE3.test(email)) return "Formato de e-mail inv\xE1lido.";
2378
2534
  return null;
2379
2535
  }, [email]);
2380
- const canSubmit = email.length > 0 && emailError === null && !submitting;
2536
+ const emailError = touchedEmail || formSubmitAttempted ? validateEmail : null;
2537
+ const canSubmit = email.length > 0 && validateEmail === null && !submitting;
2381
2538
  const submit = useCallback7(async () => {
2539
+ setFormSubmitAttempted(true);
2382
2540
  if (!canSubmit) return false;
2383
2541
  setSubmitting(true);
2384
2542
  setError(null);
@@ -2397,6 +2555,8 @@ function useForgotForm() {
2397
2555
  email,
2398
2556
  setEmail,
2399
2557
  emailError,
2558
+ markEmailTouched: () => setTouchedEmail(true),
2559
+ formSubmitAttempted,
2400
2560
  submit,
2401
2561
  submitting,
2402
2562
  canSubmit,
@@ -2417,24 +2577,30 @@ function useResetForm() {
2417
2577
  const [submitting, setSubmitting] = useState10(false);
2418
2578
  const [done, setDone] = useState10(false);
2419
2579
  const [error, setError] = useState10(null);
2580
+ const [touchedPassword, setTouchedPassword] = useState10(false);
2581
+ const [touchedConfirm, setTouchedConfirm] = useState10(false);
2582
+ const [formSubmitAttempted, setFormSubmitAttempted] = useState10(false);
2420
2583
  useEffect8(() => {
2421
2584
  if (typeof window === "undefined") return;
2422
2585
  const params = new URLSearchParams(window.location.search);
2423
2586
  const t = params.get("token");
2424
2587
  setToken(t && t.length > 0 ? t : null);
2425
2588
  }, []);
2426
- const passwordError = useMemo7(() => {
2589
+ const validatePassword = useMemo7(() => {
2427
2590
  if (password.length === 0) return null;
2428
2591
  if (password.length < MIN_PASSWORD3) return `M\xEDnimo de ${MIN_PASSWORD3} caracteres.`;
2429
2592
  return null;
2430
2593
  }, [password]);
2431
- const confirmError = useMemo7(() => {
2594
+ const validateConfirm = useMemo7(() => {
2432
2595
  if (confirm.length === 0) return null;
2433
2596
  if (confirm !== password) return "Senhas n\xE3o coincidem.";
2434
2597
  return null;
2435
2598
  }, [confirm, password]);
2436
- const canSubmit = token !== null && password.length >= MIN_PASSWORD3 && confirm === password && passwordError === null && confirmError === null && !submitting && !done;
2599
+ const passwordError = touchedPassword || formSubmitAttempted ? validatePassword : null;
2600
+ const confirmError = touchedConfirm || formSubmitAttempted ? validateConfirm : null;
2601
+ const canSubmit = token !== null && password.length >= MIN_PASSWORD3 && confirm === password && validatePassword === null && validateConfirm === null && !submitting && !done;
2437
2602
  const submit = useCallback8(async () => {
2603
+ setFormSubmitAttempted(true);
2438
2604
  if (!canSubmit || token === null) return;
2439
2605
  setSubmitting(true);
2440
2606
  setError(null);
@@ -2458,9 +2624,12 @@ function useResetForm() {
2458
2624
  password,
2459
2625
  setPassword,
2460
2626
  passwordError,
2627
+ markPasswordTouched: () => setTouchedPassword(true),
2461
2628
  confirm,
2462
2629
  setConfirm,
2463
2630
  confirmError,
2631
+ markConfirmTouched: () => setTouchedConfirm(true),
2632
+ formSubmitAttempted,
2464
2633
  submit,
2465
2634
  submitting,
2466
2635
  canSubmit,
@@ -2607,20 +2776,20 @@ function useToast() {
2607
2776
 
2608
2777
  // src/RouteBoundary.tsx
2609
2778
  import { Routes as Routes2, Route as Route2 } from "react-router-dom";
2610
- import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
2779
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
2611
2780
  function RouteBoundary({ children }) {
2612
2781
  return /* @__PURE__ */ jsxs17(Routes2, { children: [
2613
2782
  children,
2614
- /* @__PURE__ */ jsx23(Route2, { path: "*", element: /* @__PURE__ */ jsx23(DefaultNotFound, {}) })
2783
+ /* @__PURE__ */ jsx24(Route2, { path: "*", element: /* @__PURE__ */ jsx24(DefaultNotFound, {}) })
2615
2784
  ] });
2616
2785
  }
2617
2786
  function DefaultNotFound() {
2618
- return /* @__PURE__ */ jsx23("div", { role: "alert", children: "P\xE1gina n\xE3o encontrada" });
2787
+ return /* @__PURE__ */ jsx24("div", { role: "alert", children: "P\xE1gina n\xE3o encontrada" });
2619
2788
  }
2620
2789
 
2621
2790
  // src/PreAuthShell.tsx
2622
2791
  import { BrowserRouter as BrowserRouter2, MemoryRouter as MemoryRouter2, Routes as Routes3 } from "react-router-dom";
2623
- import { jsx as jsx24 } from "react/jsx-runtime";
2792
+ import { jsx as jsx25 } from "react/jsx-runtime";
2624
2793
  function PreAuthShell({
2625
2794
  basename,
2626
2795
  testRouter,
@@ -2628,9 +2797,9 @@ function PreAuthShell({
2628
2797
  children
2629
2798
  }) {
2630
2799
  if (testRouter === "memory") {
2631
- return /* @__PURE__ */ jsx24(MemoryRouter2, { basename, initialEntries: testInitialEntries, children: /* @__PURE__ */ jsx24(Routes3, { children }) });
2800
+ return /* @__PURE__ */ jsx25(MemoryRouter2, { basename, initialEntries: testInitialEntries, children: /* @__PURE__ */ jsx25(Routes3, { children }) });
2632
2801
  }
2633
- return /* @__PURE__ */ jsx24(BrowserRouter2, { basename, children: /* @__PURE__ */ jsx24(Routes3, { children }) });
2802
+ return /* @__PURE__ */ jsx25(BrowserRouter2, { basename, children: /* @__PURE__ */ jsx25(Routes3, { children }) });
2634
2803
  }
2635
2804
 
2636
2805
  // src/OnboardingFlow.tsx
@@ -2651,7 +2820,7 @@ function useOnboardingStep() {
2651
2820
  }
2652
2821
 
2653
2822
  // src/OnboardingFlow.tsx
2654
- import { jsx as jsx25 } from "react/jsx-runtime";
2823
+ import { jsx as jsx26 } from "react/jsx-runtime";
2655
2824
  var isFilled = (v) => v != null && v !== "";
2656
2825
  var CURRENT_STEP_FIELD = "currentStep";
2657
2826
  function readPersistedStepIdx(draft) {
@@ -2729,7 +2898,7 @@ function OnboardingFlow({
2729
2898
  `[hook-template] OnboardingFlow: missing screen component for step '${step.id}' (expected key '${step.screen}' in screens prop)`
2730
2899
  );
2731
2900
  }
2732
- return /* @__PURE__ */ jsx25(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx25(Screen, {}) });
2901
+ return /* @__PURE__ */ jsx26(OnboardingStepContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx26(Screen, {}) });
2733
2902
  }
2734
2903
 
2735
2904
  // src/hooks/useFeature.ts