Merge pull request #226 from sidgaikwad/Fixes/adding-new-deep-link-format-stripe

Fixes/adding new deep link format stripe
This commit is contained in:
John Yeo
2025-10-13 20:34:01 +01:00
committed by GitHub
5 changed files with 282 additions and 182 deletions

View File

@@ -2,13 +2,12 @@ import { AppEnv, ErrCode } from "@autumn/shared";
import Stripe from "stripe";
import { z } from "zod";
import { ensureStripeProductsWithEnv } from "@/external/stripe/stripeEnsureUtils.js";
import {
checkKeyValid,
createWebhookEndpoint,
checkKeyValid, createWebhookEndpoint,
} from "@/external/stripe/stripeOnboardingUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { encryptData } from "@/utils/encryptUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { nullish } from "@/utils/genUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { OrgService } from "../OrgService.js";
@@ -287,3 +286,17 @@ export const handleConnectStripe = async (req: any, res: any) =>
});
},
});
export const handleGetStripe = async (req: any, res: any) => {
try {
const org = await OrgService.getFromReq(req);
const stripeCli = createStripeCli({ org, env: req.env });
const account_details = await stripeCli.accounts.retrieve();
res.status(200).json(account_details);
} catch (error) {
handleRequestError({ req, error, res, action: "Get invoice" });
}
};

View File

