@sudobility/building_blocks 0.0.60 → 0.0.64

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));
@@ -1258,212 +1259,54 @@ const GlobalSettingsPage = ({
1258
1259
  }
1259
1260
  ) });
1260
1261
  };
1261
- function useSubscriptions(offerId) {
1262
- const [offer, setOffer] = useState(null);
1263
- const [isLoading, setIsLoading] = useState(true);
1264
- const [error, setError] = useState(null);
1265
- const loadData = useCallback(async () => {
1266
- {
1267
- setError(new Error("Subscription not initialized"));
1268
- setIsLoading(false);
1269
- return;
1270
- }
1271
- }, [offerId]);
1272
- useEffect(() => {
1273
- loadData();
1274
- }, [loadData]);
1275
- const refetch = useCallback(async () => {
1276
- return;
1277
- }, [offerId]);
1278
- return { offer, isLoading, error, refetch };
1279
- }
1280
- function useUserSubscription() {
1281
- const [subscription, setSubscription] = useState(null);
1282
- const [isLoading, setIsLoading] = useState(true);
1283
- const [error, setError] = useState(null);
1284
- const loadData = useCallback(async () => {
1285
- {
1286
- setError(new Error("Subscription not initialized"));
1287
- setIsLoading(false);
1288
- return;
1289
- }
1290
- }, []);
1291
- useEffect(() => {
1292
- loadData();
1293
- }, [loadData]);
1294
- const refetch = useCallback(async () => {
1295
- return;
1296
- }, []);
1297
- return { subscription, isLoading, error, refetch };
1298
- }
1299
- function calculatePackageLevels(packages) {
1300
- var _a;
1301
- const levels = /* @__PURE__ */ new Map();
1302
- const byPeriod = /* @__PURE__ */ new Map();
1303
- for (const pkg of packages) {
1304
- if (!pkg.product) {
1305
- levels.set(pkg.packageId, 0);
1306
- continue;
1307
- }
1308
- const period = pkg.product.period;
1309
- const existing = byPeriod.get(period) ?? [];
1310
- existing.push(pkg);
1311
- byPeriod.set(period, existing);
1312
- }
1313
- for (const [_period, periodPackages] of byPeriod) {
1314
- const sorted = [...periodPackages].sort((a, b) => {
1315
- var _a2, _b;
1316
- const priceA = ((_a2 = a.product) == null ? void 0 : _a2.price) ?? 0;
1317
- const priceB = ((_b = b.product) == null ? void 0 : _b.price) ?? 0;
1318
- return priceA - priceB;
1319
- });
1320
- let currentLevel = 0;
1321
- let lastPrice = null;
1322
- for (const pkg of sorted) {
1323
- const price = ((_a = pkg.product) == null ? void 0 : _a.price) ?? 0;
1324
- if (lastPrice === null || price > lastPrice) {
1325
- currentLevel++;
1326
- lastPrice = price;
1327
- }
1328
- levels.set(pkg.packageId, currentLevel);
1329
- }
1330
- }
1331
- return levels;
1332
- }
1333
- function findUpgradeablePackages(current, packages) {
1334
- let currentPackageId;
1335
- let currentProductId;
1336
- if (typeof current === "string") {
1337
- currentPackageId = current;
1338
- } else if (current) {
1339
- currentPackageId = current.packageId;
1340
- currentProductId = current.productId;
1341
- }
1342
- if (!currentPackageId && !currentProductId) {
1343
- return packages.map((p) => p.packageId);
1344
- }
1345
- let currentPkg = currentPackageId ? packages.find((p) => p.packageId === currentPackageId) : void 0;
1346
- if (!currentPkg && currentProductId) {
1347
- currentPkg = packages.find((p) => {
1348
- var _a;
1349
- return ((_a = p.product) == null ? void 0 : _a.productId) === currentProductId;
1350
- });
1351
- }
1352
- if (!currentPkg) {
1353
- return packages.map((p) => p.packageId);
1354
- }
1355
- const matchedPackageId = currentPkg.packageId;
1356
- if (!currentPkg.product) {
1357
- return packages.filter((p) => p.product !== void 0).map((p) => p.packageId);
1358
- }
1359
- const levels = calculatePackageLevels(packages);
1360
- const currentLevel = levels.get(matchedPackageId) ?? 0;
1361
- const currentPeriod = currentPkg.product.period;
1362
- const upgrades = [];
1363
- for (const pkg of packages) {
1364
- if (!pkg.product)
1365
- continue;
1366
- if (pkg.packageId === matchedPackageId) {
1367
- continue;
1368
- }
1369
- const pkgLevel = levels.get(pkg.packageId) ?? 0;
1370
- const pkgPeriod = pkg.product.period;
1371
- const periodRanks = {
1372
- weekly: 1,
1373
- monthly: 2,
1374
- quarterly: 3,
1375
- yearly: 4,
1376
- lifetime: 5
1377
- };
1378
- const isPeriodOk = periodRanks[pkgPeriod] >= periodRanks[currentPeriod];
1379
- const isLevelOk = pkgLevel >= currentLevel;
1380
- if (isPeriodOk && isLevelOk) {
1381
- upgrades.push(pkg.packageId);
1382
- }
1383
- }
1384
- return upgrades;
1385
- }
1386
- function useSubscribable(offerId) {
1387
- const { offer, isLoading: loadingOffer, error: offerError } = useSubscriptions(offerId);
1388
- const { subscription, isLoading: loadingSub, error: subError } = useUserSubscription();
1389
- const isLoading = loadingOffer || loadingSub;
1390
- const error = offerError || subError;
1391
- const subscribablePackageIds = useMemo(() => {
1392
- if (!offer) {
1393
- return [];
1394
- }
1395
- let allPackages = [...offer.packages];
1396
- if (!subscription || !subscription.isActive) {
1397
- return allPackages.map((p) => p.packageId);
1398
- }
1399
- const current = {
1400
- packageId: subscription.packageId,
1401
- productId: subscription.productId
1402
- };
1403
- if (!current.packageId && !current.productId) {
1404
- return allPackages.map((p) => p.packageId);
1405
- }
1406
- return findUpgradeablePackages(current, allPackages);
1407
- }, [offer, subscription]);
1408
- return { subscribablePackageIds, isLoading, error };
1409
- }
1410
1262
  function AppSubscriptionsPage({
1411
- subscription,
1263
+ offerId,
1412
1264
  rateLimitsConfig,
1413
- subscriptionUserId,
1414
1265
  labels,
1415
1266
  formatters,
1267
+ onPurchase,
1268
+ onRestore,
1416
1269
  onPurchaseSuccess,
1417
1270
  onRestoreSuccess,
1418
1271
  onError,
1419
1272
  onWarning,
1420
- onTrack,
1421
- offerId
1273
+ onTrack
1422
1274
  }) {
1423
- const { subscribablePackageIds } = useSubscribable(offerId);
1424
1275
  const {
1425
- products,
1426
- currentSubscription,
1427
- isLoading,
1428
- error,
1429
- purchase,
1430
- restore,
1431
- clearError
1432
- } = subscription;
1433
- 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
+ update: updateSubscription
1301
+ } = useUserSubscription();
1302
+ useEffect(() => {
1303
+ updateSubscription();
1304
+ }, [updateSubscription]);
1305
+ const isLoading = periodsLoading || packagesLoading || subscribableLoading || subscriptionLoading;
1306
+ const error = periodsError || packagesError || subscribableError || subscriptionError;
1434
1307
  const [selectedPlan, setSelectedPlan] = useState(null);
