mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-02 16:56:39 +00:00
Compare commits
3 Commits
1.15.4-s.4
...
1.15.4-s.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf97b6df9c | ||
|
|
6d9b129ac9 | ||
|
|
b786497299 |
35
.github/workflows/saas.yml
vendored
35
.github/workflows/saas.yml
vendored
@@ -56,6 +56,41 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
|
- name: Download MaxMind GeoLite2 databases
|
||||||
|
env:
|
||||||
|
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "Downloading MaxMind GeoLite2 databases..."
|
||||||
|
|
||||||
|
# Download GeoLite2-Country
|
||||||
|
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||||
|
-o GeoLite2-Country.tar.gz
|
||||||
|
|
||||||
|
# Download GeoLite2-ASN
|
||||||
|
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||||
|
-o GeoLite2-ASN.tar.gz
|
||||||
|
|
||||||
|
# Extract the .mmdb files
|
||||||
|
tar -xzf GeoLite2-Country.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||||
|
tar -xzf GeoLite2-ASN.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||||
|
|
||||||
|
# Verify files exist
|
||||||
|
if [ ! -f "GeoLite2-Country.mmdb" ]; then
|
||||||
|
echo "ERROR: Failed to download GeoLite2-Country.mmdb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "GeoLite2-ASN.mmdb" ]; then
|
||||||
|
echo "ERROR: Failed to download GeoLite2-ASN.mmdb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up tar files
|
||||||
|
rm -f GeoLite2-Country.tar.gz GeoLite2-ASN.tar.gz
|
||||||
|
|
||||||
|
echo "MaxMind databases downloaded successfully"
|
||||||
|
ls -lh GeoLite2-*.mmdb
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
THRESHOLD=75
|
THRESHOLD=75
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ COPY server/db/ios_models.json ./dist/ios_models.json
|
|||||||
COPY server/db/mac_models.json ./dist/mac_models.json
|
COPY server/db/mac_models.json ./dist/mac_models.json
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
|
||||||
|
# Copy MaxMind databases for SaaS builds
|
||||||
|
ARG BUILD=oss
|
||||||
|
RUN mkdir -p ./maxmind
|
||||||
|
|
||||||
|
# This is only for saas
|
||||||
|
COPY --from=builder-dev /app/GeoLite2-Country.mmdb ./maxmind/GeoLite2-Country.mmdb
|
||||||
|
COPY --from=builder-dev /app/GeoLite2-ASN.mmdb ./maxmind/GeoLite2-ASN.mmdb
|
||||||
|
|
||||||
# OCI Image Labels - Build Args for dynamic values
|
# OCI Image Labels - Build Args for dynamic values
|
||||||
ARG VERSION="dev"
|
ARG VERSION="dev"
|
||||||
ARG REVISION=""
|
ARG REVISION=""
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
import { config } from "@server/lib/config";
|
import { config } from "@server/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { redis } from "#private/lib/redis";
|
import { redis } from "#private/lib/redis";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
const instanceId = uuidv4();
|
||||||
|
|
||||||
export class LockManager {
|
export class LockManager {
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +36,7 @@ export class LockManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lockValue = `${
|
const lockValue = `${
|
||||||
config.getRawConfig().gerbil.exit_node_name
|
instanceId
|
||||||
}:${Date.now()}`;
|
}:${Date.now()}`;
|
||||||
const redisKey = `lock:${lockKey}`;
|
const redisKey = `lock:${lockKey}`;
|
||||||
|
|
||||||
@@ -52,7 +55,7 @@ export class LockManager {
|
|||||||
if (result === "OK") {
|
if (result === "OK") {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Lock acquired: ${lockKey} by ${
|
`Lock acquired: ${lockKey} by ${
|
||||||
config.getRawConfig().gerbil.exit_node_name
|
instanceId
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -63,14 +66,14 @@ export class LockManager {
|
|||||||
if (
|
if (
|
||||||
existingValue &&
|
existingValue &&
|
||||||
existingValue.startsWith(
|
existingValue.startsWith(
|
||||||
`${config.getRawConfig().gerbil.exit_node_name}:`
|
`${instanceId}:`
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Extend the lock TTL since it's the same worker
|
// Extend the lock TTL since it's the same worker
|
||||||
await redis.pexpire(redisKey, ttlMs);
|
await redis.pexpire(redisKey, ttlMs);
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Lock extended: ${lockKey} by ${
|
`Lock extended: ${lockKey} by ${
|
||||||
config.getRawConfig().gerbil.exit_node_name
|
instanceId
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -129,19 +132,19 @@ export class LockManager {
|
|||||||
luaScript,
|
luaScript,
|
||||||
1,
|
1,
|
||||||
redisKey,
|
redisKey,
|
||||||
`${config.getRawConfig().gerbil.exit_node_name}:`
|
`${instanceId}:`
|
||||||
)) as number;
|
)) as number;
|
||||||
|
|
||||||
if (result === 1) {
|
if (result === 1) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Lock released: ${lockKey} by ${
|
`Lock released: ${lockKey} by ${
|
||||||
config.getRawConfig().gerbil.exit_node_name
|
instanceId
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Lock not released - not owned by worker: ${lockKey} by ${
|
`Lock not released - not owned by worker: ${lockKey} by ${
|
||||||
config.getRawConfig().gerbil.exit_node_name
|
instanceId
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -198,7 +201,7 @@ export class LockManager {
|
|||||||
const ownedByMe =
|
const ownedByMe =
|
||||||
exists &&
|
exists &&
|
||||||
value!.startsWith(
|
value!.startsWith(
|
||||||
`${config.getRawConfig().gerbil.exit_node_name}:`
|
`${instanceId}:`
|
||||||
);
|
);
|
||||||
const owner = exists ? value!.split(":")[0] : undefined;
|
const owner = exists ? value!.split(":")[0] : undefined;
|
||||||
|
|
||||||
@@ -246,14 +249,14 @@ export class LockManager {
|
|||||||
luaScript,
|
luaScript,
|
||||||
1,
|
1,
|
||||||
redisKey,
|
redisKey,
|
||||||
`${config.getRawConfig().gerbil.exit_node_name}:`,
|
`${instanceId}:`,
|
||||||
ttlMs.toString()
|
ttlMs.toString()
|
||||||
)) as number;
|
)) as number;
|
||||||
|
|
||||||
if (result === 1) {
|
if (result === 1) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Lock extended: ${lockKey} by ${
|
`Lock extended: ${lockKey} by ${
|
||||||
config.getRawConfig().gerbil.exit_node_name
|
instanceId
|
||||||
} for ${ttlMs}ms`
|
} for ${ttlMs}ms`
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -356,7 +359,7 @@ export class LockManager {
|
|||||||
(value) =>
|
(value) =>
|
||||||
value &&
|
value &&
|
||||||
value.startsWith(
|
value.startsWith(
|
||||||
`${config.getRawConfig().gerbil.exit_node_name}:`
|
`${instanceId}:`
|
||||||
)
|
)
|
||||||
).length;
|
).length;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,15 +72,15 @@ export const privateConfigSchema = z.object({
|
|||||||
db: z.int().nonnegative().optional().default(0)
|
db: z.int().nonnegative().optional().default(0)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
.optional(),
|
||||||
|
tls: z
|
||||||
|
.object({
|
||||||
|
rejectUnauthorized: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
// tls: z
|
|
||||||
// .object({
|
|
||||||
// reject_unauthorized: z
|
|
||||||
// .boolean()
|
|
||||||
// .optional()
|
|
||||||
// .default(true)
|
|
||||||
// })
|
|
||||||
// .optional()
|
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
gerbil: z
|
gerbil: z
|
||||||
|
|||||||
@@ -108,11 +108,15 @@ class RedisManager {
|
|||||||
port: redisConfig.port!,
|
port: redisConfig.port!,
|
||||||
password: redisConfig.password,
|
password: redisConfig.password,
|
||||||
db: redisConfig.db
|
db: redisConfig.db
|
||||||
// tls: {
|
|
||||||
// rejectUnauthorized:
|
|
||||||
// redisConfig.tls?.reject_unauthorized || false
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
||||||
|
if (redisConfig.tls) {
|
||||||
|
opts.tls = {
|
||||||
|
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,11 +134,15 @@ class RedisManager {
|
|||||||
port: replica.port!,
|
port: replica.port!,
|
||||||
password: replica.password,
|
password: replica.password,
|
||||||
db: replica.db || redisConfig.db
|
db: replica.db || redisConfig.db
|
||||||
// tls: {
|
|
||||||
// rejectUnauthorized:
|
|
||||||
// replica.tls?.reject_unauthorized || false
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
||||||
|
if (redisConfig.tls) {
|
||||||
|
opts.tls = {
|
||||||
|
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import { db, dnsRecords } from "@server/db";
|
|||||||
import { domains, exitNodes, orgDomains, orgs, resources } from "@server/db";
|
import { domains, exitNodes, orgDomains, orgs, resources } from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { eq, ne } from "drizzle-orm";
|
import { eq, ne } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export async function copyInConfig() {
|
export async function copyInConfig() {
|
||||||
|
if (build == "saas") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const endpoint = config.getRawConfig().gerbil.base_endpoint;
|
const endpoint = config.getRawConfig().gerbil.base_endpoint;
|
||||||
const listenPort = config.getRawConfig().gerbil.start_port;
|
const listenPort = config.getRawConfig().gerbil.start_port;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import m11 from "./scriptsPg/1.14.0";
|
|||||||
import m12 from "./scriptsPg/1.15.0";
|
import m12 from "./scriptsPg/1.15.0";
|
||||||
import m13 from "./scriptsPg/1.15.3";
|
import m13 from "./scriptsPg/1.15.3";
|
||||||
import m14 from "./scriptsPg/1.15.4";
|
import m14 from "./scriptsPg/1.15.4";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -53,6 +54,10 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runMigrations() {
|
export async function runMigrations() {
|
||||||
|
if (build == "saas") {
|
||||||
|
console.log("Running in SaaS mode, skipping migrations...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (process.env.DISABLE_MIGRATIONS) {
|
if (process.env.DISABLE_MIGRATIONS) {
|
||||||
console.log("Migrations are disabled. Skipping...");
|
console.log("Migrations are disabled. Skipping...");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import m32 from "./scriptsSqlite/1.14.0";
|
|||||||
import m33 from "./scriptsSqlite/1.15.0";
|
import m33 from "./scriptsSqlite/1.15.0";
|
||||||
import m34 from "./scriptsSqlite/1.15.3";
|
import m34 from "./scriptsSqlite/1.15.3";
|
||||||
import m35 from "./scriptsSqlite/1.15.4";
|
import m35 from "./scriptsSqlite/1.15.4";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -105,6 +106,10 @@ function backupDb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runMigrations() {
|
export async function runMigrations() {
|
||||||
|
if (build == "saas") {
|
||||||
|
console.log("Running in SaaS mode, skipping migrations...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (process.env.DISABLE_MIGRATIONS) {
|
if (process.env.DISABLE_MIGRATIONS) {
|
||||||
console.log("Migrations are disabled. Skipping...");
|
console.log("Migrations are disabled. Skipping...");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -445,6 +445,54 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
const currentPlanId = getCurrentPlanId();
|
const currentPlanId = getCurrentPlanId();
|
||||||
|
|
||||||
|
// Check if subscription is in a problematic state that requires attention
|
||||||
|
const hasProblematicSubscription = (): boolean => {
|
||||||
|
if (!tierSubscription?.subscription) return false;
|
||||||
|
const status = tierSubscription.subscription.status;
|
||||||
|
return (
|
||||||
|
status === "past_due" ||
|
||||||
|
status === "unpaid" ||
|
||||||
|
status === "incomplete" ||
|
||||||
|
status === "incomplete_expired"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isProblematicState = hasProblematicSubscription();
|
||||||
|
|
||||||
|
// Get user-friendly subscription status message
|
||||||
|
const getSubscriptionStatusMessage = (): { title: string; description: string } | null => {
|
||||||
|
if (!tierSubscription?.subscription || !isProblematicState) return null;
|
||||||
|
|
||||||
|
const status = tierSubscription.subscription.status;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "past_due":
|
||||||
|
return {
|
||||||
|
title: t("billingPastDueTitle") || "Payment Past Due",
|
||||||
|
description: t("billingPastDueDescription") || "Your payment is past due. Please update your payment method to continue using your current plan features. If not resolved, your subscription will be canceled and you'll be reverted to the free tier."
|
||||||
|
};
|
||||||
|
case "unpaid":
|
||||||
|
return {
|
||||||
|
title: t("billingUnpaidTitle") || "Subscription Unpaid",
|
||||||
|
description: t("billingUnpaidDescription") || "Your subscription is unpaid and you have been reverted to the free tier. Please update your payment method to restore your subscription."
|
||||||
|
};
|
||||||
|
case "incomplete":
|
||||||
|
return {
|
||||||
|
title: t("billingIncompleteTitle") || "Payment Incomplete",
|
||||||
|
description: t("billingIncompleteDescription") || "Your payment is incomplete. Please complete the payment process to activate your subscription."
|
||||||
|
};
|
||||||
|
case "incomplete_expired":
|
||||||
|
return {
|
||||||
|
title: t("billingIncompleteExpiredTitle") || "Payment Expired",
|
||||||
|
description: t("billingIncompleteExpiredDescription") || "Your payment was never completed and has expired. You have been reverted to the free tier. Please subscribe again to restore access to paid features."
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusMessage = getSubscriptionStatusMessage();
|
||||||
|
|
||||||
// Get button label and action for each plan
|
// Get button label and action for each plan
|
||||||
const getPlanAction = (plan: PlanOption) => {
|
const getPlanAction = (plan: PlanOption) => {
|
||||||
if (plan.id === "enterprise") {
|
if (plan.id === "enterprise") {
|
||||||
@@ -458,7 +506,7 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
if (plan.id === currentPlanId) {
|
if (plan.id === currentPlanId) {
|
||||||
// If it's the basic plan (basic with no subscription), show as current but disabled
|
// If it's the basic plan (basic with no subscription), show as current but disabled
|
||||||
if (plan.id === "basic" && !hasSubscription) {
|
if (plan.id === "basic" && !hasSubscription && !isProblematicState) {
|
||||||
return {
|
return {
|
||||||
label: "Current Plan",
|
label: "Current Plan",
|
||||||
action: () => {},
|
action: () => {},
|
||||||
@@ -466,8 +514,17 @@ export default function BillingPage() {
|
|||||||
disabled: true
|
disabled: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// If on free tier but has a problematic subscription, allow them to manage it
|
||||||
|
if (plan.id === "basic" && isProblematicState) {
|
||||||
|
return {
|
||||||
|
label: "Manage Subscription",
|
||||||
|
action: handleModifySubscription,
|
||||||
|
variant: "default" as const,
|
||||||
|
disabled: false
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
label: "Modify Current Plan",
|
label: "Manage Current Plan",
|
||||||
action: handleModifySubscription,
|
action: handleModifySubscription,
|
||||||
variant: "default" as const,
|
variant: "default" as const,
|
||||||
disabled: false
|
disabled: false
|
||||||
@@ -503,7 +560,7 @@ export default function BillingPage() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
disabled: false
|
disabled: isProblematicState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,7 +579,7 @@ export default function BillingPage() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
disabled: false
|
disabled: isProblematicState
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -648,6 +705,26 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
|
{/* Subscription Status Alert */}
|
||||||
|
{isProblematicState && statusMessage && (
|
||||||
|
<Alert variant="destructive" className="mb-6">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>
|
||||||
|
{statusMessage.title}
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{statusMessage.description}
|
||||||
|
{" "}
|
||||||
|
<button
|
||||||
|
onClick={handleModifySubscription}
|
||||||
|
className="underline font-semibold hover:no-underline"
|
||||||
|
>
|
||||||
|
{t("billingManageSubscription") || "Manage your subscription"}
|
||||||
|
</button>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Your Plan Section */}
|
{/* Your Plan Section */}
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
@@ -692,22 +769,50 @@ export default function BillingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Button
|
{isProblematicState && planAction.disabled && !isCurrentPlan && plan.id !== "enterprise" ? (
|
||||||
variant={
|
<Tooltip>
|
||||||
isCurrentPlan
|
<TooltipTrigger asChild>
|
||||||
? "default"
|
<div>
|
||||||
: "outline"
|
<Button
|
||||||
}
|
variant={
|
||||||
size="sm"
|
isCurrentPlan
|
||||||
className="w-full"
|
? "default"
|
||||||
onClick={planAction.action}
|
: "outline"
|
||||||
disabled={
|
}
|
||||||
isLoading || planAction.disabled
|
size="sm"
|
||||||
}
|
className="w-full"
|
||||||
loading={isLoading && isCurrentPlan}
|
onClick={planAction.action}
|
||||||
>
|
disabled={
|
||||||
{planAction.label}
|
isLoading || planAction.disabled
|
||||||
</Button>
|
}
|
||||||
|
loading={isLoading && isCurrentPlan}
|
||||||
|
>
|
||||||
|
{planAction.label}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("billingResolvePaymentIssue") || "Please resolve your payment issue before upgrading or downgrading"}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
isCurrentPlan
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={planAction.action}
|
||||||
|
disabled={
|
||||||
|
isLoading || planAction.disabled
|
||||||
|
}
|
||||||
|
loading={isLoading && isCurrentPlan}
|
||||||
|
>
|
||||||
|
{planAction.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user