Skip to content

Commit f5e4420

Browse files
committed
feat(dashboard): upgrade autumn-js to v1.2.2 with full type migration
Migrate dashboard billing from autumn-js v0.1.85 to v1.2.2: - Rename Product→Plan, CustomerProduct→Subscription, CustomerFeature→Balance - Replace usePricingTable with useListPlans, cancel with updateSubscription - Convert all snake_case properties to camelCase (canceledAt, currentPeriodEnd, etc.) - Simplify AttachDialog, remove dialog/reward props from attach calls - Add includeCredentials to AutumnProvider for cross-origin session auth - Decouple utility types from SDK branded types to avoid dual-module mismatch
1 parent 30a72d3 commit f5e4420

16 files changed

Lines changed: 389 additions & 621 deletions

File tree

apps/dashboard/app/(main)/billing/components/credit-card-display.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function CreditCardDisplay({ customer }: CreditCardDisplayProps) {
1515
true
1616
);
1717

18-
const paymentMethod = customer?.payment_method;
18+
const paymentMethod = customer?.paymentMethod;
1919
const card = paymentMethod?.card;
2020

2121
if (!card) {
@@ -32,10 +32,10 @@ export function CreditCardDisplay({ customer }: CreditCardDisplayProps) {
3232
}
3333

3434
const cardHolder =
35-
paymentMethod?.billing_details?.name || customer?.name || "CARD HOLDER";
35+
paymentMethod?.billingDetails?.name || customer?.name || "CARD HOLDER";
3636
const last4 = card.last4 || "****";
37-
const expMonth = card.exp_month?.toString().padStart(2, "0") || "00";
38-
const expYear = card.exp_year?.toString().slice(-2) || "00";
37+
const expMonth = card.expMonth?.toString().padStart(2, "0") || "00";
38+
const expYear = card.expYear?.toString().slice(-2) || "00";
3939
const cardNumber = `•••• •••• •••• ${last4}`;
4040
const expiration = `${expMonth}/${expYear}`;
4141
const brand = (card.brand || "card").toLowerCase();

apps/dashboard/app/(main)/billing/history/page.tsx

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
ReceiptIcon,
99
XCircleIcon,
1010
} from "@phosphor-icons/react";
11-
import type { CustomerInvoice } from "autumn-js";
11+
import type { Invoice } from "autumn-js";
1212
import { memo, useMemo } from "react";
1313
import { EmptyState } from "@/components/empty-state";
1414
import { RightSidebar } from "@/components/right-sidebar";
@@ -26,16 +26,16 @@ export default function HistoryPage() {
2626

2727
const invoices = customerData?.invoices ?? [];
2828
const sortedInvoices = useMemo(
29-
() => [...invoices].sort((a, b) => b.created_at - a.created_at),
29+
() => [...invoices].sort((a, b) => b.createdAt - a.createdAt),
3030
[invoices]
3131
);
3232

3333
const subscriptionHistory = useMemo(() => {
34-
if (!customerData?.products?.length) {
34+
if (!customerData?.subscriptions?.length) {
3535
return [];
3636
}
37-
return customerData.products;
38-
}, [customerData?.products]);
37+
return customerData.subscriptions;
38+
}, [customerData?.subscriptions]);
3939

