mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-08 05:56:38 +00:00
Checkout flow works
This commit is contained in:
@@ -45,6 +45,10 @@ export const privateConfigSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform(getEnvOrYaml("REO_CLIENT_ID")),
|
.transform(getEnvOrYaml("REO_CLIENT_ID")),
|
||||||
|
fossorial_api: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("https://api.fossorial.io"),
|
||||||
fossorial_api_key: z
|
fossorial_api_key: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -164,7 +168,10 @@ export const privateConfigSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
stripe: z
|
stripe: z
|
||||||
.object({
|
.object({
|
||||||
secret_key: z.string().optional().transform(getEnvOrYaml("STRIPE_SECRET_KEY")),
|
secret_key: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(getEnvOrYaml("STRIPE_SECRET_KEY")),
|
||||||
webhook_secret: z
|
webhook_secret: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
35
server/private/routers/billing/hooks/getSubType.ts
Normal file
35
server/private/routers/billing/hooks/getSubType.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
getLicensePriceSet,
|
||||||
|
} from "@server/lib/billing/licenses";
|
||||||
|
import {
|
||||||
|
getTierPriceSet,
|
||||||
|
} from "@server/lib/billing/tiers";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription>): "saas" | "license" {
|
||||||
|
// Determine subscription type by checking subscription items
|
||||||
|
let type: "saas" | "license" = "saas";
|
||||||
|
if (Array.isArray(fullSubscription.items?.data)) {
|
||||||
|
for (const item of fullSubscription.items.data) {
|
||||||
|
const priceId = item.price.id;
|
||||||
|
|
||||||
|
// Check if price ID matches any license price
|
||||||
|
const licensePrices = Object.values(getLicensePriceSet());
|
||||||
|
|
||||||
|
if (licensePrices.includes(priceId)) {
|
||||||
|
type = "license";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if price ID matches any tier price (saas)
|
||||||
|
const tierPrices = Object.values(getTierPriceSet());
|
||||||
|
|
||||||
|
if (tierPrices.includes(priceId)) {
|
||||||
|
type = "saas";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ import logger from "@server/logger";
|
|||||||
import stripe from "#private/lib/stripe";
|
import stripe from "#private/lib/stripe";
|
||||||
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
|
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
|
||||||
import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
|
import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
|
||||||
|
import { getSubType } from "./getSubType";
|
||||||
|
import privateConfig from "#private/lib/config";
|
||||||
|
|
||||||
export async function handleSubscriptionCreated(
|
export async function handleSubscriptionCreated(
|
||||||
subscription: Stripe.Subscription
|
subscription: Stripe.Subscription
|
||||||
@@ -123,7 +125,16 @@ export async function handleSubscriptionCreated(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleSubscriptionLifesycle(customer.orgId, subscription.status);
|
const type = getSubType(fullSubscription);
|
||||||
|
if (type === "saas") {
|
||||||
|
logger.debug(
|
||||||
|
`Handling SAAS subscription lifecycle for org ${customer.orgId}`
|
||||||
|
);
|
||||||
|
// we only need to handle the limit lifecycle for saas subscriptions not for the licenses
|
||||||
|
await handleSubscriptionLifesycle(
|
||||||
|
customer.orgId,
|
||||||
|
subscription.status
|
||||||
|
);
|
||||||
|
|
||||||
const [orgUserRes] = await db
|
const [orgUserRes] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -143,6 +154,59 @@ export async function handleSubscriptionCreated(
|
|||||||
moveEmailToAudience(email, AudienceIds.Subscribed);
|
moveEmailToAudience(email, AudienceIds.Subscribed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (type === "license") {
|
||||||
|
logger.debug(
|
||||||
|
`License subscription created for org ${customer.orgId}, no lifecycle handling needed.`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Retrieve the client_reference_id from the checkout session
|
||||||
|
let licenseId: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessions = await stripe!.checkout.sessions.list({
|
||||||
|
subscription: subscription.id,
|
||||||
|
limit: 1
|
||||||
|
});
|
||||||
|
if (sessions.data.length > 0) {
|
||||||
|
licenseId = sessions.data[0].client_reference_id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!licenseId) {
|
||||||
|
logger.error(
|
||||||
|
`No client_reference_id found for subscription ${subscription.id}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Retrieved licenseId ${licenseId} from checkout session for subscription ${subscription.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, // this says enterprise but it does both
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"api-key":
|
||||||
|
privateConfig.getRawPrivateConfig().server
|
||||||
|
.fossorial_api_key!,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
licenseId: parseInt(licenseId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
logger.debug("Fossorial API response:", { data });
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating new license:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error handling subscription created event for ID ${subscription.id}:`,
|
`Error handling subscription created event for ID ${subscription.id}:`,
|
||||||
|
|||||||
@@ -16,4 +16,3 @@ export * from "./createPortalSession";
|
|||||||
export * from "./getOrgSubscription";
|
export * from "./getOrgSubscription";
|
||||||
export * from "./getOrgUsage";
|
export * from "./getOrgUsage";
|
||||||
export * from "./internalGetOrgTier";
|
export * from "./internalGetOrgTier";
|
||||||
export * from "./createCheckoutSessionLicense";
|
|
||||||
|
|||||||
@@ -166,14 +166,6 @@ if (build === "saas") {
|
|||||||
billing.createCheckoutSessionSAAS
|
billing.createCheckoutSessionSAAS
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/org/:orgId/billing/create-checkout-session-license",
|
|
||||||
verifyOrgAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.billing),
|
|
||||||
logActionAudit(ActionsEnum.billing),
|
|
||||||
billing.createCheckoutSessionoLicense
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/billing/create-portal-session",
|
"/org/:orgId/billing/create-portal-session",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -208,6 +200,14 @@ if (build === "saas") {
|
|||||||
generateLicense.generateNewLicense
|
generateLicense.generateNewLicense
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/license/enterprise",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.billing),
|
||||||
|
logActionAudit(ActionsEnum.billing),
|
||||||
|
generateLicense.generateNewEnterpriseLicense
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/send-support-request",
|
"/send-support-request",
|
||||||
rateLimit({
|
rateLimit({
|
||||||
|
|||||||
@@ -12,33 +12,33 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
|
||||||
import { customers, db } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { response as sendResponse } from "@server/lib/response";
|
||||||
|
import privateConfig from "#private/lib/config";
|
||||||
|
import { createNewLicense } from "./generateNewLicense";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import stripe from "#private/lib/stripe";
|
|
||||||
import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses";
|
import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses";
|
||||||
|
import stripe from "#private/lib/stripe";
|
||||||
|
import { customers, db } from "@server/db";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import z from "zod";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { log } from "winston";
|
||||||
|
|
||||||
const createCheckoutSessionParamsSchema = z.strictObject({
|
const generateNewEnterpriseLicenseParamsSchema = z.strictObject({
|
||||||
orgId: z.string(),
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
const createCheckoutSessionBodySchema = z.strictObject({
|
export async function generateNewEnterpriseLicense(
|
||||||
tier: z.enum([LicenseId.BIG_LICENSE, LicenseId.SMALL_LICENSE]),
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function createCheckoutSessionoLicense(
|
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedParams = createCheckoutSessionParamsSchema.safeParse(req.params);
|
|
||||||
|
const parsedParams = generateNewEnterpriseLicenseParamsSchema.safeParse(req.params);
|
||||||
if (!parsedParams.success) {
|
if (!parsedParams.success) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -50,17 +50,49 @@ export async function createCheckoutSessionoLicense(
|
|||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
const parsedBody = createCheckoutSessionBodySchema.safeParse(req.body);
|
if (!orgId) {
|
||||||
if (!parsedBody.success) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
fromError(parsedBody.error).toString()
|
"Organization ID is required"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tier } = parsedBody.data;
|
logger.debug(`Generating new license for orgId: ${orgId}`);
|
||||||
|
|
||||||
|
const licenseData = req.body;
|
||||||
|
|
||||||
|
if (licenseData.tier != "big_license" && licenseData.tier != "small_license") {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid tier specified. Must be either 'big_license' or 'small_license'."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiResponse = await createNewLicense(orgId, licenseData);
|
||||||
|
|
||||||
|
// Check if the API call was successful
|
||||||
|
if (!apiResponse.success || apiResponse.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
apiResponse.status || HttpCode.BAD_REQUEST,
|
||||||
|
apiResponse.message || "Failed to create license from Fossorial API"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyId = apiResponse?.data?.licenseKey?.id;
|
||||||
|
if (!keyId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Fossorial API did not return a valid license key ID"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// check if we already have a customer for this org
|
// check if we already have a customer for this org
|
||||||
const [customer] = await db
|
const [customer] = await db
|
||||||
@@ -80,10 +112,11 @@ export async function createCheckoutSessionoLicense(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tier = licenseData.tier === "big_license" ? LicenseId.BIG_LICENSE : LicenseId.SMALL_LICENSE;
|
||||||
const tierPrice = getLicensePriceSet()[tier]
|
const tierPrice = getLicensePriceSet()[tier]
|
||||||
|
|
||||||
const session = await stripe!.checkout.sessions.create({
|
const session = await stripe!.checkout.sessions.create({
|
||||||
client_reference_id: orgId, // So we can look it up the org later on the webhook
|
client_reference_id: keyId.toString(),
|
||||||
billing_address_collection: "required",
|
billing_address_collection: "required",
|
||||||
line_items: [
|
line_items: [
|
||||||
{
|
{
|
||||||
@@ -97,17 +130,20 @@ export async function createCheckoutSessionoLicense(
|
|||||||
cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true`
|
cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true`
|
||||||
});
|
});
|
||||||
|
|
||||||
return response<string>(res, {
|
return sendResponse<string>(res, {
|
||||||
data: session.url,
|
data: session.url,
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Checkout session created successfully",
|
message: "License and checkout session created successfully",
|
||||||
status: HttpCode.CREATED
|
status: HttpCode.CREATED
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"An error occurred while generating new license."
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,40 @@ import { response as sendResponse } from "@server/lib/response";
|
|||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types";
|
import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types";
|
||||||
|
|
||||||
async function createNewLicense(orgId: string, licenseData: any): Promise<any> {
|
export interface CreateNewLicenseResponse {
|
||||||
|
data: Data
|
||||||
|
success: boolean
|
||||||
|
error: boolean
|
||||||
|
message: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
licenseKey: LicenseKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LicenseKey {
|
||||||
|
id: number
|
||||||
|
instanceName: any
|
||||||
|
instanceId: string
|
||||||
|
licenseKey: string
|
||||||
|
tier: string
|
||||||
|
type: string
|
||||||
|
quantity: number
|
||||||
|
quantity_2: number
|
||||||
|
isValid: boolean
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
expiresAt: string
|
||||||
|
paidFor: boolean
|
||||||
|
orgId: string
|
||||||
|
metadata: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNewLicense(orgId: string, licenseData: any): Promise<CreateNewLicenseResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/create`,
|
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/${orgId}/create`, // this says enterprise but it does both
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -35,9 +65,8 @@ async function createNewLicense(orgId: string, licenseData: any): Promise<any> {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
const data: CreateNewLicenseResponse = await response.json();
|
||||||
|
|
||||||
logger.debug("Fossorial API response:", { data });
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating new license:", error);
|
console.error("Error creating new license:", error);
|
||||||
|
|||||||
@@ -13,3 +13,4 @@
|
|||||||
|
|
||||||
export * from "./listGeneratedLicenses";
|
export * from "./listGeneratedLicenses";
|
||||||
export * from "./generateNewLicense";
|
export * from "./generateNewLicense";
|
||||||
|
export * from "./generateNewEnterpriseLicense";
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
async function fetchLicenseKeys(orgId: string): Promise<any> {
|
async function fetchLicenseKeys(orgId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/list`,
|
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/${orgId}/list`,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -250,6 +250,26 @@ export default function GenerateLicenseKeyForm({
|
|||||||
const submitLicenseRequest = async (payload: any) => {
|
const submitLicenseRequest = async (payload: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
// Check if this is a business/enterprise license request
|
||||||
|
if (payload.useCaseType === "business") {
|
||||||
|
const response = await api.put<
|
||||||
|
AxiosResponse<string>
|
||||||
|
>(`/org/${orgId}/license/enterprise`, { ...payload, tier: "big_license" } );
|
||||||
|
|
||||||
|
console.log("Checkout session response:", response.data);
|
||||||
|
const checkoutUrl = response.data.data;
|
||||||
|
if (checkoutUrl) {
|
||||||
|
window.location.href = checkoutUrl;
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Failed to get checkout URL",
|
||||||
|
description: "Please try again later",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Personal license flow
|
||||||
const response = await api.put<
|
const response = await api.put<
|
||||||
AxiosResponse<GenerateNewLicenseResponse>
|
AxiosResponse<GenerateNewLicenseResponse>
|
||||||
>(`/org/${orgId}/license`, payload);
|
>(`/org/${orgId}/license`, payload);
|
||||||
@@ -265,6 +285,7 @@ export default function GenerateLicenseKeyForm({
|
|||||||
variant: "default"
|
variant: "default"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast({
|
toast({
|
||||||
@@ -345,37 +366,6 @@ export default function GenerateLicenseKeyForm({
|
|||||||
resetForm();
|
resetForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTestCheckout = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await api.post<AxiosResponse<string>>(
|
|
||||||
`/org/${orgId}/billing/create-checkout-session-license`,
|
|
||||||
{
|
|
||||||
tier: "big_license"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("Checkout session response:", response.data);
|
|
||||||
const checkoutUrl = response.data.data;
|
|
||||||
if (checkoutUrl) {
|
|
||||||
window.location.href = checkoutUrl;
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: "Failed to get checkout URL",
|
|
||||||
description: "Please try again later",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
title: "Checkout error",
|
|
||||||
description: formatAxiosError(error),
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Credenza open={open} onOpenChange={handleClose}>
|
<Credenza open={open} onOpenChange={handleClose}>
|
||||||
<CredenzaContent className="max-w-4xl">
|
<CredenzaContent className="max-w-4xl">
|
||||||
@@ -1097,15 +1087,6 @@ export default function GenerateLicenseKeyForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!generatedKey && useCaseType === "business" && (
|
{!generatedKey && useCaseType === "business" && (
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleTestCheckout}
|
|
||||||
disabled={loading}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
TEST: Go to Checkout
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="generate-license-business-form"
|
form="generate-license-business-form"
|
||||||
@@ -1116,7 +1097,6 @@ export default function GenerateLicenseKeyForm({
|
|||||||
"generateLicenseKeyForm.buttons.generateLicenseKey"
|
"generateLicenseKeyForm.buttons.generateLicenseKey"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user