1435
1308
  const [isPurchasing, setIsPurchasing] = useState(false);
1436
1309
  const [isRestoring, setIsRestoring] = useState(false);
1437
- const isCurrentPlanProduct = useCallback(
1438
- (productId) => {
1439
- return !!((currentSubscription == null ? void 0 : currentSubscription.isActive) && currentSubscription.productIdentifier === productId);
1440
- },
1441
- [currentSubscription]
1442
- );
1443
- const isPackageEnabled = useCallback(
1444
- (productId) => {
1445
- return subscribablePackageIds.includes(productId);
1446
- },
1447
- [subscribablePackageIds]
1448
- );
1449
- const canUpgradeTo = useCallback(
1450
- (productId) => {
1451
- return isPackageEnabled(productId) && !isCurrentPlanProduct(productId);
1452
- },
1453
- [isPackageEnabled, isCurrentPlanProduct]
1454
- );
1455
- useEffect(() => {
1456
- if ((currentSubscription == null ? void 0 : currentSubscription.isActive) && currentSubscription.productIdentifier) {
1457
- setSelectedPlan(currentSubscription.productIdentifier);
1458
- const currentProduct = products.find(
1459
- (p) => p.identifier === currentSubscription.productIdentifier
1460
- );
1461
- if (currentProduct == null ? void 0 : currentProduct.period) {
1462
- const isYearly = currentProduct.period.includes("Y") || currentProduct.period.includes("year");
1463
- setBillingPeriod(isYearly ? "yearly" : "monthly");
1464
- }
1465
- }
1466
- }, [currentSubscription, products]);
1467
1310
  const track = useCallback(
1468
1311
  (label, params) => {
1469
1312
  onTrack == null ? void 0 : onTrack({
@@ -1475,32 +1318,99 @@ function AppSubscriptionsPage({
1475
1318
  },
1476
1319
  [onTrack]
1477
1320
  );
1478
- useEffect(() => {
1479
- if (error) {
1480
- onError == null ? void 0 : onError(labels.errorTitle, error);
1481
- clearError();
1482
- }
1483
- }, [error, clearError, labels.errorTitle, onError]);
1484
- const filteredProducts = products.filter((product) => {
1485
- if (!product.period) return false;
1486
- const isYearly = product.period.includes("Y") || product.period.includes("year");
1487
- return billingPeriod === "yearly" ? isYearly : !isYearly;
1488
- }).sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
1489
1321
  const handlePeriodChange = useCallback(
1490
- (period) => {
1491
- setBillingPeriod(period);
1322
+ (value) => {
1323
+ const newPeriod = value;
1324
+ setSelectedPeriod(newPeriod);
1492
1325
  setSelectedPlan(null);
1493
- track("billing_period_changed", { billing_period: period });
1326
+ track("billing_period_changed", { billing_period: newPeriod });
1327
+ },
1328
+ [track]
1329
+ );
1330
+ const getPeriodLabel = useCallback(
1331
+ (period) => {
1332
+ switch (period) {
1333
+ case "yearly":
1334
+ return labels.periodYear;
1335
+ case "monthly":
1336
+ return labels.periodMonth;
1337
+ case "weekly":
1338
+ return labels.periodWeek;
1339
+ default:
1340
+ return period;
1341
+ }
1342
+ },
1343
+ [labels]
1344
+ );
1345
+ const getYearlySavingsPercent = useCallback(
1346
+ (yearlyPackage) => {
1347
+ if (!yearlyPackage.product) return void 0;
1348
+ const monthlyPackageId = yearlyPackage.packageId.replace(
1349
+ "_yearly",
1350
+ "_monthly"
1351
+ );
1352
+ const monthlyPkg = packages.find((p) => p.packageId === monthlyPackageId);
1353
+ if (!(monthlyPkg == null ? void 0 : monthlyPkg.product)) return void 0;
1354
+ const yearlyPrice = yearlyPackage.product.price;
1355
+ const monthlyPrice = monthlyPkg.product.price;
1356
+ if (monthlyPrice <= 0 || yearlyPrice <= 0) return void 0;
1357
+ const annualizedMonthly = monthlyPrice * 12;
1358
+ const savings = (annualizedMonthly - yearlyPrice) / annualizedMonthly * 100;
1359
+ return Math.round(savings);
1360
+ },
1361
+ [packages]
1362
+ );
1363
+ const billingPeriodOptions = useMemo(() => {
1364
+ return periods.map((period) => ({
1365
+ value: period,
1366
+ label: period === "monthly" ? labels.billingMonthly : labels.billingYearly
1367
+ }));
1368
+ }, [periods, labels]);
1369
+ const isCurrentPlan = useCallback(
1370
+ (packageId, productId) => {
1371
+ if (!(currentSubscription == null ? void 0 : currentSubscription.isActive)) return false;
1372
+ return productId === currentSubscription.productId || packageId === currentSubscription.packageId;
1373
+ },
1374
+ [currentSubscription]
1375
+ );
1376
+ const isPackageEnabled = useCallback(
1377
+ (packageId) => {
1378
+ if (subscribableLoading || subscribablePackageIds.length === 0)
1379
+ return true;
1380
+ return subscribablePackageIds.includes(packageId);
1381
+ },
1382
+ [subscribableLoading, subscribablePackageIds]
1383
+ );
1384
+ const canUpgradeTo = useCallback(
1385
+ (packageId, productId) => {
1386
+ return isPackageEnabled(packageId) && !isCurrentPlan(packageId, productId);
1387
+ },
1388
+ [isPackageEnabled, isCurrentPlan]
1389
+ );
1390
+ useEffect(() => {
1391
+ if ((currentSubscription == null ? void 0 : currentSubscription.isActive) && currentSubscription.packageId) {
1392
+ setSelectedPlan(currentSubscription.packageId);
1393
+ if (currentSubscription.period && periods.includes(currentSubscription.period)) {
1394
+ setSelectedPeriod(currentSubscription.period);
1395
+ }
1396
+ }
1397
+ }, [currentSubscription, periods]);
1398
+ const handlePlanSelect = useCallback(
1399
+ (planIdentifier) => {
1400
+ setSelectedPlan(planIdentifier);
1401
+ track("plan_selected", {
1402
+ plan_identifier: planIdentifier ?? "free",
1403
+ is_free_tier: planIdentifier === null
1404
+ });
1494
1405
  },
1495
1406
  [track]
1496
1407
  );
1497
1408
  const handlePurchase = useCallback(async () => {
1498
1409
  if (!selectedPlan) return;
1499
1410
  setIsPurchasing(true);
1500
- clearError();
1501
1411
  track("purchase_initiated", { plan_identifier: selectedPlan });
1502
1412
  try {
1503
- const result = await purchase(selectedPlan, subscriptionUserId);
1413
+ const result = await onPurchase(selectedPlan);
1504
1414
  if (result) {
1505
1415
  track("purchase_completed", { plan_identifier: selectedPlan });
1506
1416
  onPurchaseSuccess == null ? void 0 : onPurchaseSuccess();
@@ -1523,31 +1433,18 @@ function AppSubscriptionsPage({
1523
1433
  }
1524
1434
  }, [
1525
1435
  selectedPlan,
1526
- clearError,
1527
1436
  track,
1528
- purchase,
1529
- subscriptionUserId,
1437
+ onPurchase,
1530
1438
  onPurchaseSuccess,
1531
1439
  labels.errorTitle,
1532
1440
  labels.purchaseError,
1533
1441
  onError
1534
1442
  ]);
1535
- const handlePlanSelect = useCallback(
1536
- (planIdentifier) => {
1537
- setSelectedPlan(planIdentifier);
1538
- track("plan_selected", {
1539
- plan_identifier: planIdentifier ?? "free",
1540
- is_free_tier: planIdentifier === null
1541
- });
1542
- },
1543
- [track]
1544
- );
1545
1443
  const handleRestore = useCallback(async () => {
1546
1444
  setIsRestoring(true);
1547
- clearError();
1548
1445
  track("restore_initiated");
1549
1446
  try {
1550
- const result = await restore(subscriptionUserId);
1447
+ const result = await onRestore();
1551
1448
  if (result) {
1552
1449
  track("restore_completed");
1553
1450
  onRestoreSuccess == null ? void 0 : onRestoreSuccess();
@@ -1563,10 +1460,8 @@ function AppSubscriptionsPage({
1563
1460
  setIsRestoring(false);
1564
1461
  }
1565
1462
  }, [
1566
- clearError,
1567
1463
  track,
1568
- restore,
1569
- subscriptionUserId,
1464
+ onRestore,
1570
1465
  onRestoreSuccess,
1571
1466
  labels.errorTitle,
1572
1467
  labels.restoreNoPurchases,
@@ -1582,41 +1477,20 @@ function AppSubscriptionsPage({
1582
1477
  day: "numeric"
1583
1478
  }).format(date);
1584
1479
  }, []);
1585
- const formatProductIdentifier = useCallback(
1586
- (identifier) => {
1587
- if (!identifier) return labels.labelPremium;
1588
- const product = products.find((p) => p.identifier === identifier);
1589
- if (product == null ? void 0 : product.title) return product.title;
1480
+ const formatProductName = useCallback(
1481
+ (packageId, productId) => {
1482
+ if (!packageId && !productId) return labels.labelPremium;
1483
+ const pkg = packages.find(
1484
+ (p) => {
1485
+ var _a;
1486
+ return p.packageId === packageId || ((_a = p.product) == null ? void 0 : _a.productId) === productId;
1487
+ }
1488
+ );
1489
+ if (pkg == null ? void 0 : pkg.name) return pkg.name;
1490
+ const identifier = packageId || productId || "";
1590
1491
  return identifier.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
1591
1492
  },
1592
- [products, labels.labelPremium]
1593
- );
1594
- const getPeriodLabel = useCallback(
1595
- (period) => {
1596
- if (!period) return "";
1597
- if (period.includes("Y") || period.includes("year"))
1598
- return labels.periodYear;
1599
- if (period.includes("M") || period.includes("month"))
1600
- return labels.periodMonth;
1601
- if (period.includes("W") || period.includes("week"))
1602
- return labels.periodWeek;
1603
- return "";
1604
- },
1605
- [labels]
1606
- );
1607
- const getTrialLabel = useCallback(
1608
- (trialPeriod) => {
1609
- if (!trialPeriod) return void 0;
1610
- const num = parseInt(trialPeriod.replace(/\D/g, "") || "1", 10);
1611
- if (trialPeriod.includes("W")) {
1612
- return formatters.formatTrialWeeks(num);
1613
- }
1614
- if (trialPeriod.includes("M")) {
1615
- return formatters.formatTrialMonths(num);
1616
- }
1617
- return formatters.formatTrialDays(num);
1618
- },
1619
- [formatters]
1493
+ [packages, labels.labelPremium]
1620
1494
  );
1621
1495
  const formatRateLimit = useCallback(
1622
1496
  (limit) => {
@@ -1625,44 +1499,13 @@ function AppSubscriptionsPage({
1625
1499
  },
1626
1500
  [labels.unlimited]
1627
1501
  );
1628
- const getProductFeatures = useCallback(
1629
- (packageId) => {
1630
- return formatters.getProductFeatures(packageId);
1631
- },
1632
- [formatters]
1633
- );
1634
- const getFreeTierFeatures = useCallback(() => {
1635
- return labels.freeTierFeatures;
1636
- }, [labels.freeTierFeatures]);
1637
- const getYearlySavingsPercent = useCallback(
1638
- (yearlyPackageId) => {
1639
- const yearlyProduct = products.find(
1640
- (p) => p.identifier === yearlyPackageId
1641
- );
1642
- if (!yearlyProduct) return void 0;
1643
- const monthlyPackageId = yearlyPackageId.replace("_yearly", "_monthly");
1644
- const monthlyProduct = products.find(
1645
- (p) => p.identifier === monthlyPackageId
1646
- );
1647
- if (!monthlyProduct) return void 0;
1648
- const yearlyPrice = parseFloat(yearlyProduct.price);
1649
- const monthlyPrice = parseFloat(monthlyProduct.price);
1650
- if (monthlyPrice <= 0 || yearlyPrice <= 0) return void 0;
1651
- const annualizedMonthly = monthlyPrice * 12;
1652
- const savings = (annualizedMonthly - yearlyPrice) / annualizedMonthly * 100;
1653
- return Math.round(savings);
1654
- },
1655
- [products]
1656
- );
1657
- const billingPeriodOptions = [
1658
- { value: "monthly", label: labels.billingMonthly },
1659
- { value: "yearly", label: labels.billingYearly }
1660
- ];
1502
+ const freeTierPackage = packages.find((p) => !p.product);
1503
+ const paidPackages = packages.filter((p) => p.product);
1661
1504
  return /* @__PURE__ */ jsx(
1662
1505
  SubscriptionLayout,
1663
1506
  {
1664
1507
  title: labels.title,
1665
- error,
1508
+ error: error == null ? void 0 : error.message,
1666
1509
  currentStatusLabel: labels.currentStatusLabel,
1667
1510
  currentStatus: {
1668
1511
  isActive: (currentSubscription == null ? void 0 : currentSubscription.isActive) ?? false,
@@ -1671,8 +1514,9 @@ function AppSubscriptionsPage({
1671
1514
  fields: [
1672
1515
  {
1673
1516
  label: labels.labelPlan,
1674
- value: formatProductIdentifier(
1675
- currentSubscription.productIdentifier
1517
+ value: formatProductName(
1518
+ currentSubscription.packageId,
1519
+ currentSubscription.productId
1676
1520
  )
1677
1521
  },
1678
1522
  {
@@ -1702,11 +1546,11 @@ function AppSubscriptionsPage({
1702
1546
  message: labels.statusInactiveMessage
1703
1547
  } : void 0
1704
1548
  },
1705
- aboveProducts: !isLoading && products.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-6", children: /* @__PURE__ */ jsx(
1549
+ aboveProducts: !isLoading && periods.length > 1 ? /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-6", children: /* @__PURE__ */ jsx(
1706
1550
  SegmentedControl,
1707
1551
  {
1708
1552
  options: billingPeriodOptions,
1709
- value: billingPeriod,
1553
+ value: selectedPeriod,
1710
1554
  onChange: handlePeriodChange
1711
1555
  }
1712
1556
  ) }) : null,
@@ -1722,19 +1566,19 @@ function AppSubscriptionsPage({
1722
1566
  disabled: isPurchasing || isRestoring,
1723
1567
  loading: isRestoring
1724
1568
  },
1725
- 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: [
1726
- !(currentSubscription == null ? void 0 : currentSubscription.isActive) && /* @__PURE__ */ jsx(
1569
+ 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: [
1570
+ freeTierPackage && !(currentSubscription == null ? void 0 : currentSubscription.isActive) && /* @__PURE__ */ jsx(
1727
1571
  SubscriptionTile,
1728
1572
  {
1729
1573
  id: "free",
1730
1574
  title: labels.freeTierTitle,
1731
1575
  price: labels.freeTierPrice,
1732
1576
  periodLabel: labels.periodMonth,
1733
- features: getFreeTierFeatures(),
1577
+ features: labels.freeTierFeatures,
1734
1578
  isSelected: selectedPlan === null,
1735
1579
  isCurrentPlan: !(currentSubscription == null ? void 0 : currentSubscription.isActive),
1736
1580
  onSelect: () => handlePlanSelect(null),
1737
- enabled: subscribablePackageIds.includes("free"),
1581
+ enabled: isPackageEnabled("free"),
1738
1582
  topBadge: {
1739
1583
  text: labels.currentPlanBadge,
1740
1584
  color: "green"
@@ -1744,42 +1588,59 @@ function AppSubscriptionsPage({
1744
1588
  },
1745
1589
  "free"
1746
1590
  ),
1747
- filteredProducts.map((product) => {
1748
- var _a;
1749
- const isCurrent = isCurrentPlanProduct(product.identifier);
1750
- const isEnabled = isPackageEnabled(product.identifier);
1751
- const canUpgrade = canUpgradeTo(product.identifier);
1591
+ paidPackages.map((pkg) => {
1592
+ var _a, _b, _c, _d, _e, _f;
1593
+ const isCurrent = isCurrentPlan(
1594
+ pkg.packageId,
1595
+ (_a = pkg.product) == null ? void 0 : _a.productId
1596
+ );
1597
+ const isEnabled = isPackageEnabled(pkg.packageId);
1598
+ const canUpgrade = canUpgradeTo(
1599
+ pkg.packageId,
1600
+ (_b = pkg.product) == null ? void 0 : _b.productId
1601
+ );
1752
1602
  const canSelect = canUpgrade || isCurrent;
1753
1603
  return /* @__PURE__ */ jsx(
1754
1604
  SubscriptionTile,
1755
1605
  {
1756
- id: product.identifier,
1757
- title: product.title,
1758
- price: product.priceString,
1759
- periodLabel: getPeriodLabel(product.period),
1760
- features: getProductFeatures(product.identifier),
1761
- isSelected: selectedPlan === product.identifier,
1606
+ id: pkg.packageId,
1607
+ title: pkg.name,
1608
+ price: ((_c = pkg.product) == null ? void 0 : _c.priceString) ?? "$0",
1609
+ periodLabel: getPeriodLabel(((_d = pkg.product) == null ? void 0 : _d.period) ?? "monthly"),
1610
+ features: formatters.getProductFeatures(pkg.packageId),
1611
+ isSelected: selectedPlan === pkg.packageId,
1762
1612
  isCurrentPlan: isCurrent,
1763
- onSelect: () => canSelect && handlePlanSelect(product.identifier),
1613
+ onSelect: () => canSelect && handlePlanSelect(pkg.packageId),
1764
1614
  enabled: isEnabled,
1765
- isBestValue: product.identifier.includes("pro"),
1615
+ isBestValue: pkg.packageId.includes("pro"),
1766
1616
  topBadge: isCurrent ? {
1767
1617
  text: labels.currentPlanBadge,
1768
1618
  color: "green"
1769
1619
  } : void 0,
1770
- discountBadge: ((_a = product.period) == null ? void 0 : _a.includes("Y")) ? (() => {
1771
- const savings = getYearlySavingsPercent(
1772
- product.identifier
1773
- );
1620
+ discountBadge: selectedPeriod === "yearly" ? (() => {
1621
+ const savings = getYearlySavingsPercent(pkg);
1774
1622
  return savings && savings > 0 ? {
1775
1623
  text: formatters.formatSavePercent(savings),
1776
1624
  isBestValue: true
1777
1625
  } : void 0;
1778
1626
  })() : void 0,
1779
- introPriceNote: product.freeTrialPeriod ? getTrialLabel(product.freeTrialPeriod) : product.introPrice ? formatters.formatIntroNote(product.introPrice) : void 0,
1627
+ introPriceNote: ((_e = pkg.product) == null ? void 0 : _e.trialPeriod) ? (() => {
1628
+ var _a2;
1629
+ const period = (_a2 = pkg.product) == null ? void 0 : _a2.trialPeriod;
1630
+ if (!period) return void 0;
1631
+ const num = parseInt(
1632
+ period.replace(/\D/g, "") || "1",
1633
+ 10
1634
+ );
1635
+ if (period.includes("W"))
1636
+ return formatters.formatTrialWeeks(num);
1637
+ if (period.includes("M"))
1638
+ return formatters.formatTrialMonths(num);
1639
+ return formatters.formatTrialDays(num);
1640
+ })() : ((_f = pkg.product) == null ? void 0 : _f.introPrice) ? formatters.formatIntroNote(pkg.product.introPrice) : void 0,
1780
1641
  disabled: isPurchasing || isRestoring || !canSelect
1781
1642
  },
1782
- product.identifier
1643
+ pkg.packageId
1783
1644
  );
1784
1645
  })
1785
1646
  ] })
@@ -1787,10 +1648,7 @@ function AppSubscriptionsPage({
1787
1648
  );
1788
1649
  }
1789
1650
  function AppPricingPage({
1790
- products,
1791
1651
  isAuthenticated,
1792
- hasActiveSubscription,
1793
- currentProductIdentifier,
1794
1652
  labels,
1795
1653
  formatters,
1796
1654
  onPlanClick,
@@ -1800,8 +1658,40 @@ function AppPricingPage({
1800
1658
  onTrack,
1801
1659
  offerId
1802
1660
  }) {
1803
- const [billingPeriod, setBillingPeriod] = useState("monthly");
1804
- const { subscribablePackageIds } = useSubscribable(offerId);
1661
+ const {
1662
+ subscription: currentSubscription,
1663
+ isLoading: subscriptionLoading,
1664
+ error: subscriptionError,
1665
+ update: updateSubscription
1666
+ } = useUserSubscription();
1667
+ useEffect(() => {
1668
+ updateSubscription();
1669
+ }, [updateSubscription]);
1670
+ const hasActiveSubscription = (currentSubscription == null ? void 0 : currentSubscription.isActive) ?? false;
1671
+ const currentProductIdentifier = currentSubscription == null ? void 0 : currentSubscription.productId;
1672
+ const {
1673
+ periods,
1674
+ isLoading: periodsLoading,
1675
+ error: periodsError
1676
+ } = useSubscriptionPeriods(offerId);
1677
+ const [selectedPeriod, setSelectedPeriod] = useState("monthly");
1678
+ useEffect(() => {
1679
+ if (periods.length > 0 && !periods.includes(selectedPeriod)) {
1680
+ setSelectedPeriod(periods[0]);
1681
+ }
1682
+ }, [periods, selectedPeriod]);
1683
+ const {
1684
+ packages,
1685
+ isLoading: packagesLoading,
1686
+ error: packagesError
1687
+ } = useSubscriptionForPeriod(offerId, selectedPeriod);
1688
+ const {
1689
+ subscribablePackageIds,
1690
+ isLoading: subscribableLoading,
1691
+ error: subscribableError
1692
+ } = useSubscribable(offerId);
1693
+ const isLoading = periodsLoading || packagesLoading || subscribableLoading || subscriptionLoading;
1694
+ const error = periodsError || packagesError || subscribableError || subscriptionError;
1805
1695
  const track = useCallback(
1806
1696
  (label, params) => {
1807
1697
  onTrack == null ? void 0 : onTrack({
@@ -1816,7 +1706,7 @@ function AppPricingPage({
1816
1706
  const handleBillingPeriodChange = useCallback(
1817
1707
  (value) => {
1818
1708
  const newPeriod = value;
1819
- setBillingPeriod(newPeriod);
1709
+ setSelectedPeriod(newPeriod);
1820
1710
  track("billing_period_changed", { billing_period: newPeriod });
1821
1711
  },
1822
1712
  [track]
@@ -1835,84 +1725,87 @@ function AppPricingPage({
1835
1725
  },
1836
1726
  [track, onPlanClick]
1837
1727
  );
1838
- const filteredProducts = products.filter((product) => {
1839
- if (!product.period) return false;
1840
- const isYearly = product.period.includes("Y") || product.period.includes("year");
1841
- return billingPeriod === "yearly" ? isYearly : !isYearly;
1842
- }).sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
1843
1728
  const getPeriodLabel = useCallback(
1844
1729
  (period) => {
1845
- if (!period) return "";
1846
- if (period.includes("Y") || period.includes("year"))
1847
- return labels.periodYear;
1848
- if (period.includes("M") || period.includes("month"))
1849
- return labels.periodMonth;
1850
- if (period.includes("W") || period.includes("week"))
1851
- return labels.periodWeek;
1852
- return "";
1730
+ switch (period) {
1731
+ case "yearly":
1732
+ return labels.periodYear;
1733
+ case "monthly":
1734
+ return labels.periodMonth;
1735
+ case "weekly":
1736
+ return labels.periodWeek;
1737
+ default:
1738
+ return period;
1739
+ }
1853
1740
  },
1854
1741
  [labels]
1855
1742
  );
1856
1743
  const getYearlySavingsPercent = useCallback(
1857
- (yearlyPackageId) => {
1858
- const yearlyProduct = products.find(
1859
- (p) => p.identifier === yearlyPackageId
1860
- );
1861
- if (!yearlyProduct) return void 0;
1862
- const monthlyPackageId = yearlyPackageId.replace("_yearly", "_monthly");
1863
- const monthlyProduct = products.find(
1864
- (p) => p.identifier === monthlyPackageId
1744
+ (yearlyPackage) => {
1745
+ if (!yearlyPackage.product) return void 0;
1746
+ const monthlyPackageId = yearlyPackage.packageId.replace(
1747
+ "_yearly",
1748
+ "_monthly"
1865
1749
  );
1866
- if (!monthlyProduct) return void 0;
1867
- const yearlyPrice = parseFloat(yearlyProduct.price);
1868
- const monthlyPrice = parseFloat(monthlyProduct.price);
1750
+ const monthlyPkg = packages.find((p) => p.packageId === monthlyPackageId);
1751
+ if (!(monthlyPkg == null ? void 0 : monthlyPkg.product)) return void 0;
1752
+ const yearlyPrice = yearlyPackage.product.price;
1753
+ const monthlyPrice = monthlyPkg.product.price;
1869
1754
  if (monthlyPrice <= 0 || yearlyPrice <= 0) return void 0;
1870
1755
  const annualizedMonthly = monthlyPrice * 12;
1871
1756
  const savings = (annualizedMonthly - yearlyPrice) / annualizedMonthly * 100;
1872
1757
  return Math.round(savings);
1873
1758
  },
1874
- [products]
1759
+ [packages]
1875
1760
  );
1876
- const billingPeriodOptions = [
1877
- { value: "monthly", label: labels.billingMonthly },
1878
- { value: "yearly", label: labels.billingYearly }
1879
- ];
1761
+ const billingPeriodOptions = useMemo(() => {
1762
+ return periods.map((period) => ({
1763
+ value: period,
1764
+ label: period === "monthly" ? labels.billingMonthly : labels.billingYearly
1765
+ }));
1766
+ }, [periods, labels]);
1880
1767
  const isCurrentPlan = useCallback(
1881
- (productId) => {
1768
+ (packageId, productId) => {
1882
1769
  if (!isAuthenticated) return false;
1883
1770
  if (!hasActiveSubscription) return false;
1884
- return productId === currentProductIdentifier;
1771
+ return productId === currentProductIdentifier || packageId === currentProductIdentifier;
1885
1772
  },
1886
1773
  [isAuthenticated, hasActiveSubscription, currentProductIdentifier]
1887
1774
  );
1888
1775
  const isPackageEnabled = useCallback(
1889
- (productId) => {
1776
+ (packageId) => {
1890
1777
  if (!isAuthenticated) return true;
1891
- return subscribablePackageIds.includes(productId);
1778
+ if (subscribableLoading || subscribablePackageIds.length === 0)
1779
+ return true;
1780
+ return subscribablePackageIds.includes(packageId);
1892
1781
  },
1893
- [isAuthenticated, subscribablePackageIds]
1782
+ [isAuthenticated, subscribableLoading, subscribablePackageIds]
1894
1783
  );
1895
1784
  const canUpgradeTo = useCallback(
1896
- (productId) => {
1897
- return isPackageEnabled(productId) && !isCurrentPlan(productId);
1785
+ (packageId, productId) => {
1786
+ return isPackageEnabled(packageId) && !isCurrentPlan(packageId, productId);
1898
1787
  },
1899
1788
  [isPackageEnabled, isCurrentPlan]
1900
1789
  );
1790
+ const freeTierPackage = packages.find((p) => !p.product);
1791
+ const paidPackages = packages.filter((p) => p.product);
1901
1792
  return /* @__PURE__ */ jsxs("div", { className, children: [
1902
1793
  /* @__PURE__ */ jsx(Section, { spacing: "2xl", maxWidth: "4xl", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
1903
1794
  /* @__PURE__ */ jsx("h1", { className: "text-4xl sm:text-5xl font-bold text-theme-text-primary mb-4", children: labels.title }),
1904
1795
  /* @__PURE__ */ jsx("p", { className: "text-lg text-theme-text-secondary", children: labels.subtitle })
1905
1796
  ] }) }),
1906
1797
  /* @__PURE__ */ jsxs(Section, { spacing: "3xl", maxWidth: "6xl", children: [
1907
- /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-8", children: /* @__PURE__ */ jsx(
1798
+ periods.length > 1 && /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-8", children: /* @__PURE__ */ jsx(
1908
1799
  SegmentedControl,
1909
1800
  {
1910
1801
  options: billingPeriodOptions,
1911
- value: billingPeriod,
1802
+ value: selectedPeriod,
1912
1803
  onChange: handleBillingPeriodChange
1913
1804
  }
1914
1805
  ) }),
1915
- /* @__PURE__ */ jsxs(
1806
+ 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" }) }),
1807
+ error && !isLoading && /* @__PURE__ */ jsx("div", { className: "text-center py-12 text-red-500", children: error.message }),
1808
+ !isLoading && !error && /* @__PURE__ */ jsxs(
1916
1809
  "div",
1917
1810
  {
1918
1811
  style: {
@@ -1923,7 +1816,7 @@ function AppPricingPage({
1923
1816
  overflow: "visible"
1924
1817
  },
1925
1818
  children: [
1926
- /* @__PURE__ */ jsx(
1819
+ freeTierPackage && /* @__PURE__ */ jsx(
1927
1820
  SubscriptionTile,
1928
1821
  {
1929
1822
  id: "free",
@@ -1935,10 +1828,7 @@ function AppPricingPage({
1935
1828
  isCurrentPlan: isAuthenticated && !hasActiveSubscription,
1936
1829
  onSelect: () => {
1937
1830
  },
1938
- enabled: (
1939
- // Free tier enabled: not authenticated, or in subscribable list
1940
- !isAuthenticated || subscribablePackageIds.includes("free")
1941
- ),
1831
+ enabled: isPackageEnabled("free"),
1942
1832
  topBadge: isAuthenticated && !hasActiveSubscription ? {
1943
1833
  text: labels.currentPlanBadge,
1944
1834
  color: "green"
@@ -1955,23 +1845,29 @@ function AppPricingPage({
1955
1845
  hideSelectionIndicator: isAuthenticated
1956
1846
  }
1957
1847
  ),
1958
- filteredProducts.map((product) => {
1959
- var _a;
1960
- const isCurrent = isCurrentPlan(product.identifier);
1961
- const isEnabled = isPackageEnabled(product.identifier);
1962
- const canUpgrade = canUpgradeTo(product.identifier);
1848
+ paidPackages.map((pkg) => {
1849
+ var _a, _b, _c, _d;
1850
+ const isCurrent = isCurrentPlan(
1851
+ pkg.packageId,
1852
+ (_a = pkg.product) == null ? void 0 : _a.productId
1853
+ );
1854
+ const isEnabled = isPackageEnabled(pkg.packageId);
1855
+ const canUpgrade = canUpgradeTo(
1856
+ pkg.packageId,
1857
+ (_b = pkg.product) == null ? void 0 : _b.productId
1858
+ );
1963
1859
  let ctaButton;
1964
1860
  if (!isAuthenticated) {
1965
1861
  ctaButton = {
1966
1862
  label: labels.ctaLogIn,
1967
- onClick: () => handlePlanClick(product.identifier, "login")
1863
+ onClick: () => handlePlanClick(pkg.packageId, "login")
1968
1864
  };
1969
1865
  } else if (isCurrent) {
1970
1866
  ctaButton = void 0;
1971
1867
  } else if (canUpgrade) {
1972
1868
  ctaButton = {
1973
1869
  label: labels.ctaUpgrade,
1974
- onClick: () => handlePlanClick(product.identifier, "upgrade")
1870
+ onClick: () => handlePlanClick(pkg.packageId, "upgrade")
1975
1871
  };
1976
1872
  }
1977
1873
  let topBadge;
@@ -1980,7 +1876,7 @@ function AppPricingPage({
1980
1876
  text: labels.currentPlanBadge,
1981
1877
  color: "green"
1982
1878
  };
1983
- } else if (product.identifier.includes("pro")) {
1879
+ } else if (pkg.packageId.includes("pro")) {
1984
1880
  topBadge = {
1985
1881
  text: labels.mostPopularBadge,
1986
1882
  color: "yellow"
@@ -1989,22 +1885,20 @@ function AppPricingPage({
1989
1885
  return /* @__PURE__ */ jsx(
1990
1886
  SubscriptionTile,
1991
1887
  {
1992
- id: product.identifier,
1993
- title: product.title,
1994
- price: product.priceString,
1995
- periodLabel: getPeriodLabel(product.period),
1996
- features: formatters.getProductFeatures(product.identifier),
1888
+ id: pkg.packageId,
1889
+ title: pkg.name,
1890
+ price: ((_c = pkg.product) == null ? void 0 : _c.priceString) ?? "$0",
1891
+ periodLabel: getPeriodLabel(((_d = pkg.product) == null ? void 0 : _d.period) ?? "monthly"),
1892
+ features: formatters.getProductFeatures(pkg.packageId),
1997
1893
  isSelected: false,
1998
1894
  isCurrentPlan: isCurrent,
1999
1895
  onSelect: () => {
2000
1896
  },
2001
1897
  enabled: isEnabled,
2002
- isBestValue: product.identifier.includes("pro"),
1898
+ isBestValue: pkg.packageId.includes("pro"),
2003
1899
  topBadge,
2004
- discountBadge: ((_a = product.period) == null ? void 0 : _a.includes("Y")) ? (() => {
2005
- const savings = getYearlySavingsPercent(
2006
- product.identifier
2007
- );
1900
+ discountBadge: selectedPeriod === "yearly" ? (() => {
1901
+ const savings = getYearlySavingsPercent(pkg);
2008
1902
  return savings && savings > 0 ? {
2009
1903
  text: formatters.formatSavePercent(savings),
2010
1904
  isBestValue: true
@@ -2013,7 +1907,7 @@ function AppPricingPage({
2013
1907
  ctaButton,
2014
1908
  hideSelectionIndicator: !ctaButton
2015
1909
  },
2016
- product.identifier
1910
+ pkg.packageId
2017
1911
  );
2018
1912
  })
2019
1913
  ]