Skip to content

Commit acc296e

Browse files
authored
Merge pull request #2287 from useautumn/frontend/license-cleanup-and-billing
license leanup and billing
2 parents 197df53 + b484878 commit acc296e

7 files changed

Lines changed: 90 additions & 53 deletions

File tree

packages/ui/src/components/ui/button.tsx

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority";
77
import * as React from "react";
88

99
const buttonVariants = cva(
10-
`inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none cursor-pointer
10+
`relative inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none cursor-pointer
1111
rounded-lg group/btn transition-colors w-fit transform-gpu
1212
`,
1313
{
@@ -138,17 +138,27 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
138138
disabled={isLoading || props.disabled}
139139
{...props}
140140
>
141-
{effectiveRender ? undefined : isLoading ? (
142-
<SmallSpinner
143-
size={14}
144-
className={variant === "primary" ? "relative z-10" : undefined}
145-
/>
146-
) : variant === "primary" ? (
147-
<span className="relative z-10 inline-flex items-center gap-2 transition-none">
148-
{children}
149-
</span>
150-
) : (
151-
children
141+
{effectiveRender ? undefined : (
142+
<>
143+
{isLoading && (
144+
<SmallSpinner
145+
size={14}
146+
className={cn(
147+
"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
148+
variant === "primary" && "z-10",
149+
)}
150+
/>
151+
)}
152+
<span
153+
className={cn(
154+
"inline-flex items-center gap-2",
155+
variant === "primary" && "relative z-10 transition-none",
156+
isLoading && "invisible",
157+
)}
158+
>
159+
{children}
160+
</span>
161+
</>
152162
)}
153163
</ButtonPrimitive>
154164
);

server/src/internal/products/internalHandlers/handleGetProducts.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,19 @@ export const handleGetLicenseProducts = createRoute({
106106
orgId: org.id,
107107
env,
108108
});
109-
const linkedInternalIds = new Set(
110-
links.map((link) => link.license_internal_product_id),
109+
const linkedInternalIds = links.map(
110+
(link) => link.license_internal_product_id,
111+
);
112+
113+
// A link points at the exact version's internal_id it was created against,
114+
// which may be an older version. Resolve to public ids across all versions
115+
// so versioned license plans still match the latest-version list below.
116+
const linkedProducts = await planLicenseRepo.listProductsByInternalIds({
117+
db,
118+
internalProductIds: linkedInternalIds,
119+
});
120+
const linkedExternalIds = new Set(
121+
linkedProducts.map((product) => product.id),
111122
);
112123

113124
const products = await ProductService.listFull({
@@ -116,11 +127,6 @@ export const handleGetLicenseProducts = createRoute({
116127
env,
117128
returnAll: all_versions,
118129
});
119-
const linkedExternalIds = new Set(
120-
products
121-
.filter((product) => linkedInternalIds.has(product.internal_id))
122-
.map((product) => product.id),
123-
);
124130
const licenseProducts = products.filter((product) =>
125131
linkedExternalIds.has(product.id),
126132
);
Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { SectionTag } from "@autumn/ui";
2-
import { LicenseIcon } from "./LicenseIcon";
32

4-
/** The "Licenses" section tag (icon + label), shared by the products list and
5-
* the customer view so the label stays identical. */
3+
/** The "Licenses" section tag, shared by the products list and the customer
4+
* view so the label stays identical. */
65
export const LicenseSectionTag = ({ className }: { className?: string }) => (
7-
<SectionTag className={className}>
8-
<span className="flex items-center gap-1">
9-
<LicenseIcon size={11} className="text-subtle" />
10-
Licenses
11-
</span>
12-
</SectionTag>
6+
<SectionTag className={className}>Licenses</SectionTag>
137
);

vite/src/views/products/plan/components/edit-plan-feature/AdvancedSettings.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
getFeatureUsageType,
1414
} from "@/utils/product/entitlementUtils";
1515
import { useProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
16-
import { useHasEntityFeatureId } from "../../hooks/useHasEntityFeatureId";
1716
import { EntityFeatureConfig } from "./advanced-settings/EntityFeatureConfig";
1817
import { ProrationConfig } from "./advanced-settings/ProrationConfig";
1918
import { ResetIntervalConfig } from "./advanced-settings/ResetIntervalConfig";
@@ -23,25 +22,19 @@ import { UsageLimit } from "./advanced-settings/UsageLimit";
2322
export function AdvancedSettings() {
2423
const { features } = useFeaturesQuery();
2524
const { item } = useProductItemContext();
26-
const { hasEntityFeatureId } = useHasEntityFeatureId();
2725

2826
if (!item) return null;
2927

3028
const usageType = getFeatureUsageType({ item, features });
3129
const hasCreditSystem = getFeatureCreditSystem({ item, features });
3230
const isPriced = isFeaturePriceItem(item);
3331

34-
// Check if there are other continuous use features (for entity feature config)
35-
const hasOtherContinuousFeatures = features.some(
36-
(f) =>
37-
f.config?.usage_type === FeatureUsageType.Continuous &&
38-
f.id !== item.feature_id,
39-
);
40-
4132
// Determine what will show in Advanced section
4233
const showUsageLimits = isPriced;
4334
const showRollover = hasCreditSystem || usageType === FeatureUsageType.Single;
44-
const showEntityFeature = hasEntityFeatureId && hasOtherContinuousFeatures;
35+
// Deprecated in favor of licenses. Only surface it for items that already
36+
// have it set, so existing plans keep working.
37+
const showEntityFeature = item.entity_feature_id != null;
4538
// Proration shows for prepaid or continuous use features (not consumable + pay-per-use)
4639
const showProration =
4740
isPriced &&

vite/src/views/products/plan/hooks/useHasEntityFeatureId.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

vite/src/views/settings/SettingsView.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BellIcon,
66
BotIcon,
77
BuildingIcon,
8+
CreditCardIcon,
89
MousePointerClickIcon,
910
PaletteIcon,
1011
ReceiptIcon,
@@ -24,12 +25,14 @@ import { CustomButtonsSection } from "./sections/CustomButtonsSection";
2425
import { InvoicesSection } from "./sections/InvoicesSection";
2526
import { MembersSection } from "./sections/MembersSection";
2627
import { OrganizationSection } from "./sections/OrganizationSection";
28+
import { SubscriptionSection } from "./sections/SubscriptionSection";
2729
import { TransitionRulesSection } from "./sections/TransitionRulesSection";
2830
import { UsageAlertsSection } from "./sections/UsageAlertsSection";
2931

3032
type SettingsTab =
3133
| "account"
3234
| "organization"
35+
| "subscription"
3336
| "members"
3437
| "agent"
3538
| "appearance"
@@ -65,6 +68,11 @@ const SETTINGS_GROUPS: readonly SettingsNavGroup[] = [
6568
label: "Organization",
6669
icon: <BuildingIcon className="size-4" />,
6770
},
71+
{
72+
id: "subscription",
73+
label: "Subscription",
74+
icon: <CreditCardIcon className="size-4" />,
75+
},
6876
{
6977
id: "members",
7078
label: "Members",
@@ -122,6 +130,7 @@ const SETTINGS_GROUPS: readonly SettingsNavGroup[] = [
122130
const SECTION_MAP: Record<SettingsTab, React.ComponentType> = {
123131
account: AccountSection,
124132
organization: OrganizationSection,
133+
subscription: SubscriptionSection,
125134
members: MembersSection,
126135
agent: AgentSection,
127136
appearance: AppearanceSection,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Button } from "@autumn/ui";
2+
import { useCustomer } from "autumn-js/react";
3+
import { useState } from "react";
4+
import { toast } from "sonner";
5+
import { SettingsSection } from "../SettingsSection";
6+
7+
export const SubscriptionSection = () => {
8+
const { openCustomerPortal } = useCustomer();
9+
const [isLoading, setIsLoading] = useState(false);
10+
11+
const handleOpenPortal = async () => {
12+
setIsLoading(true);
13+
try {
14+
await openCustomerPortal();
15+
} catch {
16+
toast.error("Failed to open billing portal");
17+
setIsLoading(false);
18+
}
19+
};
20+
21+
return (
22+
<SettingsSection
23+
title="Subscription"
24+
description="Manage your Autumn subscription and billing"
25+
card={{
26+
title: "Billing Portal",
27+
description:
28+
"View invoices, update your payment method, and manage your subscription",
29+
}}
30+
>
31+
<Button
32+
variant="primary"
33+
onClick={handleOpenPortal}
34+
isLoading={isLoading}
35+
>
36+
Open billing portal
37+
</Button>
38+
</SettingsSection>
39+
);
40+
};

0 commit comments

Comments
 (0)