add url to draft edit stripe
This commit is contained in:
@@ -228,12 +228,20 @@ export const getFeaturePriceItemDisplay = ({
|
||||
? `${numberWithCommas(includedUsage)} ${includedFeatureName}`
|
||||
: "";
|
||||
|
||||
// Build price string (e.g., "$0.01")
|
||||
const priceStr =
|
||||
formatTiers({ item, currency, amountFormatOptions }) ??
|
||||
(notNullish(item.price)
|
||||
const volumeFlatAmount = isVolumeFlatAmountItem(item);
|
||||
|
||||
// For volume flat-amount items, tier.amount is always 0 — the real price
|
||||
// lives in tier.flat_amount, so we must pass useFlatAmount: true.
|
||||
let priceStr: string;
|
||||
if (volumeFlatAmount) {
|
||||
priceStr = formatTiers({ item, currency, amountFormatOptions, useFlatAmount: true }) ?? "";
|
||||
} else if (item.tiers) {
|
||||
priceStr = formatTiers({ item, currency, amountFormatOptions }) ?? "";
|
||||
} else {
|
||||
priceStr = notNullish(item.price)
|
||||
? formatAmount({ currency, amount: item.price, amountFormatOptions })
|
||||
: "");
|
||||
: "";
|
||||
}
|
||||
|
||||
// Build billing unit string (e.g., "credit" or "100 credits")
|
||||
const billingUnits = item.billing_units ?? 1;
|
||||
@@ -260,19 +268,12 @@ export const getFeaturePriceItemDisplay = ({
|
||||
|
||||
// Format output based on what we have
|
||||
if (hasIncludedUsage) {
|
||||
if (isVolumeFlatAmountItem(item)) {
|
||||
const flatPriceStr =
|
||||
formatTiers({
|
||||
item,
|
||||
currency,
|
||||
amountFormatOptions,
|
||||
useFlatAmount: true,
|
||||
}) ?? "";
|
||||
if (volumeFlatAmount) {
|
||||
const featureName = getFeatureName({ feature, units: 2 });
|
||||
return {
|
||||
primary_text: includedUsageStr,
|
||||
secondary_text:
|
||||
`then ${flatPriceStr} for ${featureName} ${intervalStr}`.trim(),
|
||||
`then ${priceStr} for ${featureName} ${intervalStr}`.trim(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -282,6 +283,20 @@ export const getFeaturePriceItemDisplay = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (volumeFlatAmount) {
|
||||
const featureName = getFeatureName({ feature, units: 2 });
|
||||
if (showInterval) {
|
||||
return {
|
||||
primary_text: priceStr,
|
||||
secondary_text: `for ${featureName} ${intervalStr}`.trim(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
primary_text: `${priceStr} for ${featureName}`.trim(),
|
||||
secondary_text: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (showInterval) {
|
||||
return {
|
||||
primary_text: priceStr,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { ArrowLeft, CheckCircleIcon, LinkIcon } from "@phosphor-icons/react";
|
||||
import { ArrowLeft, LinkIcon } from "@phosphor-icons/react";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { CopyButton } from "@/components/v2/buttons/CopyButton";
|
||||
import type { BillingLineItem } from "@/components/v2/LineItemsPreview";
|
||||
import { LineItemsPreview } from "@/components/v2/LineItemsPreview";
|
||||
import {
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
} from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import { PlanActivationSection } from "./SendInvoiceStage";
|
||||
import { UrlSuccessView } from "./UrlSuccessView";
|
||||
|
||||
export interface GenerateCheckoutSubmitParams {
|
||||
enablePlanImmediately: boolean;
|
||||
@@ -53,13 +52,13 @@ export function GenerateCheckoutStage({
|
||||
const { paymentUrl } = await onSubmit({
|
||||
enablePlanImmediately: enableImmediately,
|
||||
});
|
||||
if (paymentUrl) {
|
||||
setCompletedCheckoutUrl(paymentUrl);
|
||||
navigator.clipboard.writeText(paymentUrl);
|
||||
toast.success("Checkout URL copied to clipboard");
|
||||
} else {
|
||||
toast.error("No checkout URL was returned. Please try again.");
|
||||
}
|
||||
if (paymentUrl) {
|
||||
setCompletedCheckoutUrl(paymentUrl);
|
||||
navigator.clipboard.writeText(paymentUrl);
|
||||
toast.success("Checkout URL copied to clipboard");
|
||||
} else {
|
||||
toast.error("No checkout URL was returned. Please try again.");
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -67,46 +66,17 @@ export function GenerateCheckoutStage({
|
||||
|
||||
if (completedCheckoutUrl) {
|
||||
return (
|
||||
<>
|
||||
<SheetHeader
|
||||
title="Checkout URL Generated"
|
||||
description={
|
||||
productName
|
||||
? `Checkout session created for ${productName}`
|
||||
: "Checkout session has been created"
|
||||
}
|
||||
noSeparator
|
||||
/>
|
||||
|
||||
<SheetSection withSeparator={false}>
|
||||
<div className="flex flex-col items-center gap-2 pt-4">
|
||||
<div className="size-10 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<CheckCircleIcon
|
||||
size={24}
|
||||
weight="duotone"
|
||||
className="text-green-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-t2 text-center">
|
||||
The checkout URL has been generated and copied to your clipboard.
|
||||
</p>
|
||||
</div>
|
||||
</SheetSection>
|
||||
|
||||
<SheetFooter className="flex flex-col grid-cols-1 mt-0">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => window.open(completedCheckoutUrl, "_blank")}
|
||||
>
|
||||
Open checkout URL
|
||||
</Button>
|
||||
<CopyButton
|
||||
text={completedCheckoutUrl}
|
||||
innerClassName="text-xs text-t3 font-mono w-96"
|
||||
/>
|
||||
</SheetFooter>
|
||||
</>
|
||||
<UrlSuccessView
|
||||
title="Checkout URL Generated"
|
||||
description={
|
||||
productName
|
||||
? `Checkout session created for ${productName}`
|
||||
: "Checkout session has been created"
|
||||
}
|
||||
message="The checkout URL has been generated and copied to your clipboard."
|
||||
buttonLabel="Open checkout URL"
|
||||
url={completedCheckoutUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import type { AppEnv } from "@autumn/shared";
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircleIcon,
|
||||
HourglassIcon,
|
||||
LightningIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { ArrowLeft, HourglassIcon, LightningIcon } from "@phosphor-icons/react";
|
||||
import { format } from "date-fns";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { CopyButton } from "@/components/v2/buttons/CopyButton";
|
||||
import { PanelButton } from "@/components/v2/buttons/PanelButton";
|
||||
import { Input } from "@/components/v2/inputs/Input";
|
||||
import type { BillingLineItem } from "@/components/v2/LineItemsPreview";
|
||||
@@ -22,6 +16,7 @@ import {
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { getStripeInvoiceLink } from "@/utils/linkUtils";
|
||||
import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
import { UrlSuccessView } from "./UrlSuccessView";
|
||||
|
||||
export interface SendInvoiceSubmitParams {
|
||||
enableProductImmediately: boolean;
|
||||
@@ -117,6 +112,9 @@ export function SendInvoiceStage({
|
||||
const [completedInvoiceUrl, setCompletedInvoiceUrl] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [completedDraftUrl, setCompletedDraftUrl] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeAction, setActiveAction] = useState<"draft" | "finalize" | null>(
|
||||
null,
|
||||
);
|
||||
@@ -149,7 +147,9 @@ export function SendInvoiceStage({
|
||||
finalizeInvoice: false,
|
||||
});
|
||||
if (stripeId) {
|
||||
window.open(getInvoiceUrl(stripeId), "_blank");
|
||||
const invoiceUrl = getInvoiceUrl(stripeId);
|
||||
window.open(invoiceUrl, "_blank");
|
||||
setCompletedDraftUrl(invoiceUrl);
|
||||
}
|
||||
} finally {
|
||||
setActiveAction(null);
|
||||
@@ -175,48 +175,35 @@ export function SendInvoiceStage({
|
||||
|
||||
const needsEmail = !customer?.email && !emailSaved;
|
||||
|
||||
if (completedDraftUrl) {
|
||||
return (
|
||||
<UrlSuccessView
|
||||
title="Invoice Drafted"
|
||||
description={
|
||||
productName
|
||||
? `Draft invoice for ${productName} created in Stripe`
|
||||
: "Draft invoice created in Stripe"
|
||||
}
|
||||
message="Stripe should have opened in a new tab. If it was blocked, use the link below."
|
||||
buttonLabel="Open in Stripe"
|
||||
url={completedDraftUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (completedInvoiceUrl) {
|
||||
return (
|
||||
<>
|
||||
<SheetHeader
|
||||
title="Invoice Sent"
|
||||
description={
|
||||
productName
|
||||
? `Invoice for ${productName} has been finalized and sent`
|
||||
: "Invoice has been finalized and sent"
|
||||
}
|
||||
noSeparator
|
||||
/>
|
||||
|
||||
<SheetSection withSeparator={false}>
|
||||
<div className="flex flex-col items-center gap-2 pt-4">
|
||||
<div className="size-10 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<CheckCircleIcon
|
||||
size={24}
|
||||
weight="duotone"
|
||||
className="text-green-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-t2 text-center">
|
||||
The invoice has been finalized and sent to the customer.
|
||||
</p>
|
||||
</div>
|
||||
</SheetSection>
|
||||
|
||||
<SheetFooter className="flex flex-col grid-cols-1 mt-0">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => window.open(completedInvoiceUrl, "_blank")}
|
||||
>
|
||||
View Stripe invoice
|
||||
</Button>
|
||||
<CopyButton
|
||||
text={completedInvoiceUrl}
|
||||
innerClassName="text-xs text-t3 font-mono w-96"
|
||||
/>
|
||||
</SheetFooter>
|
||||
</>
|
||||
<UrlSuccessView
|
||||
title="Invoice Sent"
|
||||
description={
|
||||
productName
|
||||
? `Invoice for ${productName} has been finalized and sent`
|
||||
: "Invoice has been finalized and sent"
|
||||
}
|
||||
message="The invoice has been finalized and sent to the customer."
|
||||
buttonLabel="View Stripe invoice"
|
||||
url={completedInvoiceUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
55
vite/src/components/forms/shared/UrlSuccessView.tsx
Normal file
55
vite/src/components/forms/shared/UrlSuccessView.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { CheckCircleIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { CopyButton } from "@/components/v2/buttons/CopyButton";
|
||||
import {
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
} from "@/components/v2/sheets/SharedSheetComponents";
|
||||
|
||||
export function UrlSuccessView({
|
||||
title,
|
||||
description,
|
||||
message,
|
||||
buttonLabel,
|
||||
url,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
message: string;
|
||||
buttonLabel: string;
|
||||
url: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<SheetHeader title={title} description={description} noSeparator />
|
||||
|
||||
<SheetSection withSeparator={false}>
|
||||
<div className="flex flex-col items-center gap-2 pt-4">
|
||||
<div className="size-10 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<CheckCircleIcon
|
||||
size={24}
|
||||
weight="duotone"
|
||||
className="text-green-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-t2 text-center">{message}</p>
|
||||
</div>
|
||||
</SheetSection>
|
||||
|
||||
<SheetFooter className="flex flex-col grid-cols-1 mt-0">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => window.open(url, "_blank")}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
<CopyButton
|
||||
text={url}
|
||||
innerClassName="text-xs text-t3 font-mono w-96"
|
||||
/>
|
||||
</SheetFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user