@@ -10,7 +10,10 @@ import { createOrgResponse } from "./orgUtils.js";
import { handleGetUploadUrl } from "./handlers/handleGetUploadUrl.js";
import { handleDeleteOrg } from "./handlers/handleDeleteOrg.js";
import { handleGetInvites } from "./handlers/handleGetInvites.js";
import { handleConnectStripe } from "./handlers/handleConnectStripe.js";
import {
handleConnectStripe,
handleGetStripe,
} from "./handlers/handleConnectStripe.js";
import { handleDeleteStripe } from "./handlers/handleDeleteStripe.js";
import { handleGetOrg } from "./handlers/handleGetOrg.js";
@@ -29,6 +32,8 @@ orgRouter.delete("/delete-user", async (req: any, res) => {
orgRouter.get("", handleGetOrg);
orgRouter.get("/stripe", handleGetStripe);
orgRouter.post("/stripe", handleConnectStripe);
orgRouter.delete("/stripe", handleDeleteStripe);

View File

@@ -1,24 +1,36 @@
import { AppEnv } from "@autumn/shared";
export const getStripeCusLink = (customerId: string, env: AppEnv) => {
return `https://dashboard.stripe.com${
env == AppEnv.Live ? "" : "/test"
}/customers/${customerId}`;
export const getStripeCusLink = (
customerId: string,
env: AppEnv,
accountId?: string
) => {
return `https://dashboard.stripe.com${
env == AppEnv.Live ? "" : "/test"
}/${accountId}/customers/${customerId}`;
};
export const getStripeSubLink = (subscriptionId: string, env: AppEnv) => {
return `https://dashboard.stripe.com${
env == AppEnv.Live ? "" : "/test"
}/subscriptions/${subscriptionId}`;
export const getStripeSubLink = (
subscriptionId: string,
env: AppEnv,
accountId?: string
) => {
return `https://dashboard.stripe.com${
env == AppEnv.Live ? "" : "/test"
}/${accountId}/subscriptions/${subscriptionId}`;
};
export const getStripeSubScheduleLink = (scheduledId: string, env: AppEnv) => {
return `https://dashboard.stripe.com${
env == AppEnv.Live ? "" : "/test"
}/subscription_schedules/${scheduledId}`;
export const getStripeSubScheduleLink = (
scheduledId: string,
env: AppEnv,
accountId?: string
) => {
return `https://dashboard.stripe.com${
env == AppEnv.Live ? "" : "/test"
}/${accountId}/subscription_schedules/${scheduledId}`;
};
export const getStripeInvoiceLink = (stripeInvoice: any) => {
return `https://dashboard.stripe.com${
stripeInvoice.livemode ? "" : "/test"
}/invoices/${stripeInvoice.id || stripeInvoice.stripe_id}`;
return `https://dashboard.stripe.com${
stripeInvoice.livemode ? "" : "/test"
}/invoices/${stripeInvoice.id || stripeInvoice.stripe_id}`;
};

View File

@@ -10,119 +10,158 @@ import { Link } from "react-router";
import { useEnv } from "@/utils/envUtils";
import { SidebarLabel } from "@/components/general/sidebar/sidebar-label";
import { useCusQuery } from "../../hooks/useCusQuery";
import { useOrg } from "@/hooks/common/useOrg";
import Stripe from "stripe";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { toast } from "sonner";
export const CustomerDetails = ({
setIsModalOpen,
setModalType,
setIsModalOpen,
setModalType,
}: {
setIsModalOpen: (isModalOpen: boolean) => void;
setModalType: (modalType: string) => void;
setIsModalOpen: (isModalOpen: boolean) => void;
setModalType: (modalType: string) => void;
}) => {
const { customer } = useCusQuery();
const env = useEnv();
const { customer } = useCusQuery();
const env = useEnv();
const { org } = useOrg();
return (
<div className="flex w-full border-b mt-[2.5px] p-4">
<SideAccordion title="Details" value="details">
<div className="grid grid-cols-8 auto-rows-[16px] gap-y-4 w-full items-center">
<SidebarLabel>ID</SidebarLabel>
<div className="col-span-6 justify-end flex">
<div className="w-full flex justify-end">
{customer.id ? (
<CopyButton text={customer.id} className="">
{customer.id}
</CopyButton>
) : (
<Button
variant="sidebarItem"
onClick={() => {
setIsModalOpen(true);
setModalType("customer");
}}
>
<span className="truncate text-t3">N/A</span>
</Button>
)}
</div>
</div>
const axiosInstance = useAxiosInstance();
<span className="text-t3 text-xs font-medium col-span-2">Name</span>
<div className="col-span-6 justify-end flex">
<Button
variant="sidebarItem"
onClick={() => {
setIsModalOpen(true);
setModalType("customer");
}}
>
<span className="truncate">
{customer.name || <span className="text-t3">None</span>}
</span>
</Button>
</div>
const getStripeAccountInfo = async () => {
try {
const { data } = await axiosInstance.get(`/organization/stripe`);
return data;
} catch (error) {
toast.error("Failed to get invoice URL");
return null;
}
};
<span className="text-t3 text-xs font-medium col-span-2">Email</span>
<div className="col-span-6 justify-end flex">
<Button
variant="sidebarItem"
onClick={() => {
setIsModalOpen(true);
setModalType("customer");
}}
>
<span className="truncate">
{customer.email || <span className="text-t3">None</span>}
</span>
</Button>
</div>
return (
<div className="flex w-full border-b mt-[2.5px] p-4">
<SideAccordion title="Details" value="details">
<div className="grid grid-cols-8 auto-rows-[16px] gap-y-4 w-full items-center">
<SidebarLabel>ID</SidebarLabel>
<div className="col-span-6 justify-end flex">
<div className="w-full flex justify-end">
{customer.id ? (
<CopyButton text={customer.id} className="">
{customer.id}
</CopyButton>
) : (
<Button
variant="sidebarItem"
onClick={() => {
setIsModalOpen(true);
setModalType("customer");
}}
>
<span className="truncate text-t3">N/A</span>
</Button>
)}
</div>
</div>
<span className="text-t3 text-xs font-medium col-span-2">
Fingerprint
</span>
<div className="col-span-6 justify-end flex">
<Button
variant="sidebarItem"
className="text-t2 px-2 h-fit py-0.5"
onClick={() => {
setIsModalOpen(true);
setModalType("customer");
}}
>
<span className="truncate">
{customer.fingerprint || <span className="text-t3">None</span>}
</span>
</Button>
</div>
<span className="text-t3 text-xs font-medium col-span-2">Name</span>
<div className="col-span-6 justify-end flex">
<Button
variant="sidebarItem"
onClick={() => {
setIsModalOpen(true);
setModalType("customer");
}}
>
<span className="truncate">
{customer.name || <span className="text-t3">None</span>}
</span>
</Button>
</div>
{customer.processor?.id && (
<>
<span className="text-t3 text-xs font-medium col-span-2 h-4">
Stripe
</span>
<div className="col-span-6">
<Link
className="!cursor-pointer hover:underline"
to={getStripeCusLink(customer.processor?.id, env)}
target="_blank"
>
<div className="flex items-center gap-2 justify-end">
<Button
variant="sidebarItem"
// className="bg-white border shadow-sm rounded-md gap-2 h-6 max-h-6 !py-0"
>
<FontAwesomeIcon
icon={faStripe}
className="!h-6 !w-6 text-t2"
/>
<ArrowUpRightFromSquare size={12} className="text-t2" />
</Button>
</div>
</Link>
</div>
</>
)}
</div>
</SideAccordion>
</div>
);
<span className="text-t3 text-xs font-medium col-span-2">Email</span>
<div className="col-span-6 justify-end flex">
<Button
variant="sidebarItem"
onClick={() => {
setIsModalOpen(true);
setModalType("customer");
}}
>
<span className="truncate">
{customer.email || <span className="text-t3">None</span>}
</span>
</Button>
</div>
<span className="text-t3 text-xs font-medium col-span-2">
Fingerprint
</span>
<div className="col-span-6 justify-end flex">
<Button
variant="sidebarItem"
className="text-t2 px-2 h-fit py-0.5"
onClick={() => {
setIsModalOpen(true);
setModalType("customer");
}}
>
<span className="truncate">
{customer.fingerprint || <span className="text-t3">None</span>}
</span>
</Button>
</div>
{customer.processor?.id && (
<>
<span className="text-t3 text-xs font-medium col-span-2 h-4">
Stripe
</span>
<div className="col-span-6">
<div className="!cursor-pointer hover:underline">
<div className="flex items-center gap-2 justify-end">
<Button
variant="sidebarItem"
className="!cursor-pointer hover:underline"
onClick={async () => {
try {
const result = await getStripeAccountInfo();
if (result) {
console.log("result", result);
window.open(
getStripeCusLink(
customer.processor?.id,
env,
result.id
),
"_blank"
);
} else {
window.location.href = getStripeCusLink(
customer.processor?.id,
env
);
}
} catch (err) {
console.error(
"Failed to get Stripe customer link:",
err
);
}
}}
>
<FontAwesomeIcon
icon={faStripe}
className="!h-6 !w-6 text-t2"
/>
<ArrowUpRightFromSquare size={12} className="text-t2" />
</Button>
</div>
</div>
</div>
</>
)}
</div>
</SideAccordion>
</div>
);
};

View File

@@ -1,67 +1,98 @@
import { useOrg } from "@/hooks/common/useOrg";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useEnv } from "@/utils/envUtils";
import { getStripeSubLink, getStripeSubScheduleLink } from "@/utils/linkUtils";
import { CusProductStatus, FullCusProduct } from "@autumn/shared";
import { ArrowUpRightFromSquare } from "lucide-react";
import React from "react";
import { Link } from "react-router";
import { toast } from "sonner";
export const CusProductStripeLink = ({
cusProduct,
cusProduct,
}: {
cusProduct: FullCusProduct;
cusProduct: FullCusProduct;
}) => {
const env = useEnv();
return (
<>
{cusProduct.subscription_ids &&
cusProduct.subscription_ids.length > 0 && (
<React.Fragment>
{cusProduct.subscription_ids.map((subId: string) => {
return (
<Link
key={subId}
to={getStripeSubLink(subId, env)}
target="_blank"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="flex justify-center items-center w-fit px-2 gap-2 h-6">
<ArrowUpRightFromSquare
size={12}
className="text-[#665CFF]"
/>
</div>
</Link>
);
})}
</React.Fragment>
)}
{cusProduct.status == CusProductStatus.Scheduled &&
cusProduct.scheduled_ids &&
cusProduct.scheduled_ids.length > 0 && (
<React.Fragment>
{cusProduct.scheduled_ids.map((subId: string) => {
return (
<Link
key={subId}
to={getStripeSubScheduleLink(subId, env)}
target="_blank"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="flex justify-center items-center w-fit px-2 gap-2 h-6">
<ArrowUpRightFromSquare
size={12}
className="text-[#665CFF]"
/>
</div>
</Link>
);
})}
</React.Fragment>
)}
</>
);
const env = useEnv();
const { org } = useOrg();
const axiosInstance = useAxiosInstance();
const getStripeAccountInfo = async () => {
try {
const { data } = await axiosInstance.get(`/organization/stripe`);
return data;
} catch (error) {
toast.error("Failed to get invoice URL");
return null;
}
};
return (
<>
{cusProduct.subscription_ids &&
cusProduct.subscription_ids.length > 0 && (
<React.Fragment>
{cusProduct.subscription_ids.map((subId: string) => {
return (
<div
key={subId}
onClick={async (e) => {
e.stopPropagation();
const account = await getStripeAccountInfo();
if (account) {
window.open(
getStripeSubLink(subId, env, account.id),
"_blank"
);
} else {
window.open(getStripeSubLink(subId, env));
}
}}
>
<div className="flex justify-center items-center w-fit px-2 gap-2 h-6">
<ArrowUpRightFromSquare
size={12}
className="text-[#665CFF]"
/>
</div>
</div>
);
})}
</React.Fragment>
)}
{cusProduct.status == CusProductStatus.Scheduled &&
cusProduct.scheduled_ids &&
cusProduct.scheduled_ids.length > 0 && (
<React.Fragment>
{cusProduct.scheduled_ids.map((subId: string) => {
return (
<div
key={subId}
onClick={async (e) => {
e.stopPropagation();
const account = await getStripeAccountInfo();
if (account) {
window.open(
getStripeSubScheduleLink(subId, env, account.id),
"_blank"
);
} else {
window.open(getStripeSubScheduleLink(subId, env));
}
}}
>
<div className="flex justify-center items-center w-fit px-2 gap-2 h-6">
<ArrowUpRightFromSquare
size={12}
className="text-[#665CFF]"
/>
</div>
</div>
);
})}
</React.Fragment>
)}
</>
);
};