@sudobility/building_blocks 0.0.54 → 0.0.62

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
@@ -7,6 +7,7 @@ import { ChevronDownIcon, CalendarDaysIcon, PaintBrushIcon, LanguageIcon, Chevro
7
7
  import { GRADIENT_CLASSES, textVariants } from "@sudobility/design";
8
8
  import { cva } from "class-variance-authority";
9
9
  import { SubscriptionLayout, SubscriptionTile, SegmentedControl } from "@sudobility/subscription-components";
10
+ import { useSubscriptionPeriods, useSubscriptionForPeriod, useSubscribable, useUserSubscription } from "@sudobility/subscription_lib";
10
11
  import { createUserWithEmailAndPassword, signInWithEmailAndPassword, GoogleAuthProvider, signInWithPopup } from "firebase/auth";
11
12
  function cn(...inputs) {
12
13
  return twMerge(clsx(inputs));
@@ -1259,13 +1260,12 @@ const GlobalSettingsPage = ({
1259
1260
  ) });
1260
1261
  };
1261
1262
  function AppSubscriptionsPage({
1262
- subscription,
1263
+ offerId,
1263
1264
  rateLimitsConfig,
1264
- subscriptionUserId,
1265
1265
  labels,
1266
1266
  formatters,
1267
- entitlementMap,
1268
- entitlementLevels: _entitlementLevels,
1267
+ onPurchase,
1268
+ onRestore,
1269
1269
  onPurchaseSuccess,
1270
1270
  onRestoreSuccess,
1271
1271
  onError,
@@ -1273,15 +1273,33 @@ function AppSubscriptionsPage({
1273
1273
  onTrack
1274
1274
  }) {
1275
1275
  const {
1276
- products,
1277
- currentSubscription,
1278
- isLoading,
1279
- error,
1280
- purchase,
1281
- restore,
1282
- clearError
1283
- } = subscription;
1284
- const [billingPeriod, setBillingPeriod] = useState("monthly");
1276
+ periods,
1277
+ isLoading: periodsLoading,
1278
+ error: periodsError
1279
+ } = useSubscriptionPeriods(offerId);
1280
+ const [selectedPeriod, setSelectedPeriod] = useState("monthly");
1281
+ useEffect(() => {
1282
+ if (periods.length > 0 && !periods.includes(selectedPeriod)) {
1283
+ setSelectedPeriod(periods[0]);
1284
+ }
1285
+ }, [periods, selectedPeriod]);
1286
+ const {
1287
+ packages,
1288
+ isLoading: packagesLoading,
1289
+ error: packagesError
1290
+ } = useSubscriptionForPeriod(offerId, selectedPeriod);
1291
+ const {
1292
+ subscribablePackageIds,
1293
+ isLoading: subscribableLoading,
1294
+ error: subscribableError
1295
+ } = useSubscribable(offerId);
1296
+ const {
1297
+ subscription: currentSubscription,
1298
+ isLoading: subscriptionLoading,
1299
+ error: subscriptionError
1300
+ } = useUserSubscription();
1301
+ const isLoading = periodsLoading || packagesLoading || subscribableLoading || subscriptionLoading;
1302
+ const error = periodsError || packagesError || subscribableError || subscriptionError;
1285
1303
  const [selectedPlan, setSelectedPlan] = useState(null);
1286
1304
  const [isPurchasing, setIsPurchasing] = useState(false);
1287
1305
  const [isRestoring, setIsRestoring] = useState(false);
@@ -1296,32 +1314,99 @@ function AppSubscriptionsPage({
1296
1314
  },
1297
1315
  [onTrack]
1298
1316
  );
1299
- useEffect(() => {
1300
- if (error) {
1301
- onError == null ? void 0 : onError(labels.errorTitle, error);
1302
- clearError();
1303
- }
1304
- }, [error, clearError, labels.errorTitle, onError]);
1305
- const filteredProducts = products.filter((product) => {
1306
- if (!product.period) return false;
1307
- const isYearly = product.period.includes("Y") || product.period.includes("year");
1308
- return billingPeriod === "yearly" ? isYearly : !isYearly;
1309
- }).sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
1310
1317
  const handlePeriodChange = useCallback(
1311
- (period) => {
1312
- setBillingPeriod(period);
1318
+ (value) => {
1319
+ const newPeriod = value;
1320
+ setSelectedPeriod(newPeriod);
1313
1321
  setSelectedPlan(null);
1314
- track("billing_period_changed", { billing_period: period });
1322
+ track("billing_period_changed", { billing_period: newPeriod });
1323
+ },
1324
+ [track]
1325
+ );
1326
+ const getPeriodLabel = useCallback(
1327
+ (period) => {
1328
+ switch (period) {
1329
+ case "yearly":
1330
+ return labels.periodYear;
1331
+ case "monthly":
1332
+ return labels.periodMonth;
1333
+ case "weekly":
1334
+ return labels.periodWeek;
1335
+ default:
1336
+ return period;
1337
+ }
1338
+ },
1339
+ [labels]
1340
+ );
1341
+ const getYearlySavingsPercent = useCallback(
1342
+ (yearlyPackage) => {
1343
+ if (!yearlyPackage.product) return void 0;
1344
+ const monthlyPackageId = yearlyPackage.packageId.replace(
1345
+ "_yearly",
1346
+ "_monthly"
1347
+ );
1348
+ const monthlyPkg = packages.find((p) => p.packageId === monthlyPackageId);
1349
+ if (!(monthlyPkg == null ? void 0 : monthlyPkg.product)) return void 0;
1350
+ const yearlyPrice = yearlyPackage.product.price;
1351
+ const monthlyPrice = monthlyPkg.product.price;
1352
+ if (monthlyPrice <= 0 || yearlyPrice <= 0) return void 0;
1353
+ const annualizedMonthly = monthlyPrice * 12;
1354
+ const savings = (annualizedMonthly - yearlyPrice) / annualizedMonthly * 100;
1355
+ return Math.round(savings);
1356
+ },
1357
+ [packages]
1358
+ );
1359
+ const billingPeriodOptions = useMemo(() => {
1360
+ return periods.map((period) => ({
1361
+ value: period,
1362
+ label: period === "monthly" ? labels.billingMonthly : labels.billingYearly
1363
+ }));
1364
+ }, [periods, labels]);
1365
+ const isCurrentPlan = useCallback(
1366
+ (packageId, productId) => {
1367
+ if (!(currentSubscription == null ? void 0 : currentSubscription.isActive)) return false;
1368
+ return productId === currentSubscription.productId || packageId === currentSubscription.packageId;
1369
+ },
1370
+ [currentSubscription]
1371
+ );
1372
+ const isPackageEnabled = useCallback(
1373
+ (packageId) => {
1374
+ if (subscribableLoading || subscribablePackageIds.length === 0)
1375
+ return true;
1376
+ return subscribablePackageIds.includes(packageId);
1377
+ },
1378
+ [subscribableLoading, subscribablePackageIds]
1379
+ );
1380
+ const canUpgradeTo = useCallback(
1381
+ (packageId, productId) => {
1382
+ return isPackageEnabled(packageId) && !isCurrentPlan(packageId, productId);
1383
+ },
1384
+ [isPackageEnabled, isCurrentPlan]
1385
+ );
1386
+ useEffect(() => {
1387
+ if ((currentSubscription == null ? void 0 : currentSubscription.isActive) && currentSubscription.packageId) {
1388
+ setSelectedPlan(currentSubscription.packageId);
1389
+ if (currentSubscription.period && periods.includes(currentSubscription.period)) {
1390
+ setSelectedPeriod(currentSubscription.period);
1391
+ }
1392
+ }
1393
+ }, [currentSubscription, periods]);
1394
+ const handlePlanSelect = useCallback(
1395
+ (planIdentifier) => {
1396
+ setSelectedPlan(planIdentifier);
1397
+ track("plan_selected", {
1398
+ plan_identifier: planIdentifier ?? "free",
1399
+ is_free_tier: planIdentifier === null
1400
+ });
1315
1401
  },
1316
1402
  [track]
1317
1403
  );
1318
1404
  const handlePurchase = useCallback(async () => {
1319
1405
  if (!selectedPlan) return;
1320
1406
  setIsPurchasing(true);
1321
- clearError();
1322
1407
  track("purchase_initiated", { plan_identifier: selectedPlan });
1323
1408
  try {
1324
- const result = await purchase(selectedPlan, subscriptionUserId);
1409
+ const result = await onPurchase(selectedPlan);
1325
1410
  if (result) {
1326
1411
  track("purchase_completed", { plan_identifier: selectedPlan });
1327
1412
  onPurchaseSuccess == null ? void 0 : onPurchaseSuccess();
@@ -1344,31 +1429,18 @@ function AppSubscriptionsPage({
1344
1429
  }
1345
1430
  }, [
1346
1431
  selectedPlan,
1347
- clearError,
1348
1432
  track,
1349
- purchase,
1350
- subscriptionUserId,
1433
+ onPurchase,
1351
1434
  onPurchaseSuccess,
1352
1435
  labels.errorTitle,
1353
1436
  labels.purchaseError,
1354
1437
  onError
1355
1438
  ]);
1356
- const handlePlanSelect = useCallback(
1357
- (planIdentifier) => {
1358
- setSelectedPlan(planIdentifier);
1359
- track("plan_selected", {
1360
- plan_identifier: planIdentifier ?? "free",
1361
- is_free_tier: planIdentifier === null
1362
- });
1363
- },
1364
- [track]
1365
- );
1366
1439
  const handleRestore = useCallback(async () => {
1367
1440
  setIsRestoring(true);
1368
- clearError();
1369
1441
  track("restore_initiated");
1370
1442
  try {
1371
- const result = await restore(subscriptionUserId);
1443
+ const result = await onRestore();
1372
1444
  if (result) {
1373
1445
  track("restore_completed");
1374
1446
  onRestoreSuccess == null ? void 0 : onRestoreSuccess();
@@ -1384,10 +1456,8 @@ function AppSubscriptionsPage({
1384
1456
  setIsRestoring(false);
1385
1457
  }
1386
1458
  }, [
1387
- clearError,
1388
1459
  track,
1389
- restore,
1390
- subscriptionUserId,
1460
+ onRestore,
1391
1461
  onRestoreSuccess,
1392
1462
  labels.errorTitle,
1393
1463
  labels.restoreNoPurchases,
@@ -1403,32 +1473,20 @@ function AppSubscriptionsPage({
1403
1473
  day: "numeric"
1404
1474
  }).format(date);
1405
1475
  }, []);
1406
- const getPeriodLabel = useCallback(
1407
- (period) => {
1408
- if (!period) return "";
1409
- if (period.includes("Y") || period.includes("year"))
1410
- return labels.periodYear;
1411
- if (period.includes("M") || period.includes("month"))
1412
- return labels.periodMonth;
1413
- if (period.includes("W") || period.includes("week"))
1414
- return labels.periodWeek;
1415
- return "";
1416
- },
1417
- [labels]
1418
- );
1419
- const getTrialLabel = useCallback(
1420
- (trialPeriod) => {
1421
- if (!trialPeriod) return void 0;
1422
- const num = parseInt(trialPeriod.replace(/\D/g, "") || "1", 10);
1423
- if (trialPeriod.includes("W")) {
1424
- return formatters.formatTrialWeeks(num);
1425
- }
1426
- if (trialPeriod.includes("M")) {
1427
- return formatters.formatTrialMonths(num);
1428
- }
1429
- return formatters.formatTrialDays(num);
1476
+ const formatProductName = useCallback(
1477
+ (packageId, productId) => {
1478
+ if (!packageId && !productId) return labels.labelPremium;
1479
+ const pkg = packages.find(
1480
+ (p) => {
1481
+ var _a;
1482
+ return p.packageId === packageId || ((_a = p.product) == null ? void 0 : _a.productId) === productId;
1483
+ }
1484
+ );
1485
+ if (pkg == null ? void 0 : pkg.name) return pkg.name;
1486
+ const identifier = packageId || productId || "";
1487
+ return identifier.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
1430
1488
  },
1431
- [formatters]
1489
+ [packages, labels.labelPremium]
1432
1490
  );
1433
1491
  const formatRateLimit = useCallback(
1434
1492
  (limit) => {
@@ -1437,50 +1495,13 @@ function AppSubscriptionsPage({
1437
1495
  },
1438
1496
  [labels.unlimited]
1439
1497
  );
1440
- const getProductFeatures = useCallback(
1441
- (packageId) => {
1442
- return formatters.getProductFeatures(packageId);
1443
- },
1444
- [formatters]
1445
- );
1446
- const getFreeTierFeatures = useCallback(() => {
1447
- return labels.freeTierFeatures;
1448
- }, [labels.freeTierFeatures]);
1449
- const getYearlySavingsPercent = useCallback(
1450
- (yearlyPackageId) => {
1451
- var _a;
1452
- const yearlyEntitlement = entitlementMap[yearlyPackageId];
1453
- if (!yearlyEntitlement) return void 0;
1454
- const yearlyProduct = products.find(
1455
- (p) => p.identifier === yearlyPackageId
1456
- );
1457
- if (!yearlyProduct) return void 0;
1458
- const monthlyPackageId = (_a = Object.entries(entitlementMap).find(
1459
- ([pkgId, ent]) => ent === yearlyEntitlement && pkgId.includes("monthly")
1460
- )) == null ? void 0 : _a[0];
1461
- if (!monthlyPackageId) return void 0;
1462
- const monthlyProduct = products.find(
1463
- (p) => p.identifier === monthlyPackageId
1464
- );
1465
- if (!monthlyProduct) return void 0;
1466
- const yearlyPrice = parseFloat(yearlyProduct.price);
1467
- const monthlyPrice = parseFloat(monthlyProduct.price);
1468
- if (monthlyPrice <= 0 || yearlyPrice <= 0) return void 0;
1469
- const annualizedMonthly = monthlyPrice * 12;
1470
- const savings = (annualizedMonthly - yearlyPrice) / annualizedMonthly * 100;
1471
- return Math.round(savings);
1472
- },
1473
- [products, entitlementMap]
1474
- );
1475
- const billingPeriodOptions = [
1476
- { value: "monthly", label: labels.billingMonthly },
1477
- { value: "yearly", label: labels.billingYearly }
1478
- ];
1498
+ const freeTierPackage = packages.find((p) => !p.product);
1499
+ const paidPackages = packages.filter((p) => p.product);
1479
1500
  return /* @__PURE__ */ jsx(
1480
1501
  SubscriptionLayout,
1481
1502
  {
1482
1503
  title: labels.title,
1483
- error,
1504
+ error: error == null ? void 0 : error.message,
1484
1505
  currentStatusLabel: labels.currentStatusLabel,
1485
1506
  currentStatus: {
1486
1507
  isActive: (currentSubscription == null ? void 0 : currentSubscription.isActive) ?? false,
@@ -1489,7 +1510,10 @@ function AppSubscriptionsPage({
1489
1510
  fields: [
1490
1511
  {
1491
1512
  label: labels.labelPlan,
1492
- value: currentSubscription.productIdentifier || labels.labelPremium
1513
+ value: formatProductName(
1514
+ currentSubscription.packageId,
1515
+ currentSubscription.productId
1516
+ )
1493
1517
  },
1494
1518
  {
1495
1519
  label: labels.labelExpires,
@@ -1518,11 +1542,11 @@ function AppSubscriptionsPage({
1518
1542
  message: labels.statusInactiveMessage
1519
1543
  } : void 0
1520
1544
  },
1521
- aboveProducts: !isLoading && products.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-6", children: /* @__PURE__ */ jsx(
1545
+ aboveProducts: !isLoading && periods.length > 1 ? /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-6", children: /* @__PURE__ */ jsx(
1522
1546
  SegmentedControl,
1523
1547
  {
1524
1548
  options: billingPeriodOptions,
1525
- value: billingPeriod,
1549
+ value: selectedPeriod,
1526
1550
  onChange: handlePeriodChange
1527
1551
  }
1528
1552
  ) }) : null,
@@ -1538,52 +1562,81 @@ function AppSubscriptionsPage({
1538
1562
  disabled: isPurchasing || isRestoring,
1539
1563
  loading: isRestoring
1540
1564
  },
1541
- children: isLoading ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }) }) : products.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center py-12 text-theme-text-secondary", children: labels.noProducts }) : filteredProducts.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center py-12 text-theme-text-secondary", children: labels.noProductsForPeriod }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1542
- /* @__PURE__ */ jsx(
1565
+ children: isLoading ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }) }) : error ? /* @__PURE__ */ jsx("div", { className: "text-center py-12 text-red-500", children: error.message }) : packages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center py-12 text-theme-text-secondary", children: labels.noProducts }) : paidPackages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center py-12 text-theme-text-secondary", children: labels.noProductsForPeriod }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1566
+ freeTierPackage && !(currentSubscription == null ? void 0 : currentSubscription.isActive) && /* @__PURE__ */ jsx(
1543
1567
  SubscriptionTile,
1544
1568
  {
1545
1569
  id: "free",
1546
1570
  title: labels.freeTierTitle,
1547
1571
  price: labels.freeTierPrice,
1548
1572
  periodLabel: labels.periodMonth,
1549
- features: getFreeTierFeatures(),
1550
- isSelected: !(currentSubscription == null ? void 0 : currentSubscription.isActive) && selectedPlan === null,
1573
+ features: labels.freeTierFeatures,
1574
+ isSelected: selectedPlan === null,
1575
+ isCurrentPlan: !(currentSubscription == null ? void 0 : currentSubscription.isActive),
1551
1576
  onSelect: () => handlePlanSelect(null),
1552
- topBadge: !(currentSubscription == null ? void 0 : currentSubscription.isActive) ? {
1577
+ enabled: isPackageEnabled("free"),
1578
+ topBadge: {
1553
1579
  text: labels.currentPlanBadge,
1554
1580
  color: "green"
1555
- } : void 0,
1581
+ },
1556
1582
  disabled: isPurchasing || isRestoring,
1557
1583
  hideSelectionIndicator: true
1558
1584
  },
1559
1585
  "free"
1560
1586
  ),
1561
- filteredProducts.map((product) => {
1562
- var _a;
1587
+ paidPackages.map((pkg) => {
1588
+ var _a, _b, _c, _d, _e, _f;
1589
+ const isCurrent = isCurrentPlan(
1590
+ pkg.packageId,
1591
+ (_a = pkg.product) == null ? void 0 : _a.productId
1592
+ );
1593
+ const isEnabled = isPackageEnabled(pkg.packageId);
1594
+ const canUpgrade = canUpgradeTo(
1595
+ pkg.packageId,
1596
+ (_b = pkg.product) == null ? void 0 : _b.productId
1597
+ );
1598
+ const canSelect = canUpgrade || isCurrent;
1563
1599
  return /* @__PURE__ */ jsx(
1564
1600
  SubscriptionTile,
1565
1601
  {
1566
- id: product.identifier,
1567
- title: product.title,
1568
- price: product.priceString,
1569
- periodLabel: getPeriodLabel(product.period),
1570
- features: getProductFeatures(product.identifier),
1571
- isSelected: selectedPlan === product.identifier,
1572
- onSelect: () => handlePlanSelect(product.identifier),
1573
- isBestValue: product.identifier.includes("pro"),
1574
- discountBadge: ((_a = product.period) == null ? void 0 : _a.includes("Y")) ? (() => {
1575
- const savings = getYearlySavingsPercent(
1576
- product.identifier
1577
- );
1602
+ id: pkg.packageId,
1603
+ title: pkg.name,
1604
+ price: ((_c = pkg.product) == null ? void 0 : _c.priceString) ?? "$0",
1605
+ periodLabel: getPeriodLabel(((_d = pkg.product) == null ? void 0 : _d.period) ?? "monthly"),
1606
+ features: formatters.getProductFeatures(pkg.packageId),
1607
+ isSelected: selectedPlan === pkg.packageId,
1608
+ isCurrentPlan: isCurrent,
1609
+ onSelect: () => canSelect && handlePlanSelect(pkg.packageId),
1610
+ enabled: isEnabled,
1611
+ isBestValue: pkg.packageId.includes("pro"),
1612
+ topBadge: isCurrent ? {
1613
+ text: labels.currentPlanBadge,
1614
+ color: "green"
1615
+ } : void 0,
1616
+ discountBadge: selectedPeriod === "yearly" ? (() => {
1617
+ const savings = getYearlySavingsPercent(pkg);
1578
1618
  return savings && savings > 0 ? {
1579
1619
  text: formatters.formatSavePercent(savings),
1580
1620
  isBestValue: true
1581
1621
  } : void 0;
1582
1622
  })() : void 0,
1583
- introPriceNote: product.freeTrialPeriod ? getTrialLabel(product.freeTrialPeriod) : product.introPrice ? formatters.formatIntroNote(product.introPrice) : void 0,
1584
- disabled: isPurchasing || isRestoring
1623
+ introPriceNote: ((_e = pkg.product) == null ? void 0 : _e.trialPeriod) ? (() => {
1624
+ var _a2;
1625
+ const period = (_a2 = pkg.product) == null ? void 0 : _a2.trialPeriod;
1626
+ if (!period) return void 0;
1627
+ const num = parseInt(
1628
+ period.replace(/\D/g, "") || "1",
1629
+ 10
1630
+ );
1631
+ if (period.includes("W"))
1632
+ return formatters.formatTrialWeeks(num);
1633
+ if (period.includes("M"))
1634
+ return formatters.formatTrialMonths(num);
1635
+ return formatters.formatTrialDays(num);
1636
+ })() : ((_f = pkg.product) == null ? void 0 : _f.introPrice) ? formatters.formatIntroNote(pkg.product.introPrice) : void 0,
1637
+ disabled: isPurchasing || isRestoring || !canSelect
1585
1638
  },
1586
- product.identifier
1639
+ pkg.packageId
1587
1640
  );
1588
1641
  })
1589
1642
  ] })
@@ -1591,21 +1644,46 @@ function AppSubscriptionsPage({
1591
1644
  );
1592
1645
  }
1593
1646
  function AppPricingPage({
1594
- products,
1595
1647
  isAuthenticated,
1596
- hasActiveSubscription,
1597
- currentProductIdentifier,
1598
1648
  labels,
1599
1649
  formatters,
1600
- entitlementMap,
1601
- entitlementLevels,
1602
1650
  onPlanClick,
1603
1651
  onFreePlanClick,
1604
1652
  faqItems,
1605
1653
  className,
1606
- onTrack
1654
+ onTrack,
1655
+ offerId
1607
1656
  }) {
1608
- const [billingPeriod, setBillingPeriod] = useState("monthly");
1657
+ const {
1658
+ subscription: currentSubscription,
1659
+ isLoading: subscriptionLoading,
1660
+ error: subscriptionError
1661
+ } = useUserSubscription();
1662
+ const hasActiveSubscription = (currentSubscription == null ? void 0 : currentSubscription.isActive) ?? false;
1663
+ const currentProductIdentifier = currentSubscription == null ? void 0 : currentSubscription.productId;
1664
+ const {
1665
+ periods,
1666
+ isLoading: periodsLoading,
1667
+ error: periodsError
1668
+ } = useSubscriptionPeriods(offerId);
1669
+ const [selectedPeriod, setSelectedPeriod] = useState("monthly");
1670
+ useEffect(() => {
1671
+ if (periods.length > 0 && !periods.includes(selectedPeriod)) {
1672
+ setSelectedPeriod(periods[0]);
1673
+ }
1674
+ }, [periods, selectedPeriod]);
1675
+ const {
1676
+ packages,
1677
+ isLoading: packagesLoading,
1678
+ error: packagesError
1679
+ } = useSubscriptionForPeriod(offerId, selectedPeriod);
1680
+ const {
1681
+ subscribablePackageIds,
1682
+ isLoading: subscribableLoading,
1683
+ error: subscribableError
1684
+ } = useSubscribable(offerId);
1685
+ const isLoading = periodsLoading || packagesLoading || subscribableLoading || subscriptionLoading;
1686
+ const error = periodsError || packagesError || subscribableError || subscriptionError;
1609
1687
  const track = useCallback(
1610
1688
  (label, params) => {
1611
1689
  onTrack == null ? void 0 : onTrack({
@@ -1620,7 +1698,7 @@ function AppPricingPage({
1620
1698
  const handleBillingPeriodChange = useCallback(
1621
1699
  (value) => {
1622
1700
  const newPeriod = value;
1623
- setBillingPeriod(newPeriod);
1701
+ setSelectedPeriod(newPeriod);
1624
1702
  track("billing_period_changed", { billing_period: newPeriod });
1625
1703
  },
1626
1704
  [track]
@@ -1639,93 +1717,87 @@ function AppPricingPage({
1639
1717
  },
1640
1718
  [track, onPlanClick]
1641
1719
  );
1642
- const getProductLevel = useCallback(
1643
- (productId) => {
1644
- const entitlement = entitlementMap[productId];
1645
- if (!entitlement) return 0;
1646
- return entitlementLevels[entitlement] ?? 0;
1647
- },
1648
- [entitlementMap, entitlementLevels]
1649
- );
1650
- const currentLevel = currentProductIdentifier ? getProductLevel(currentProductIdentifier) : 0;
1651
- const filteredProducts = products.filter((product) => {
1652
- if (!product.period) return false;
1653
- const isYearly = product.period.includes("Y") || product.period.includes("year");
1654
- return billingPeriod === "yearly" ? isYearly : !isYearly;
1655
- }).sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
1656
1720
  const getPeriodLabel = useCallback(
1657
1721
  (period) => {
1658
- if (!period) return "";
1659
- if (period.includes("Y") || period.includes("year"))
1660
- return labels.periodYear;
1661
- if (period.includes("M") || period.includes("month"))
1662
- return labels.periodMonth;
1663
- if (period.includes("W") || period.includes("week"))
1664
- return labels.periodWeek;
1665
- return "";
1722
+ switch (period) {
1723
+ case "yearly":
1724
+ return labels.periodYear;
1725
+ case "monthly":
1726
+ return labels.periodMonth;
1727
+ case "weekly":
1728
+ return labels.periodWeek;
1729
+ default:
1730
+ return period;
1731
+ }
1666
1732
  },
1667
1733
  [labels]
1668
1734
  );
1669
1735
  const getYearlySavingsPercent = useCallback(
1670
- (yearlyPackageId) => {
1671
- var _a;
1672
- const yearlyEntitlement = entitlementMap[yearlyPackageId];
1673
- if (!yearlyEntitlement) return void 0;
1674
- const yearlyProduct = products.find(
1675
- (p) => p.identifier === yearlyPackageId
1736
+ (yearlyPackage) => {
1737
+ if (!yearlyPackage.product) return void 0;
1738
+ const monthlyPackageId = yearlyPackage.packageId.replace(
1739
+ "_yearly",
1740
+ "_monthly"
1676
1741
  );
1677
- if (!yearlyProduct) return void 0;
1678
- const monthlyPackageId = (_a = Object.entries(entitlementMap).find(
1679
- ([pkgId, ent]) => ent === yearlyEntitlement && pkgId.includes("monthly")
1680
- )) == null ? void 0 : _a[0];
1681
- if (!monthlyPackageId) return void 0;
1682
- const monthlyProduct = products.find(
1683
- (p) => p.identifier === monthlyPackageId
1684
- );
1685
- if (!monthlyProduct) return void 0;
1686
- const yearlyPrice = parseFloat(yearlyProduct.price);
1687
- const monthlyPrice = parseFloat(monthlyProduct.price);
1742
+ const monthlyPkg = packages.find((p) => p.packageId === monthlyPackageId);
1743
+ if (!(monthlyPkg == null ? void 0 : monthlyPkg.product)) return void 0;
1744
+ const yearlyPrice = yearlyPackage.product.price;
1745
+ const monthlyPrice = monthlyPkg.product.price;
1688
1746
  if (monthlyPrice <= 0 || yearlyPrice <= 0) return void 0;
1689
1747
  const annualizedMonthly = monthlyPrice * 12;
1690
1748
  const savings = (annualizedMonthly - yearlyPrice) / annualizedMonthly * 100;
1691
1749
  return Math.round(savings);
1692
1750
  },
1693
- [products, entitlementMap]
1751
+ [packages]
1694
1752
  );
1695
- const billingPeriodOptions = [
1696
- { value: "monthly", label: labels.billingMonthly },
1697
- { value: "yearly", label: labels.billingYearly }
1698
- ];
1753
+ const billingPeriodOptions = useMemo(() => {
1754
+ return periods.map((period) => ({
1755
+ value: period,
1756
+ label: period === "monthly" ? labels.billingMonthly : labels.billingYearly
1757
+ }));
1758
+ }, [periods, labels]);
1699
1759
  const isCurrentPlan = useCallback(
1700
- (productId) => {
1760
+ (packageId, productId) => {
1701
1761
  if (!isAuthenticated) return false;
1702
1762
  if (!hasActiveSubscription) return false;
1703
- return getProductLevel(productId) === currentLevel && currentLevel > 0;
1763
+ return productId === currentProductIdentifier || packageId === currentProductIdentifier;
1704
1764
  },
1705
- [isAuthenticated, hasActiveSubscription, getProductLevel, currentLevel]
1765
+ [isAuthenticated, hasActiveSubscription, currentProductIdentifier]
1706
1766
  );
1707
- const isUpgrade = useCallback(
1708
- (productId) => {
1709
- const productLevel = getProductLevel(productId);
1710
- return productLevel > currentLevel;
1767
+ const isPackageEnabled = useCallback(
1768
+ (packageId) => {
1769
+ if (!isAuthenticated) return true;
1770
+ if (subscribableLoading || subscribablePackageIds.length === 0)
1771
+ return true;
1772
+ return subscribablePackageIds.includes(packageId);
1711
1773
  },
1712
- [getProductLevel, currentLevel]
1774
+ [isAuthenticated, subscribableLoading, subscribablePackageIds]
1713
1775
  );
1776
+ const canUpgradeTo = useCallback(
1777
+ (packageId, productId) => {
1778
+ return isPackageEnabled(packageId) && !isCurrentPlan(packageId, productId);
1779
+ },
1780
+ [isPackageEnabled, isCurrentPlan]
1781
+ );
1782
+ const freeTierPackage = packages.find((p) => !p.product);
1783
+ const paidPackages = packages.filter((p) => p.product);
1714
1784
  return /* @__PURE__ */ jsxs("div", { className, children: [
1715
1785
  /* @__PURE__ */ jsx(Section, { spacing: "2xl", maxWidth: "4xl", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
1716
1786
  /* @__PURE__ */ jsx("h1", { className: "text-4xl sm:text-5xl font-bold text-theme-text-primary mb-4", children: labels.title }),
1717
1787
  /* @__PURE__ */ jsx("p", { className: "text-lg text-theme-text-secondary", children: labels.subtitle })
1718
1788
  ] }) }),
1719
1789
  /* @__PURE__ */ jsxs(Section, { spacing: "3xl", maxWidth: "6xl", children: [
1720
- /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-8", children: /* @__PURE__ */ jsx(
1790
+ periods.length > 1 && /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-8", children: /* @__PURE__ */ jsx(
1721
1791
  SegmentedControl,
1722
1792
  {
1723
1793
  options: billingPeriodOptions,
1724
- value: billingPeriod,
1794
+ value: selectedPeriod,
1725
1795
  onChange: handleBillingPeriodChange
1726
1796
  }
1727
1797
  ) }),
1728
- /* @__PURE__ */ jsxs(
1798
+ isLoading && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }) }),
1799
+ error && !isLoading && /* @__PURE__ */ jsx("div", { className: "text-center py-12 text-red-500", children: error.message }),
1800
+ !isLoading && !error && /* @__PURE__ */ jsxs(
1729
1801
  "div",
1730
1802
  {
1731
1803
  style: {
@@ -1736,7 +1808,7 @@ function AppPricingPage({
1736
1808
  overflow: "visible"
1737
1809
  },
1738
1810
  children: [
1739
- /* @__PURE__ */ jsx(
1811
+ freeTierPackage && /* @__PURE__ */ jsx(
1740
1812
  SubscriptionTile,
1741
1813
  {
1742
1814
  id: "free",
@@ -1745,8 +1817,10 @@ function AppPricingPage({
1745
1817
  periodLabel: labels.periodMonth,
1746
1818
  features: labels.freeTierFeatures,
1747
1819
  isSelected: false,
1820
+ isCurrentPlan: isAuthenticated && !hasActiveSubscription,
1748
1821
  onSelect: () => {
1749
1822
  },
1823
+ enabled: isPackageEnabled("free"),
1750
1824
  topBadge: isAuthenticated && !hasActiveSubscription ? {
1751
1825
  text: labels.currentPlanBadge,
1752
1826
  color: "green"
@@ -1763,22 +1837,29 @@ function AppPricingPage({
1763
1837
  hideSelectionIndicator: isAuthenticated
1764
1838
  }
1765
1839
  ),
1766
- filteredProducts.map((product) => {
1767
- var _a;
1768
- const isCurrent = isCurrentPlan(product.identifier);
1769
- const canUpgrade = isUpgrade(product.identifier);
1840
+ paidPackages.map((pkg) => {
1841
+ var _a, _b, _c, _d;
1842
+ const isCurrent = isCurrentPlan(
1843
+ pkg.packageId,
1844
+ (_a = pkg.product) == null ? void 0 : _a.productId
1845
+ );
1846
+ const isEnabled = isPackageEnabled(pkg.packageId);
1847
+ const canUpgrade = canUpgradeTo(
1848
+ pkg.packageId,
1849
+ (_b = pkg.product) == null ? void 0 : _b.productId
1850
+ );
1770
1851
  let ctaButton;
1771
1852
  if (!isAuthenticated) {
1772
1853
  ctaButton = {
1773
1854
  label: labels.ctaLogIn,
1774
- onClick: () => handlePlanClick(product.identifier, "login")
1855
+ onClick: () => handlePlanClick(pkg.packageId, "login")
1775
1856
  };
1776
1857
  } else if (isCurrent) {
1777
1858
  ctaButton = void 0;
1778
1859
  } else if (canUpgrade) {
1779
1860
  ctaButton = {
1780
1861
  label: labels.ctaUpgrade,
1781
- onClick: () => handlePlanClick(product.identifier, "upgrade")
1862
+ onClick: () => handlePlanClick(pkg.packageId, "upgrade")
1782
1863
  };
1783
1864
  }
1784
1865
  let topBadge;
@@ -1787,7 +1868,7 @@ function AppPricingPage({
1787
1868
  text: labels.currentPlanBadge,
1788
1869
  color: "green"
1789
1870
  };
1790
- } else if (product.identifier.includes("pro")) {
1871
+ } else if (pkg.packageId.includes("pro")) {
1791
1872
  topBadge = {
1792
1873
  text: labels.mostPopularBadge,
1793
1874
  color: "yellow"
@@ -1796,20 +1877,20 @@ function AppPricingPage({
1796
1877
  return /* @__PURE__ */ jsx(
1797
1878
  SubscriptionTile,
1798
1879
  {
1799
- id: product.identifier,
1800
- title: product.title,
1801
- price: product.priceString,
1802
- periodLabel: getPeriodLabel(product.period),
1803
- features: formatters.getProductFeatures(product.identifier),
1880
+ id: pkg.packageId,
1881
+ title: pkg.name,
1882
+ price: ((_c = pkg.product) == null ? void 0 : _c.priceString) ?? "$0",
1883
+ periodLabel: getPeriodLabel(((_d = pkg.product) == null ? void 0 : _d.period) ?? "monthly"),
1884
+ features: formatters.getProductFeatures(pkg.packageId),
1804
1885
  isSelected: false,
1886
+ isCurrentPlan: isCurrent,
1805
1887
  onSelect: () => {
1806
1888
  },
1807
- isBestValue: product.identifier.includes("pro"),
1889
+ enabled: isEnabled,
1890
+ isBestValue: pkg.packageId.includes("pro"),
1808
1891
  topBadge,
1809
- discountBadge: ((_a = product.period) == null ? void 0 : _a.includes("Y")) ? (() => {
1810
- const savings = getYearlySavingsPercent(
1811
- product.identifier
1812
- );
1892
+ discountBadge: selectedPeriod === "yearly" ? (() => {
1893
+ const savings = getYearlySavingsPercent(pkg);
1813
1894
  return savings && savings > 0 ? {
1814
1895
  text: formatters.formatSavePercent(savings),
1815
1896
  isBestValue: true
@@ -1818,7 +1899,7 @@ function AppPricingPage({
1818
1899
  ctaButton,
1819
1900
  hideSelectionIndicator: !ctaButton
1820
1901
  },
1821
- product.identifier
1902
+ pkg.packageId
1822
1903
  );
1823
1904
  })
1824
1905
  ]