Codex changes

Co-authored-by: Codex
This commit is contained in:
amianthus
2026-02-15 22:15:43 +00:00
parent a668c5d9e9
commit e5589bfa7d
7 changed files with 261 additions and 32 deletions

View File

@@ -148,11 +148,21 @@ const listVercelPlansForOrg = async ({
metadata?: Record<string, any>;
canCancel?: boolean;
}) => {
const allowedProductIds =
env === AppEnv.Live
? org.processor_configs?.vercel?.allowed_product_ids_live
: org.processor_configs?.vercel?.allowed_product_ids_sandbox;
const allowedIds =
allowedProductIds !== undefined
? allowedProductIds
: org.processor_configs?.vercel?.allowed_product_ids || [];
const products = await ProductService.listFull({
db,
orgId: org.id,
env,
archived: false,
inIds: allowedIds.length > 0 ? allowedIds : undefined,
});
sortProductsByPrice({ products });

View File

@@ -1,4 +1,9 @@
import { cusProductToProduct } from "@autumn/shared";
import {
cusProductToProduct,
mapToProductV2,
type FullCusProduct,
productV2ToBasePrice,
} from "@autumn/shared";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { CusService } from "@/internal/customers/CusService.js";
import type { VercelBillingPlan } from "../../misc/vercelTypes.js";
@@ -26,15 +31,29 @@ export const handleGetInstallation = createRoute({
);
}
const getPlanAmount = (cusProduct: FullCusProduct) => {
const product = cusProductToProduct({ cusProduct });
const productV2 = mapToProductV2({ product });
const basePrice = productV2ToBasePrice({ product: productV2 });
return basePrice?.price ?? 0;
};
const nonAddonProducts = (customer.customer_products || []).filter(
(customerProduct) => !customerProduct.product.is_add_on,
);
const customerProduct =
nonAddonProducts.sort((a, b) => getPlanAmount(b) - getPlanAmount(a))[0] ??
customer.customer_products?.[0];
return c.json(
{
notification: null,
billingPlan:
// edge case: [0] = add-on [1] = main
customer.customer_products?.[0] !== undefined
customerProduct !== undefined
? (productToBillingPlan({
product: cusProductToProduct({
cusProduct: customer.customer_products?.[0],
cusProduct: customerProduct,
}),
orgCurrency: org?.default_currency ?? "usd",
}) satisfies VercelBillingPlan)

View File

@@ -36,6 +36,9 @@ export const getVercelConfigDisplay = ({
webhook_url: undefined,
custom_payment_method: undefined,
marketplace_mode: undefined,
allowed_product_ids: undefined,
allowed_product_ids_live: undefined,
allowed_product_ids_sandbox: undefined,
};
}
@@ -64,15 +67,34 @@ export const getVercelConfigDisplay = ({
webhook_url: mask(webhookUrl, 8, 6),
custom_payment_method: mask(customPaymentMethod, 5, 3),
marketplace_mode: vercelConfig.marketplace_mode,
allowed_product_ids: vercelConfig.allowed_product_ids,
allowed_product_ids_live: vercelConfig.allowed_product_ids_live,
allowed_product_ids_sandbox: vercelConfig.allowed_product_ids_sandbox,
};
};
export const handleUpsertVercelConfig = createRoute({
body: UpsertVercelProcessorConfigSchema,
handler: async (c) => {
const { db, org } = c.get("ctx");
const { db, org, env } = c.get("ctx");
const body = c.req.valid("json");
const normalizeAllowedProductIds = (ids: string[] | undefined) => {
return Array.from(new Set(ids?.map((id) => id.trim()).filter((id) => !!id)));
};
const liveAllowedProductIds =
body.allowed_product_ids_live !== undefined
? normalizeAllowedProductIds(body.allowed_product_ids_live)
: undefined;
const sandboxAllowedProductIds =
body.allowed_product_ids_sandbox !== undefined
? normalizeAllowedProductIds(body.allowed_product_ids_sandbox)
: undefined;
const allowedProductIds =
body.allowed_product_ids !== undefined
? normalizeAllowedProductIds(body.allowed_product_ids)
: undefined;
// Merge with existing processor_configs to avoid unsetting fields
const existingVercelConfig =
@@ -87,6 +109,18 @@ export const handleUpsertVercelConfig = createRoute({
}
: undefined;
const allowedProductIdsUpdates: Partial<VercelProcessorConfig> = {};
if (allowedProductIds !== undefined) {
allowedProductIdsUpdates.allowed_product_ids = allowedProductIds;
}
if (env === AppEnv.Live && liveAllowedProductIds !== undefined) {
allowedProductIdsUpdates.allowed_product_ids_live = liveAllowedProductIds;
}
if (env === AppEnv.Sandbox && sandboxAllowedProductIds !== undefined) {
allowedProductIdsUpdates.allowed_product_ids_sandbox =
sandboxAllowedProductIds;
}
await OrgService.update({
db,
orgId: org.id,
@@ -95,6 +129,7 @@ export const handleUpsertVercelConfig = createRoute({
...org.processor_configs,
vercel: {
...existingVercelConfig,
...allowedProductIdsUpdates,
// Live fields
...(body.client_integration_id
? { client_integration_id: body.client_integration_id }

View File

@@ -245,6 +245,11 @@ export const createOrgResponse = ({
webhook_url: vercelConnection.webhook_url,
custom_payment_method: vercelConnection.custom_payment_method,
marketplace_mode: vercelConnection.marketplace_mode,
allowed_product_ids: vercelConnection.allowed_product_ids,
allowed_product_ids_live:
vercelConnection.allowed_product_ids_live,
allowed_product_ids_sandbox:
vercelConnection.allowed_product_ids_sandbox,
},
revenuecat: {
connected: revenueCatConnection.connected,

View File

@@ -53,6 +53,9 @@ export const VercelProcessorConfigSchema = z.object({
sandbox_client_secret: z.string().optional(),
sandbox_webhook_url: z.string().optional(),
webhook_url: z.string(),
allowed_product_ids: z.array(z.string().min(1)).optional(),
allowed_product_ids_live: z.array(z.string().min(1)).optional(),
allowed_product_ids_sandbox: z.array(z.string().min(1)).optional(),
custom_payment_method: z
.object({
live: z.string().optional(),
@@ -78,6 +81,9 @@ export const UpsertVercelProcessorConfigSchema = z.object({
sandbox_client_id: z.string().min(8).optional(),
sandbox_client_secret: z.string().min(8).optional(),
sandbox_webhook_url: z.string().min(14).optional(),
allowed_product_ids: z.array(z.string().min(1)).optional(),
allowed_product_ids_live: z.array(z.string().min(1)).optional(),
allowed_product_ids_sandbox: z.array(z.string().min(1)).optional(),
custom_payment_method: z
.object({
live: z.string().min(8).optional(),

View File

@@ -35,6 +35,9 @@ export const FrontendOrgSchema = z.object({
webhook_url: z.string().optional(),
custom_payment_method: z.string().optional(),
marketplace_mode: z.enum(VercelMarketplaceMode).optional(),
allowed_product_ids: z.array(z.string()).optional(),
allowed_product_ids_live: z.array(z.string()).optional(),
allowed_product_ids_sandbox: z.array(z.string()).optional(),
}),
revenuecat: z.object({
connected: z.boolean(),

View File

@@ -3,7 +3,7 @@ import type {
VercelMarketplaceMode,
} from "@autumn/shared";
import type { AxiosInstance } from "axios";
import { useState } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { AppPortal } from "svix-react";
import { Button } from "@/components/v2/buttons/Button";
@@ -24,8 +24,10 @@ import {
} from "@/components/v2/cards/Card";
import { FormLabel } from "@/components/v2/form/FormLabel";
import { Input } from "@/components/v2/inputs/Input";
import { TagSelect } from "@/components/v2/selects/TagSelect";
import { useTheme } from "@/contexts/ThemeProvider";
import { useOrg } from "@/hooks/common/useOrg";
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
import { useVercelQuery } from "@/hooks/queries/useVercelQuery";
import { OrgService } from "@/services/OrgService";
import { useAxiosInstance } from "@/services/useAxiosInstance";
@@ -33,6 +35,16 @@ import { useEnv } from "@/utils/envUtils";
import { getBackendErr } from "@/utils/genUtils";
import LoadingScreen from "@/views/general/LoadingScreen";
type VercelConfigState = {
client_integration_id: string;
client_secret: string;
webhook_url: string;
custom_payment_method: string;
marketplace_mode: VercelMarketplaceMode;
allowed_product_ids_live: string[];
allowed_product_ids_sandbox: string[];
};
export const ConfigureVercel = () => {
const { org, isLoading, mutate } = useOrg();
const {
@@ -40,30 +52,84 @@ export const ConfigureVercel = () => {
isLoading: isVercelLoading,
error: vercelError,
} = useVercelQuery();
const { products } = useProductsQuery();
const env = useEnv();
const axiosInstance = useAxiosInstance();
const [vercelConfig, setVercelConfig] = useState({
const [vercelConfig, setVercelConfig] = useState<VercelConfigState>({
client_integration_id: "",
client_secret: "",
webhook_url: "",
custom_payment_method: "",
marketplace_mode: "" as VercelMarketplaceMode,
allowed_product_ids_live: [],
allowed_product_ids_sandbox: [],
});
const activeAllowedProductIds =
env === "live"
? vercelConfig.allowed_product_ids_live
: vercelConfig.allowed_product_ids_sandbox;
const setActiveAllowedProductIds = (values: string[]) => {
setVercelConfig((prev) => ({
...prev,
...(env === "live"
? { allowed_product_ids_live: values }
: { allowed_product_ids_sandbox: values }),
}));
};
useEffect(() => {
const vercelOrgConfig = org?.processor_configs?.vercel;
if (!vercelOrgConfig) {
return;
}
const fallbackAllowedProductIds = vercelOrgConfig.allowed_product_ids ?? [];
setVercelConfig((prev) => ({
...prev,
allowed_product_ids_live:
vercelOrgConfig.allowed_product_ids_live ??
fallbackAllowedProductIds,
allowed_product_ids_sandbox:
vercelOrgConfig.allowed_product_ids_sandbox ??
fallbackAllowedProductIds,
}));
}, [org]);
const productOptions = products
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((product) => ({
label: `${product.name} (${product.id})`,
value: product.id,
}));
const currentClientIntegrationId =
env === "live"
? org?.processor_configs?.vercel?.client_integration_id
: org?.processor_configs?.vercel?.sandbox_client_id;
const currentClientSecret =
env === "live"
? org?.processor_configs?.vercel?.client_secret
: org?.processor_configs?.vercel?.sandbox_client_secret;
const currentWebhookUrl =
env === "live"
? org?.processor_configs?.vercel?.webhook_url
: org?.processor_configs?.vercel?.sandbox_webhook_url;
const currentCustomPaymentMethod =
env === "live"
? org?.processor_configs?.vercel?.custom_payment_method
: org?.processor_configs?.vercel?.custom_payment_method;
const { isDark } = useTheme();
const handleSaveVercelConfig = async (
axiosInstance: AxiosInstance,
vercelConfig: {
client_integration_id?: string;
client_secret?: string;
webhook_url?: string;
custom_payment_method?: string;
marketplace_mode?: VercelMarketplaceMode;
},
vercelConfig: VercelConfigState,
) => {
try {
// Map generic field names to env-specific field names
const filteredConfig: UpsertVercelProcessorConfig = {};
// Map to correct field names based on current env
@@ -104,6 +170,14 @@ export const ConfigureVercel = () => {
filteredConfig.marketplace_mode = vercelConfig.marketplace_mode;
}
if (env === "live") {
filteredConfig.allowed_product_ids_live =
vercelConfig.allowed_product_ids_live;
} else {
filteredConfig.allowed_product_ids_sandbox =
vercelConfig.allowed_product_ids_sandbox;
}
const res = await OrgService.upsertVercelConfig(
axiosInstance,
filteredConfig,
@@ -111,15 +185,15 @@ export const ConfigureVercel = () => {
if (res.status === 200) {
toast.success("Vercel config updated successfully");
await mutate();
// Clear the form after successful update
setVercelConfig({
// Clear credential fields, keep product filters
setVercelConfig((prev) => ({
...prev,
client_integration_id: "",
client_secret: "",
webhook_url: "",
custom_payment_method: "",
marketplace_mode:
filteredConfig.marketplace_mode as VercelMarketplaceMode,
});
marketplace_mode: filteredConfig.marketplace_mode as VercelMarketplaceMode,
}));
} else {
toast.error("Failed to update Vercel config");
}
@@ -158,13 +232,13 @@ export const ConfigureVercel = () => {
<Input
value={vercelConfig.client_integration_id || ""}
onChange={(e) =>
setVercelConfig({
...vercelConfig,
setVercelConfig((prev) => ({
...prev,
client_integration_id: e.target.value,
})
}))
}
placeholder={
org?.processor_configs?.vercel?.client_integration_id ||
currentClientIntegrationId ||
"eg. oac_2ttbjWcOQ0pyH1v9wYkROKB3"
}
/>
@@ -176,17 +250,34 @@ export const ConfigureVercel = () => {
<Input
value={vercelConfig.client_secret || ""}
onChange={(e) =>
setVercelConfig({
...vercelConfig,
setVercelConfig((prev) => ({
...prev,
client_secret: e.target.value,
})
}))
}
placeholder={
org?.processor_configs?.vercel?.client_secret ||
currentClientSecret ||
"eg. VAxvZFz8ST4d5b9pa2EuXkWG"
}
/>
</div>
<div>
<FormLabel className="mb-1">
<span className="text-t2">Webhook URL</span>
</FormLabel>
<Input
value={vercelConfig.webhook_url || ""}
onChange={(e) =>
setVercelConfig((prev) => ({
...prev,
webhook_url: e.target.value,
}))
}
placeholder={
currentWebhookUrl || "eg. https://example.com/api/webhooks"
}
/>
</div>
<div>
<FormLabel className="mb-1">
<span className="text-t2">
@@ -208,17 +299,77 @@ export const ConfigureVercel = () => {
<Input
value={vercelConfig.custom_payment_method || ""}
onChange={(e) =>
setVercelConfig({
...vercelConfig,
setVercelConfig((prev) => ({
...prev,
custom_payment_method: e.target.value,
})
}))
}
placeholder={
org?.processor_configs?.vercel?.custom_payment_method ||
currentCustomPaymentMethod ||
"eg. cpmt_Yij7OBT6Fxu0UOa12XguA0vGB"
}
/>
</div>
<div className="col-span-2">
<FormLabel className="mb-1">
<span className="text-t2">Allowed Product IDs (Optional)</span>
</FormLabel>
<p className="text-t3 text-sm mb-2">
Optional: choose product IDs to show in Vercel plans.
</p>
<TagSelect
value={activeAllowedProductIds}
onChange={setActiveAllowedProductIds}
options={productOptions}
placeholder="Select product IDs to include"
showAllProducts={activeAllowedProductIds.length === 0}
renderContent={(setOpen) => (
<>
{productOptions.length === 0 ? (
<div className="p-2 text-t6">No products found</div>
) : (
productOptions.map((option) => {
const isSelected =
activeAllowedProductIds.includes(option.value);
return (
<div
key={option.value}
role="button"
tabIndex={0}
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
onClick={() => {
const nextValue = isSelected
? activeAllowedProductIds.filter(
(id) => id !== option.value,
)
: [...activeAllowedProductIds, option.value];
setActiveAllowedProductIds(nextValue);
setOpen(false);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const nextValue = isSelected
? activeAllowedProductIds.filter(
(id) => id !== option.value,
)
: [...activeAllowedProductIds, option.value];
setActiveAllowedProductIds(nextValue);
setOpen(false);
}
}}
>
<div className="flex items-center justify-between w-full">
<span className="truncate">{option.label}</span>
</div>
</div>
);
})
)}
</>
)}
/>
</div>
</div>
<div className="flex gap-2 mt-2">