feat: allow free plan users to set up rank tracking (#116)
This commit is contained in:
parent
aeafa5649d
commit
86d3714846
@ -7,12 +7,14 @@ export function ActionsMenu({
|
|||||||
onCopyKeywords,
|
onCopyKeywords,
|
||||||
isRunning,
|
isRunning,
|
||||||
hasData,
|
hasData,
|
||||||
|
checkDisabled,
|
||||||
}: {
|
}: {
|
||||||
onCheckNow: () => void;
|
onCheckNow: () => void;
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
onCopyKeywords: () => void;
|
onCopyKeywords: () => void;
|
||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
hasData: boolean;
|
hasData: boolean;
|
||||||
|
checkDisabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
@ -27,6 +29,7 @@ export function ActionsMenu({
|
|||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
||||||
<div className="absolute right-0 top-full mt-1 z-50 rounded-lg border border-base-300 bg-base-100 shadow-lg py-1 min-w-[160px]">
|
<div className="absolute right-0 top-full mt-1 z-50 rounded-lg border border-base-300 bg-base-100 shadow-lg py-1 min-w-[160px]">
|
||||||
|
{!checkDisabled && (
|
||||||
<button
|
<button
|
||||||
className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-base-200"
|
className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-base-200"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -38,6 +41,7 @@ export function ActionsMenu({
|
|||||||
<Play className="size-3.5" />
|
<Play className="size-3.5" />
|
||||||
{isRunning ? "Running..." : "Check Now"}
|
{isRunning ? "Running..." : "Check Now"}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-base-200"
|
className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-base-200"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -41,12 +41,12 @@ export function RankTrackingConfigModal({
|
|||||||
const [step, setStep] = useState<"config" | "keywords">("config");
|
const [step, setStep] = useState<"config" | "keywords">("config");
|
||||||
const [domain, setDomain] = useState(existingConfig?.domain ?? "");
|
const [domain, setDomain] = useState(existingConfig?.domain ?? "");
|
||||||
const [devices, setDevices] = useState<"both" | "desktop" | "mobile">(
|
const [devices, setDevices] = useState<"both" | "desktop" | "mobile">(
|
||||||
existingConfig?.devices ?? "both",
|
existingConfig?.devices ?? "mobile",
|
||||||
);
|
);
|
||||||
const [locationCode, setLocationCode] = useState(
|
const [locationCode, setLocationCode] = useState(
|
||||||
existingConfig?.locationCode ?? DEFAULT_LOCATION_CODE,
|
existingConfig?.locationCode ?? DEFAULT_LOCATION_CODE,
|
||||||
);
|
);
|
||||||
const [serpDepth, setSerpDepth] = useState(existingConfig?.serpDepth ?? 20);
|
const [serpDepth, setSerpDepth] = useState(existingConfig?.serpDepth ?? 40);
|
||||||
const [schedule, setSchedule] = useState<"daily" | "weekly" | "manual">(
|
const [schedule, setSchedule] = useState<"daily" | "weekly" | "manual">(
|
||||||
existingConfig?.scheduleInterval ?? "weekly",
|
existingConfig?.scheduleInterval ?? "weekly",
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AutumnProvider, useCustomer } from "autumn-js/react";
|
||||||
import {
|
import {
|
||||||
getLatestRankResults,
|
getLatestRankResults,
|
||||||
estimateRankCheckCost,
|
estimateRankCheckCost,
|
||||||
@ -15,6 +17,9 @@ import {
|
|||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useSession } from "@/lib/auth-client";
|
||||||
|
import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection";
|
||||||
|
import { SUBSCRIBE_ROUTE } from "@/shared/billing";
|
||||||
import { captureClientEvent } from "@/client/lib/posthog";
|
import { captureClientEvent } from "@/client/lib/posthog";
|
||||||
import { RankTrackingTable } from "./RankTrackingTable";
|
import { RankTrackingTable } from "./RankTrackingTable";
|
||||||
import { exportRankTrackingCsv } from "./RankTrackingTableParts";
|
import { exportRankTrackingCsv } from "./RankTrackingTableParts";
|
||||||
@ -48,7 +53,41 @@ function isComparePeriod(v: string): v is ComparePeriod {
|
|||||||
return COMPARE_PERIODS.has(v);
|
return COMPARE_PERIODS.has(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RankTrackingDomainDetail({
|
export function RankTrackingDomainDetail(props: {
|
||||||
|
config: RankTrackingConfig;
|
||||||
|
projectId: string;
|
||||||
|
onBack: () => void;
|
||||||
|
onEdit: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AutumnProvider>
|
||||||
|
<RankTrackingDomainDetailInner {...props} />
|
||||||
|
</AutumnProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FreePlanAlert({ visible }: { visible: boolean }) {
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="alert alert-warning text-sm py-2">
|
||||||
|
<AlertTriangle className="size-4" />
|
||||||
|
<span>
|
||||||
|
We only start to track keyword positions once you{" "}
|
||||||
|
<Link
|
||||||
|
to={SUBSCRIBE_ROUTE}
|
||||||
|
search={{ upgrade: true }}
|
||||||
|
className="link font-medium"
|
||||||
|
>
|
||||||
|
upgrade to the paid plan
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RankTrackingDomainDetailInner({
|
||||||
config,
|
config,
|
||||||
projectId,
|
projectId,
|
||||||
onBack,
|
onBack,
|
||||||
@ -59,6 +98,14 @@ export function RankTrackingDomainDetail({
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const customerQuery = useCustomer({
|
||||||
|
queryOptions: { enabled: Boolean(session?.user?.id) },
|
||||||
|
});
|
||||||
|
const isFreePlan =
|
||||||
|
!!customerQuery.data &&
|
||||||
|
getCustomerPlanStatus(customerQuery.data) === "free";
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [showAddKeywords, setShowAddKeywords] = useState(false);
|
const [showAddKeywords, setShowAddKeywords] = useState(false);
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
@ -180,6 +227,8 @@ export function RankTrackingDomainDetail({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<FreePlanAlert visible={isFreePlan} />
|
||||||
|
|
||||||
{/* Results card */}
|
{/* Results card */}
|
||||||
<div className="flex-1 flex flex-col min-w-0 border border-base-300 rounded-xl bg-base-100 overflow-hidden">
|
<div className="flex-1 flex flex-col min-w-0 border border-base-300 rounded-xl bg-base-100 overflow-hidden">
|
||||||
{/* Domain header */}
|
{/* Domain header */}
|
||||||
@ -322,6 +371,7 @@ export function RankTrackingDomainDetail({
|
|||||||
}}
|
}}
|
||||||
isRunning={isBusy}
|
isRunning={isBusy}
|
||||||
hasData={filtered.length > 0}
|
hasData={filtered.length > 0}
|
||||||
|
checkDisabled={isFreePlan}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
} from "@tanstack/react-start/server";
|
} from "@tanstack/react-start/server";
|
||||||
import { RankTrackingRepository } from "@/server/features/rank-tracking/repositories/RankTrackingRepository";
|
import { RankTrackingRepository } from "@/server/features/rank-tracking/repositories/RankTrackingRepository";
|
||||||
import { beginRankCheckRun } from "@/server/features/rank-tracking/services/rankCheckRunGuards";
|
import { beginRankCheckRun } from "@/server/features/rank-tracking/services/rankCheckRunGuards";
|
||||||
import { customerHasManagedServiceAccess } from "@/server/billing/subscription";
|
import { customerHasPaidPlan } from "@/server/billing/subscription";
|
||||||
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
|
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
|
||||||
import { computeNextCheckAt } from "@/shared/rank-tracking";
|
import { computeNextCheckAt } from "@/shared/rank-tracking";
|
||||||
|
|
||||||
@ -29,11 +29,8 @@ export default {
|
|||||||
|
|
||||||
for (const config of dueConfigs) {
|
for (const config of dueConfigs) {
|
||||||
try {
|
try {
|
||||||
// Skip configs whose org no longer has paid access
|
// Skip configs whose org doesn't have a paid plan
|
||||||
if (
|
if (isHosted && !(await customerHasPaidPlan(config.organizationId))) {
|
||||||
isHosted &&
|
|
||||||
!(await customerHasManagedServiceAccess(config.organizationId))
|
|
||||||
) {
|
|
||||||
console.log(
|
console.log(
|
||||||
`[cron] Skipping config ${config.id} (${config.domain}) — org ${config.organizationId} no longer has access`,
|
`[cron] Skipping config ${config.id} (${config.domain}) — org ${config.organizationId} no longer has access`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID } from "@/shared/billing";
|
import { AUTUMN_PAID_PLAN_FEATURE_ID } from "@/shared/billing";
|
||||||
|
|
||||||
const { checkMock, getOrCreateMock, isHostedServerAuthModeMock } = vi.hoisted(
|
const { checkMock, getOrCreateMock } = vi.hoisted(() => ({
|
||||||
() => ({
|
|
||||||
checkMock: vi.fn(),
|
checkMock: vi.fn(),
|
||||||
getOrCreateMock: vi.fn(),
|
getOrCreateMock: vi.fn(),
|
||||||
isHostedServerAuthModeMock: vi.fn(),
|
}));
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
vi.mock("@/server/billing/autumn", () => ({
|
vi.mock("@/server/billing/autumn", () => ({
|
||||||
autumn: {
|
autumn: {
|
||||||
@ -19,13 +16,12 @@ vi.mock("@/server/billing/autumn", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/server/lib/runtime-env", () => ({
|
vi.mock("@/server/lib/runtime-env", () => ({
|
||||||
isHostedServerAuthMode: isHostedServerAuthModeMock,
|
isHostedServerAuthMode: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
customerHasManagedServiceAccess,
|
customerHasPaidPlan,
|
||||||
getOrCreateOrganizationCustomer,
|
getOrCreateOrganizationCustomer,
|
||||||
requireManagedServiceAccess,
|
|
||||||
} from "./subscription";
|
} from "./subscription";
|
||||||
|
|
||||||
describe("subscription billing", () => {
|
describe("subscription billing", () => {
|
||||||
@ -33,46 +29,21 @@ describe("subscription billing", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checks the managed service access entitlement", async () => {
|
it("checks the paid plan entitlement", async () => {
|
||||||
checkMock.mockResolvedValue({ allowed: true });
|
checkMock.mockResolvedValue({ allowed: true });
|
||||||
|
|
||||||
await expect(customerHasManagedServiceAccess("org_123")).resolves.toBe(
|
await expect(customerHasPaidPlan("org_123")).resolves.toBe(true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(checkMock).toHaveBeenCalledWith({
|
expect(checkMock).toHaveBeenCalledWith({
|
||||||
customerId: "org_123",
|
customerId: "org_123",
|
||||||
featureId: AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID,
|
featureId: AUTUMN_PAID_PLAN_FEATURE_ID,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips the managed service check outside hosted mode", async () => {
|
it("returns false when org lacks paid plan", async () => {
|
||||||
isHostedServerAuthModeMock.mockResolvedValue(false);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
requireManagedServiceAccess({
|
|
||||||
organizationId: "org_123",
|
|
||||||
userId: "user_123",
|
|
||||||
userEmail: "alice@example.com",
|
|
||||||
}),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
|
|
||||||
expect(getOrCreateMock).not.toHaveBeenCalled();
|
|
||||||
expect(checkMock).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws payment required when the org lacks managed service access", async () => {
|
|
||||||
isHostedServerAuthModeMock.mockResolvedValue(true);
|
|
||||||
getOrCreateMock.mockResolvedValue({ id: "org_123" });
|
|
||||||
checkMock.mockResolvedValue({ allowed: false });
|
checkMock.mockResolvedValue({ allowed: false });
|
||||||
|
|
||||||
await expect(
|
await expect(customerHasPaidPlan("org_123")).resolves.toBe(false);
|
||||||
requireManagedServiceAccess({
|
|
||||||
organizationId: "org_123",
|
|
||||||
userId: "user_123",
|
|
||||||
userEmail: "alice@example.com",
|
|
||||||
}),
|
|
||||||
).rejects.toMatchObject({ code: "PAYMENT_REQUIRED" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("looks up the billing customer by organization id", async () => {
|
it("looks up the billing customer by organization id", async () => {
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import type { EnsuredUserContext } from "@/middleware/ensure-user/types";
|
import type { EnsuredUserContext } from "@/middleware/ensure-user/types";
|
||||||
import { AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID } from "@/shared/billing";
|
import { AUTUMN_PAID_PLAN_FEATURE_ID } from "@/shared/billing";
|
||||||
import { autumn } from "@/server/billing/autumn";
|
import { autumn } from "@/server/billing/autumn";
|
||||||
import { AppError } from "@/server/lib/errors";
|
import { AppError } from "@/server/lib/errors";
|
||||||
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
|
|
||||||
|
|
||||||
export type BillingCustomerContext = Pick<
|
export type BillingCustomerContext = Pick<
|
||||||
EnsuredUserContext,
|
EnsuredUserContext,
|
||||||
@ -29,24 +28,11 @@ export async function getOrCreateOrganizationCustomer(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function customerHasManagedServiceAccess(customerId: string) {
|
export async function customerHasPaidPlan(customerId: string) {
|
||||||
const result = await autumn.check({
|
const result = await autumn.check({
|
||||||
customerId,
|
customerId,
|
||||||
featureId: AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID,
|
featureId: AUTUMN_PAID_PLAN_FEATURE_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.allowed;
|
return result.allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requireManagedServiceAccess(
|
|
||||||
context: BillingCustomerContext,
|
|
||||||
) {
|
|
||||||
if (!(await isHostedServerAuthMode())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customer = await getOrCreateOrganizationCustomer(context);
|
|
||||||
if (!(await customerHasManagedServiceAccess(customer.id))) {
|
|
||||||
throw new AppError("PAYMENT_REQUIRED");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { AppError } from "@/server/lib/errors";
|
|||||||
import { errorHandlingMiddleware } from "@/middleware/errorHandling";
|
import { errorHandlingMiddleware } from "@/middleware/errorHandling";
|
||||||
import type { EnsuredUserContext } from "@/middleware/ensure-user/types";
|
import type { EnsuredUserContext } from "@/middleware/ensure-user/types";
|
||||||
import { ensureUserMiddleware } from "@/middleware/ensureUser";
|
import { ensureUserMiddleware } from "@/middleware/ensureUser";
|
||||||
import { requireManagedServiceAccess } from "@/server/billing/subscription";
|
|
||||||
|
|
||||||
const ensuredUserContextSchema: z.ZodType<EnsuredUserContext> = z.object({
|
const ensuredUserContextSchema: z.ZodType<EnsuredUserContext> = z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
@ -32,7 +31,6 @@ export const globalServerFunctionMiddleware = [
|
|||||||
export const requireAuthenticatedContext = [
|
export const requireAuthenticatedContext = [
|
||||||
createMiddleware({ type: "function" }).server(async ({ next, context }) => {
|
createMiddleware({ type: "function" }).server(async ({ next, context }) => {
|
||||||
const authenticatedContext = getAuthenticatedContext(context);
|
const authenticatedContext = getAuthenticatedContext(context);
|
||||||
await requireManagedServiceAccess(authenticatedContext);
|
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
context: authenticatedContext,
|
context: authenticatedContext,
|
||||||
@ -44,8 +42,6 @@ export const requireProjectContext = [
|
|||||||
createMiddleware({ type: "function" }).server(async ({ next, context }) => {
|
createMiddleware({ type: "function" }).server(async ({ next, context }) => {
|
||||||
const authenticatedContext = getAuthenticatedContext(context);
|
const authenticatedContext = getAuthenticatedContext(context);
|
||||||
|
|
||||||
await requireManagedServiceAccess(authenticatedContext);
|
|
||||||
|
|
||||||
if (!authenticatedContext.project) {
|
if (!authenticatedContext.project) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
"INTERNAL_ERROR",
|
"INTERNAL_ERROR",
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import { waitUntil } from "cloudflare:workers";
|
|||||||
import { RankTrackingRepository } from "@/server/features/rank-tracking/repositories/RankTrackingRepository";
|
import { RankTrackingRepository } from "@/server/features/rank-tracking/repositories/RankTrackingRepository";
|
||||||
import { RankTrackingService } from "@/server/features/rank-tracking/services/RankTrackingService";
|
import { RankTrackingService } from "@/server/features/rank-tracking/services/RankTrackingService";
|
||||||
import { getLatestResults } from "@/server/features/rank-tracking/services/rankTrackingResults";
|
import { getLatestResults } from "@/server/features/rank-tracking/services/rankTrackingResults";
|
||||||
import { asAppError } from "@/server/lib/errors";
|
import { AppError, asAppError } from "@/server/lib/errors";
|
||||||
|
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
|
||||||
|
import { customerHasPaidPlan } from "@/server/billing/subscription";
|
||||||
import { captureServerEvent } from "@/server/lib/posthog";
|
import { captureServerEvent } from "@/server/lib/posthog";
|
||||||
import { requireProjectContext } from "@/serverFunctions/middleware";
|
import { requireProjectContext } from "@/serverFunctions/middleware";
|
||||||
import {
|
import {
|
||||||
@ -83,6 +85,14 @@ export const triggerRankCheck = createServerFn({ method: "POST" })
|
|||||||
.middleware(requireProjectContext)
|
.middleware(requireProjectContext)
|
||||||
.inputValidator((data: unknown) => triggerCheckSchema.parse(data))
|
.inputValidator((data: unknown) => triggerCheckSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
|
const isHosted = await isHostedServerAuthMode();
|
||||||
|
if (isHosted && !(await customerHasPaidPlan(context.organizationId))) {
|
||||||
|
throw new AppError(
|
||||||
|
"PAYMENT_REQUIRED",
|
||||||
|
"Upgrade to the paid plan to run rank checks",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await RankTrackingService.triggerCheck({
|
const result = await RankTrackingService.triggerCheck({
|
||||||
configId: data.configId,
|
configId: data.configId,
|
||||||
projectId: context.projectId,
|
projectId: context.projectId,
|
||||||
@ -145,6 +155,11 @@ export const addTrackingKeywords = createServerFn({ method: "POST" })
|
|||||||
|
|
||||||
let checkTriggered = false;
|
let checkTriggered = false;
|
||||||
if (result.addedIds.length > 0) {
|
if (result.addedIds.length > 0) {
|
||||||
|
const isHosted = await isHostedServerAuthMode();
|
||||||
|
const hasPaidPlan =
|
||||||
|
!isHosted || (await customerHasPaidPlan(context.organizationId));
|
||||||
|
|
||||||
|
if (hasPaidPlan) {
|
||||||
try {
|
try {
|
||||||
const triggerResult = await RankTrackingService.triggerCheck({
|
const triggerResult = await RankTrackingService.triggerCheck({
|
||||||
configId: data.configId,
|
configId: data.configId,
|
||||||
@ -173,6 +188,7 @@ export const addTrackingKeywords = createServerFn({ method: "POST" })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { ...result, checkTriggered };
|
return { ...result, checkTriggered };
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,8 +3,7 @@ export const SUBSCRIBE_ROUTE = "/subscribe";
|
|||||||
|
|
||||||
export const AUTUMN_PAID_PLAN_ID = "base-plan";
|
export const AUTUMN_PAID_PLAN_ID = "base-plan";
|
||||||
export const AUTUMN_SEO_DATA_TOP_UP_PLAN_ID = "credit-top-up";
|
export const AUTUMN_SEO_DATA_TOP_UP_PLAN_ID = "credit-top-up";
|
||||||
export const AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID =
|
export const AUTUMN_PAID_PLAN_FEATURE_ID = "paid_plan";
|
||||||
"managed_service_access";
|
|
||||||
export const AUTUMN_SEO_DATA_BALANCE_FEATURE_ID = "usage_credits";
|
export const AUTUMN_SEO_DATA_BALANCE_FEATURE_ID = "usage_credits";
|
||||||
export const AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID = "topup_credits";
|
export const AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID = "topup_credits";
|
||||||
export const AUTUMN_SEO_DATA_CREDITS_PER_USD = 1000;
|
export const AUTUMN_SEO_DATA_CREDITS_PER_USD = 1000;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user