4040
if (isLoading) {
4141
return (
@@ -69,7 +69,7 @@ export default function HistoryPage() {
6969
) : (
7070
<div className="divide-y">
7171
{sortedInvoices.map((invoice) => (
72-
<InvoiceRow invoice={invoice} key={invoice.stripe_id} />
72+
<InvoiceRow invoice={invoice} key={invoice.stripeId} />
7373
))}
7474
</div>
7575
)}
@@ -86,8 +86,8 @@ export default function HistoryPage() {
8686
</p>
8787
) : (
8888
<div className="space-y-3">
89-
{subscriptionHistory.map((product) => (
90-
<SubscriptionItem key={product.id} product={product} />
89+
{subscriptionHistory.map((sub) => (
90+
<SubscriptionItem key={sub.id} sub={sub} />
9191
))}
9292
</div>
9393
)}
@@ -117,10 +117,10 @@ export default function HistoryPage() {
117117
const InvoiceRow = memo(function InvoiceRowComponent({
118118
invoice,
119119
}: {
120-
invoice: CustomerInvoice;
120+
invoice: Invoice;
121121
}) {
122122
const status = getInvoiceStatus(invoice.status);
123-
const formattedDate = dayjs(invoice.created_at).format("MMM D, YYYY");
123+
const formattedDate = dayjs(invoice.createdAt).format("MMM D, YYYY");
124124
const amount = formatCurrency(invoice.total, invoice.currency);
125125

126126
return (
@@ -156,7 +156,7 @@ const InvoiceRow = memo(function InvoiceRowComponent({
156156
<div>
157157
<div className="flex items-center gap-2">
158158
<span className="font-medium">
159-
Invoice #{invoice.stripe_id.slice(-8)}
159+
Invoice #{invoice.stripeId.slice(-8)}
160160
</span>
161161
<Badge variant={status.variant}>{status.label}</Badge>
162162
</div>
@@ -168,11 +168,13 @@ const InvoiceRow = memo(function InvoiceRowComponent({
168168
</div>
169169
</div>
170170

171-
{invoice.hosted_invoice_url && (
171+
{invoice.hostedInvoiceUrl && (
172172
<Button
173173
aria-label="View invoice"
174174
className="shrink-0"
175-
onClick={() => window.open(invoice.hosted_invoice_url, "_blank")}
175+
onClick={() =>
176+
window.open(invoice.hostedInvoiceUrl ?? "", "_blank")
177+
}
176178
size="sm"
177179
variant="secondary"
178180
>
@@ -185,21 +187,22 @@ const InvoiceRow = memo(function InvoiceRowComponent({
185187
);
186188
});
187189

188-
interface ProductStatus {
189-
id: string;
190-
name?: string | null;
191-
status?: string;
192-
started_at?: number | null;
193-
current_period_end?: number | null;
194-
canceled_at?: number | null;
195-
}
196-
197-
function SubscriptionItem({ product }: { product: ProductStatus }) {
198-
const renewalDate = product.current_period_end
199-
? dayjs(product.current_period_end)
200-
: null;
201-
const isCanceled = !!product.canceled_at;
202-
const isActive = product.status === "active";
190+
function SubscriptionItem({
191+
sub,
192+
}: {
193+
sub: {
194+
id: string;
195+
planId: string;
196+
plan?: { name?: string } | null;
197+
canceledAt?: number | null;
198+
currentPeriodEnd?: number | null;
199+
status?: string;
200+
startedAt?: number | null;
201+
};
202+
}) {
203+
const renewalDate = sub.currentPeriodEnd ? dayjs(sub.currentPeriodEnd) : null;
204+
const isCanceled = !!sub.canceledAt;
205+
const isActive = sub.status === "active";
203206

204207
return (
205208
<div className="flex items-start gap-3">
@@ -218,7 +221,7 @@ function SubscriptionItem({ product }: { product: ProductStatus }) {
218221
<div className="min-w-0 flex-1">
219222
<div className="flex items-center gap-2">
220223
<span className="truncate font-medium text-sm">
221-
{product.name || product.id}
224+
{sub.plan?.name ?? sub.planId}
222225
</span>
223226
{isActive && (
224227
<Badge className="bg-primary/10 text-primary" variant="secondary">
@@ -227,9 +230,7 @@ function SubscriptionItem({ product }: { product: ProductStatus }) {
227230
)}
228231
</div>
229232
<div className="text-muted-foreground text-xs">
230-
{product.started_at && (
231-
<span>Started {dayjs(product.started_at).fromNow()}</span>
232-
)}
233+
<span>Started {dayjs(sub.startedAt).fromNow()}</span>
233234
{renewalDate && (
234235
<span className="ml-2">
235236
· {isCanceled ? "Ends" : "Renews"} {renewalDate.fromNow()}
@@ -241,7 +242,7 @@ function SubscriptionItem({ product }: { product: ProductStatus }) {
241242
);
242243
}
243244

244-
function BillingSummary({ invoices }: { invoices: CustomerInvoice[] }) {
245+
function BillingSummary({ invoices }: { invoices: Invoice[] }) {
245246
const stats = useMemo(() => {
246247
const paid = invoices.filter((i) => i.status === "paid");
247248
const totalPaid = paid.reduce((sum, i) => sum + i.total, 0);
@@ -252,7 +253,7 @@ function BillingSummary({ invoices }: { invoices: CustomerInvoice[] }) {
252253
paidInvoices: paid.length,
253254
totalSpent: formatCurrency(totalPaid, currency),
254255
lastPayment: paid[0]
255-
? dayjs(paid[0].created_at).format("MMM D, YYYY")
256+
? dayjs(paid[0].createdAt).format("MMM D, YYYY")
256257
: "N/A",
257258
};
258259
}, [invoices]);

apps/dashboard/app/(main)/billing/hooks/use-billing.ts

Lines changed: 43 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import type { CustomerProduct } from "autumn-js";
2-
import { useCustomer, usePricingTable } from "autumn-js/react";
1+
import { useCustomer, useListPlans } from "autumn-js/react";
32
import { useMemo, useState } from "react";
43
import { toast } from "sonner";
5-
import AttachDialog from "@/components/autumn/attach-dialog";
64
import dayjs from "@/lib/dayjs";
75
import { trackCancelFeedbackAction } from "../actions/cancel-feedback-action";
86
import type { CancelFeedback } from "../components/cancel-subscription-dialog";
97
import {
108
calculateFeatureUsage,
119
type FeatureUsage,
12-
type PricingTier,
1310
} from "../utils/feature-usage";
1411
import { getStripeMetadata } from "../utils/stripe-metadata";
1512

@@ -22,20 +19,20 @@ export interface CancelTarget {
2219
currentPeriodEnd?: number;
2320
}
2421

25-
export type { Customer, CustomerInvoice as Invoice } from "autumn-js";
22+
export type { Customer, Invoice } from "autumn-js";
2623
export type { CancelFeedback } from "../components/cancel-subscription-dialog";
2724
export type { CustomerWithPaymentMethod } from "../types/billing";
2825

2926
export function useBilling(refetch?: () => void) {
30-
const { attach, cancel, check, track, openBillingPortal } = useCustomer();
27+
const { attach, updateSubscription, check, openCustomerPortal } =
28+
useCustomer();
3129
const [isLoading, setIsLoading] = useState(false);
3230
const [cancelTarget, setCancelTarget] = useState<CancelTarget | null>(null);
3331

3432
const handleUpgrade = async (planId: string) => {
3533
try {
3634
await attach({
37-
productId: planId,
38-
dialog: AttachDialog,
35+
planId,
3936
successUrl: `${window.location.origin}/billing`,
4037
metadata: getStripeMetadata(),
4138
});
@@ -49,9 +46,11 @@ export function useBilling(refetch?: () => void) {
4946
const handleCancel = async (planId: string, immediate = false) => {
5047
setIsLoading(true);
5148
try {
52-
await cancel({
53-
productId: planId,
54-
...(immediate && { cancelImmediately: true }),
49+
await updateSubscription({
50+
planId,
51+
cancelAction: immediate
52+
? "cancel_immediately"
53+
: "cancel_end_of_cycle",
5554
});
5655
toast.success(
5756
immediate
@@ -72,15 +71,20 @@ export function useBilling(refetch?: () => void) {
7271
}
7372
};
7473

75-
const getSubscriptionStatusDetails = (product: CustomerProduct) => {
76-
if (product.canceled_at && product.current_period_end) {
77-
return `Access until ${dayjs(product.current_period_end).format("MMM D, YYYY")}`;
74+
const getSubscriptionStatusDetails = (sub: {
75+
canceledAt?: number | null;
76+
currentPeriodEnd?: number | null;
77+
status?: string;
78+
startedAt?: number | null;
79+
}) => {
80+
if (sub.canceledAt && sub.currentPeriodEnd) {
81+
return `Access until ${dayjs(sub.currentPeriodEnd).format("MMM D, YYYY")}`;
7882
}
79-
if (product.status === "scheduled") {
80-
return `Starts on ${dayjs(product.started_at).format("MMM D, YYYY")}`;
83+
if (sub.status === "scheduled") {
84+
return `Starts on ${dayjs(sub.startedAt).format("MMM D, YYYY")}`;
8185
}
82-
if (product.current_period_end) {
83-
return `Renews on ${dayjs(product.current_period_end).format("MMM D, YYYY")}`;
86+
if (sub.currentPeriodEnd) {
87+
return `Renews on ${dayjs(sub.currentPeriodEnd).format("MMM D, YYYY")}`;
8488
}
8589
return "";
8690
};
@@ -108,9 +112,10 @@ export function useBilling(refetch?: () => void) {
108112
},
109113
onCancelDialogClose: () => setCancelTarget(null),
110114
onManageBilling: () =>
111-
openBillingPortal({ returnUrl: `${window.location.origin}/billing` }),
115+
openCustomerPortal({
116+
returnUrl: `${window.location.origin}/billing`,
117+
}),
112118
check,
113-
track,
114119
showCancelDialog: !!cancelTarget,
115120
cancelTarget,
116121
getSubscriptionStatusDetails,
@@ -119,69 +124,40 @@ export function useBilling(refetch?: () => void) {
119124

120125
export function useBillingData() {
121126
const {
122-
customer,
127+
data: customer,
123128
isLoading: isCustomerLoading,
124129
error: customerError,
125130
refetch: refetchCustomer,
126131
} = useCustomer({ expand: ["invoices", "payment_method"] });
127132

128133
const {
129-
products,
130-
isLoading: isPricingLoading,
131-
refetch: refetchPricing,
132-
} = usePricingTable();
133-
134-
const featureConfig = useMemo(() => {
135-
const limits: Record<string, number> = {};
136-
const tiers: Record<string, PricingTier[]> = {};
137-
138-
const activeProduct = customer?.products?.find(
139-
(p) =>
140-
p.status === "active" ||
141-
(p.canceled_at && dayjs(p.current_period_end).isAfter(dayjs()))
142-
);
143-
144-
for (const item of activeProduct?.items ?? []) {
145-
if (item.feature_id) {
146-
if (typeof item.included_usage === "number") {
147-
limits[item.feature_id] = item.included_usage;
148-
}
149-
// Tiers exist on priced_feature items but aren't in the type
150-
const itemTiers = (item as { tiers?: PricingTier[] }).tiers;
151-
if (Array.isArray(itemTiers)) {
152-
tiers[item.feature_id] = itemTiers;
153-
}
154-
}
155-
}
156-
157-
return { limits, tiers };
158-
}, [customer?.products]);
159-
160-
const usage: Usage = {
161-
features: customer?.features
162-
? Object.values(customer.features).map((f) =>
163-
calculateFeatureUsage(
164-
f,
165-
featureConfig.limits[f.id],
166-
featureConfig.tiers[f.id]
167-
)
134+
data: plans,
135+
isLoading: isPlansLoading,
136+
refetch: refetchPlans,
137+
} = useListPlans();
138+
139+
const usage: Usage = useMemo(
140+
() => ({
141+
features: customer?.balances
142+
? Object.values(customer.balances).map((bal) =>
143+
calculateFeatureUsage(bal)
168144
)
169-
: [],
170-
};
145+
: [],
146+
}),
147+
[customer?.balances]
148+
);
171149

172150
const refetch = () => {
173151
refetchCustomer();
174-
if (typeof refetchPricing === "function") {
175-
refetchPricing();
176-
}
152+
refetchPlans();
177153
};
178154

179155
return {
180-
products: products ?? [],
156+
plans: plans ?? [],
181157
usage,
182158
customer,
183159
customerData: customer,
184-
isLoading: isCustomerLoading || isPricingLoading,
160+
isLoading: isCustomerLoading || isPlansLoading,
185161
error: customerError,
186162
refetch,
187163
};

0 commit comments

Comments
 (0)