Codex changes
Co-authored-by: Codex
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user