Merge branch 'main' into attach-flows
This commit is contained in:
@@ -9,9 +9,10 @@
|
||||
"start": "tsx src/index.ts",
|
||||
"workers": "tsx watch src/workers.ts",
|
||||
"workers:start": "node --no-deprecation dist/src/workers.js",
|
||||
"build": "tsc -b",
|
||||
"prod": "tsc -b && tsc-alias && node dist/src/index.js",
|
||||
"prod:build": "tsc -b && tsc-alias",
|
||||
"build": "tsc -b tsconfig.build.json",
|
||||
"prod": "tsc -b tsconfig.build.json && tsc-alias && node dist/src/index.js",
|
||||
"prod:build": "tsc -b tsconfig.build.json && tsc-alias",
|
||||
|
||||
"prod:start": "node dist/src/index.js",
|
||||
"cron": "tsx src/cron.ts",
|
||||
"cron:start": "node --no-deprecation dist/src/cron.js",
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
Organization,
|
||||
Subscription,
|
||||
} from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
export const getEntityResponse = async ({
|
||||
db,
|
||||
@@ -101,8 +100,9 @@ export const getEntityResponse = async ({
|
||||
...(withAutumnId ? { autumn_id: entity.internal_id } : {}),
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
customer_id: customerId,
|
||||
created_at: entity.created_at,
|
||||
// feature_id: entity.feature_id,
|
||||
customer_id: customerId,
|
||||
env,
|
||||
products,
|
||||
features,
|
||||
|
||||
@@ -63,6 +63,16 @@ export const handleVersionProductV2 = async ({
|
||||
env,
|
||||
});
|
||||
|
||||
if (latestProduct.is_default) {
|
||||
await ProductService.updateByInternalId({
|
||||
db,
|
||||
internalId: latestProduct.internal_id,
|
||||
update: {
|
||||
is_default: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await ProductService.insert({ db, product: newProduct });
|
||||
|
||||
const { customPrices, customEnts } = await handleNewProductItems({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
|
||||
import { AppEnv, customers, CusProductStatus } from "@autumn/shared";
|
||||
|
||||
import { and, desc, eq, ilike, or, lt, isNotNull, gt, sql } from "drizzle-orm";
|
||||
import { customerProducts, products } from "@autumn/shared";
|
||||
|
||||
@@ -55,14 +57,13 @@ export class CusSearchService {
|
||||
filters.product_id
|
||||
? eq(customerProducts.product_id, filters.product_id)
|
||||
: undefined,
|
||||
filters.status ? eq(customerProducts.status, filters.status) : undefined,
|
||||
filters.status === "canceled"
|
||||
? and(activeProdFilter, isNotNull(customerProducts.canceled_at))
|
||||
: undefined,
|
||||
filters.status === "free_trial"
|
||||
? and(
|
||||
eq(customerProducts.status, CusProductStatus.Active),
|
||||
gt(customerProducts.trial_ends_at, Date.now()),
|
||||
isNotNull(customerProducts.free_trial_id),
|
||||
)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
@@ -210,7 +210,8 @@ export const handleStripeSubUpdate = async ({
|
||||
// CREATE NEW SUBSCRIPTIONS
|
||||
for (const itemSet of newItemSets) {
|
||||
// 1. Next billing date for first sub
|
||||
const nextCycleAnchor = firstSub.current_period_end * 1000;
|
||||
// const nextCycleAnchor = firstSub.current_period_end * 1000;
|
||||
const nextCycleAnchor = subUpdate.current_period_end * 1000;
|
||||
let nextCycleAnchorUnix = nextCycleAnchor;
|
||||
const naturalBillingDate = addBillingIntervalUnix(
|
||||
Date.now(),
|
||||
|
||||
@@ -13,7 +13,16 @@ import {
|
||||
|
||||
import { customerProducts } from "@autumn/shared";
|
||||
|
||||
import { and, arrayContains, eq, inArray, or, sql } from "drizzle-orm";
|
||||
import {
|
||||
and,
|
||||
arrayContains,
|
||||
eq,
|
||||
inArray,
|
||||
isNotNull,
|
||||
ne,
|
||||
or,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
|
||||
export const ACTIVE_STATUSES = [
|
||||
CusProductStatus.Active,
|
||||
@@ -466,12 +475,14 @@ export class CusProductService {
|
||||
|
||||
static async getByFingerprint({
|
||||
db,
|
||||
freeTrialId,
|
||||
productId,
|
||||
internalCustomerId,
|
||||
fingerprint,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
freeTrialId: string;
|
||||
fingerprint: string;
|
||||
productId: string;
|
||||
internalCustomerId: string;
|
||||
fingerprint?: string;
|
||||
}) {
|
||||
let data = await db
|
||||
.select()
|
||||
@@ -480,10 +491,18 @@ export class CusProductService {
|
||||
customers,
|
||||
eq(customerProducts.internal_customer_id, customers.internal_id),
|
||||
)
|
||||
.innerJoin(
|
||||
products,
|
||||
eq(customerProducts.internal_product_id, products.internal_id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(customers.fingerprint, fingerprint),
|
||||
eq(customerProducts.free_trial_id, freeTrialId),
|
||||
or(
|
||||
fingerprint ? eq(customers.fingerprint, fingerprint) : undefined,
|
||||
eq(customers.internal_id, internalCustomerId),
|
||||
),
|
||||
eq(products.id, productId),
|
||||
isNotNull(customerProducts.free_trial_id),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -337,10 +337,12 @@ export const getFullCusProductData = async ({
|
||||
if (!isCustom) {
|
||||
let freeTrial = null;
|
||||
let freeTrialProduct = products.find((p) => notNullish(p.free_trial));
|
||||
|
||||
if (freeTrialProduct) {
|
||||
freeTrial = await getFreeTrialAfterFingerprint({
|
||||
db,
|
||||
freeTrial: freeTrialProduct.free_trial,
|
||||
productId: freeTrialProduct.id,
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
@@ -427,6 +429,7 @@ export const getFullCusProductData = async ({
|
||||
const uniqueFreeTrial = await getFreeTrialAfterFingerprint({
|
||||
db,
|
||||
freeTrial: freeTrial,
|
||||
productId: product.id,
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
|
||||
@@ -139,6 +139,7 @@ export const getCusBalances = async ({
|
||||
|
||||
data[key].balance += balance || 0;
|
||||
data[key].adjustment += adjustment || 0;
|
||||
|
||||
let total =
|
||||
(getResetBalance({
|
||||
entitlement: ent,
|
||||
@@ -158,13 +159,13 @@ export const getCusBalances = async ({
|
||||
data[key].next_reset_at = cusEnt.next_reset_at;
|
||||
}
|
||||
|
||||
data[key].allowance +=
|
||||
(getResetBalance({
|
||||
entitlement: ent,
|
||||
options: getEntOptions(cusProduct.options, ent),
|
||||
relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
|
||||
productQuantity: cusProduct.quantity || 1,
|
||||
}) || 0) * count;
|
||||
const resetBalance = getResetBalance({
|
||||
entitlement: ent,
|
||||
options: getEntOptions(cusProduct.options, ent),
|
||||
relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
|
||||
productQuantity: cusProduct.quantity || 1,
|
||||
});
|
||||
data[key].allowance += (resetBalance || 0) * count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
RewardType,
|
||||
RewardResponse,
|
||||
CouponDurationType,
|
||||
EntityResponseSchema,
|
||||
} from "@autumn/shared";
|
||||
import { getCusInvoices, processFullCusProducts } from "./cusUtils.js";
|
||||
|
||||
@@ -295,6 +296,18 @@ export const getCustomerDetails = async ({
|
||||
: undefined,
|
||||
rewards: withRewards ? rewards : undefined,
|
||||
metadata: customer.metadata,
|
||||
entities: expand.includes(CusExpand.Entities)
|
||||
? customer.entities.map((e) =>
|
||||
EntityResponseSchema.parse({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
customer_id: customer.id,
|
||||
feature_id: e.feature_id,
|
||||
created_at: e.created_at,
|
||||
env: customer.env,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -99,11 +99,12 @@ const buildEntitiesCTE = (withEntities: boolean) => {
|
||||
customer_entities AS (
|
||||
SELECT
|
||||
COALESCE(
|
||||
json_agg(row_to_json(e)) FILTER (WHERE e.id IS NOT NULL),
|
||||
json_agg(row_to_json(e) ORDER BY e.internal_id DESC) FILTER (WHERE e.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS entities
|
||||
FROM entities e
|
||||
WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record)
|
||||
LIMIT 100
|
||||
)
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -74,6 +74,7 @@ export const verifyKey = async ({
|
||||
key: string;
|
||||
}) => {
|
||||
const hashedKey = hashApiKey(key);
|
||||
|
||||
const env = key.startsWith("am_sk_test") ? AppEnv.Sandbox : AppEnv.Live;
|
||||
|
||||
const data = await getAPIKeyCache({
|
||||
|
||||
@@ -92,64 +92,17 @@ export const freeTrialToStripeTimestamp = ({
|
||||
return Math.ceil(trialEnd.getTime() / 1000);
|
||||
};
|
||||
|
||||
export const freeTrialToNumDays = (freeTrial: FreeTrial | null) => {
|
||||
if (!freeTrial) return undefined;
|
||||
return freeTrial.length;
|
||||
};
|
||||
|
||||
export const trialFingerprintExists = async ({
|
||||
db,
|
||||
freeTrialId,
|
||||
fingerprint,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
freeTrialId: string;
|
||||
fingerprint: string;
|
||||
}) => {
|
||||
const data = await CusProductService.getByFingerprint({
|
||||
db,
|
||||
freeTrialId,
|
||||
fingerprint,
|
||||
});
|
||||
|
||||
if (data && data.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const trialWithCustomerExists = async ({
|
||||
db,
|
||||
internalCustomerId,
|
||||
freeTrialId,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
internalCustomerId: string;
|
||||
freeTrialId: string;
|
||||
}) => {
|
||||
const data = await CusProductService.getByFingerprint({
|
||||
db,
|
||||
freeTrialId,
|
||||
fingerprint: internalCustomerId,
|
||||
});
|
||||
|
||||
if (data && data.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getFreeTrialAfterFingerprint = async ({
|
||||
db,
|
||||
freeTrial,
|
||||
productId,
|
||||
fingerprint,
|
||||
internalCustomerId,
|
||||
multipleAllowed,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
freeTrial: FreeTrial | null | undefined;
|
||||
productId: string;
|
||||
fingerprint: string | null | undefined;
|
||||
internalCustomerId: string;
|
||||
multipleAllowed: boolean;
|
||||
@@ -161,31 +114,19 @@ export const getFreeTrialAfterFingerprint = async ({
|
||||
}
|
||||
|
||||
let uniqueFreeTrial: FreeTrial | null = freeTrial;
|
||||
if (uniqueFreeTrial.unique_fingerprint && fingerprint) {
|
||||
let exists = await trialFingerprintExists({
|
||||
db,
|
||||
fingerprint,
|
||||
freeTrialId: uniqueFreeTrial.id,
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
console.log("Free trial fingerprint exists");
|
||||
uniqueFreeTrial = null;
|
||||
}
|
||||
}
|
||||
const data = await CusProductService.getByFingerprint({
|
||||
db,
|
||||
productId,
|
||||
internalCustomerId,
|
||||
fingerprint: uniqueFreeTrial.unique_fingerprint ? fingerprint! : undefined,
|
||||
});
|
||||
|
||||
if (uniqueFreeTrial) {
|
||||
// Check if same customer exists
|
||||
let exists = await trialWithCustomerExists({
|
||||
db,
|
||||
internalCustomerId,
|
||||
freeTrialId: uniqueFreeTrial.id,
|
||||
});
|
||||
const exists = data && data.length > 0;
|
||||
|
||||
if (exists) {
|
||||
console.log("Free trial with customer exists");
|
||||
uniqueFreeTrial = null;
|
||||
}
|
||||
if (exists) {
|
||||
console.log("Free trial fingerprint exists");
|
||||
uniqueFreeTrial = null;
|
||||
}
|
||||
|
||||
return uniqueFreeTrial;
|
||||
|
||||
@@ -41,6 +41,7 @@ export const verifySecretKey = async (req: any, res: any, next: any) => {
|
||||
}
|
||||
|
||||
const apiKey = authHeader.split(" ")[1];
|
||||
|
||||
if (!apiKey.startsWith("am_")) {
|
||||
return {
|
||||
error: ErrCode.InvalidAuthHeader,
|
||||
|
||||
@@ -8,9 +8,9 @@ if [ "$1" == "basic-parallel" ]; then
|
||||
tests/basic/*.ts \
|
||||
tests/basic/multi-feature/*.ts \
|
||||
tests/basic/entities/*.ts \
|
||||
# 'tests/basic/referrals/*.ts' 'tests/attach/**/*.ts' \
|
||||
# && $MOCHA_CMD \
|
||||
# 'tests/basic/referrals/*.ts' 'tests/attach/**/*.ts' \
|
||||
# 'tests/basic/referrals/*.ts' 'tests/attach/**/*.ts' \
|
||||
|
||||
elif [ "$1" == "advanced-parallel" ]; then
|
||||
MOCHA_PARALLEL=true \
|
||||
|
||||
25
server/tsconfig.build.json
Normal file
25
server/tsconfig.build.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"experimentalSpecifier": true
|
||||
},
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "NodeNext",
|
||||
"declaration": true,
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
"module": "NodeNext", // or "node16"/"nodenext"
|
||||
|
||||
"declaration": true,
|
||||
// "rootDir": "../",
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist",
|
||||
@@ -18,8 +17,6 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
// "allowImportingTsExtensions": true,
|
||||
// "noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@shared/*": ["../shared/*"],
|
||||
|
||||
@@ -70,7 +70,10 @@ export * from "./models/cusModels/fullCusModel.js";
|
||||
export * from "./models/cusModels/cusExpand.js";
|
||||
export * from "./models/cusModels/invoiceModels/invoiceResponseModels.js";
|
||||
export * from "./models/cusModels/invoiceModels/invoiceTable.js";
|
||||
// Cus response
|
||||
export * from "./models/cusModels/cusResponseModels.js";
|
||||
export * from "./models/cusModels/cusResModels/cusProductResponse.js";
|
||||
export * from "./models/cusModels/cusResModels/cusFeatureResponse.js";
|
||||
|
||||
export * from "./models/cusModels/entityModels/entityModels.js";
|
||||
export * from "./models/cusModels/entityModels/entityTable.js";
|
||||
|
||||
38
shared/models/cusModels/cusResModels/cusFeatureResponse.ts
Normal file
38
shared/models/cusModels/cusResModels/cusFeatureResponse.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { z } from "zod";
|
||||
import { EntInterval } from "../../productModels/entModels/entEnums.js";
|
||||
|
||||
export const CusEntResponseSchema = z.object({
|
||||
feature_id: z.string(),
|
||||
interval: z.nativeEnum(EntInterval).nullish(),
|
||||
unlimited: z.boolean().nullish(),
|
||||
balance: z.number().nullish(), //
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const CusEntResponseV2Schema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
interval: z.nativeEnum(EntInterval).or(z.literal("multiple")).nullish(),
|
||||
unlimited: z.boolean().nullish(),
|
||||
balance: z.number().nullish(),
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
|
||||
breakdown: z
|
||||
.array(
|
||||
z.object({
|
||||
interval: z.nativeEnum(EntInterval),
|
||||
balance: z.number().nullish(),
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export type CusEntResponse = z.infer<typeof CusEntResponseSchema>;
|
||||
export type CusEntResponseV2 = z.infer<typeof CusEntResponseV2Schema>;
|
||||
18
shared/models/cusModels/cusResModels/cusProductResponse.ts
Normal file
18
shared/models/cusModels/cusResModels/cusProductResponse.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
import { CusProductStatus } from "../../cusProductModels/cusProductEnums.js";
|
||||
|
||||
export const CusProductResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullable(),
|
||||
group: z.string().nullable(),
|
||||
status: z.nativeEnum(CusProductStatus),
|
||||
// created_at: z.number(),
|
||||
canceled_at: z.number().nullish(),
|
||||
started_at: z.number(),
|
||||
|
||||
subscription_ids: z.array(z.string()).nullish(),
|
||||
|
||||
current_period_start: z.number().nullish(),
|
||||
current_period_end: z.number().nullish(),
|
||||
entity_id: z.string().nullish(),
|
||||
});
|
||||
@@ -1,58 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { AppEnv } from "../genModels/genEnums.js";
|
||||
import { EntInterval } from "../productModels/entModels/entEnums.js";
|
||||
import { InvoiceResponseSchema } from "./invoiceModels/invoiceResponseModels.js";
|
||||
import { CusProductStatus } from "../cusProductModels/cusProductEnums.js";
|
||||
import { RewardResponseSchema } from "../rewardModels/rewardModels/rewardResponseModels.js";
|
||||
|
||||
export const CusProductResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullable(),
|
||||
group: z.string().nullable(),
|
||||
status: z.nativeEnum(CusProductStatus),
|
||||
// created_at: z.number(),
|
||||
canceled_at: z.number().nullish(),
|
||||
started_at: z.number(),
|
||||
|
||||
subscription_ids: z.array(z.string()).nullish(),
|
||||
|
||||
current_period_start: z.number().nullish(),
|
||||
current_period_end: z.number().nullish(),
|
||||
entity_id: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const CusEntResponseSchema = z.object({
|
||||
feature_id: z.string(),
|
||||
interval: z.nativeEnum(EntInterval).nullish(),
|
||||
unlimited: z.boolean().nullish(),
|
||||
balance: z.number().nullish(), //
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const CusEntResponseV2Schema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
interval: z.nativeEnum(EntInterval).or(z.literal("multiple")).nullish(),
|
||||
unlimited: z.boolean().nullish(),
|
||||
balance: z.number().nullish(),
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
|
||||
breakdown: z
|
||||
.array(
|
||||
z.object({
|
||||
interval: z.nativeEnum(EntInterval),
|
||||
balance: z.number().nullish(),
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
});
|
||||
import { EntityResponseSchema } from "./entityModels/entityResModels.js";
|
||||
import { CusProductResponseSchema } from "./cusResModels/cusProductResponse.js";
|
||||
|
||||
export const TrialUsedResponseSchema = z.object({
|
||||
product_id: z.string(),
|
||||
@@ -79,10 +30,8 @@ export const CusResponseSchema = z.object({
|
||||
trials_used: z.array(TrialUsedResponseSchema).optional(),
|
||||
rewards: RewardResponseSchema.nullish(),
|
||||
metadata: z.record(z.any()).default({}),
|
||||
entities: z.array(EntityResponseSchema).optional(),
|
||||
});
|
||||
|
||||
export type CusResponse = z.infer<typeof CusResponseSchema>;
|
||||
|
||||
export type CusEntResponse = z.infer<typeof CusEntResponseSchema>;
|
||||
export type CusEntResponseV2 = z.infer<typeof CusEntResponseV2Schema>;
|
||||
export type CusProductResponse = z.infer<typeof CusProductResponseSchema>;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { AppEnv } from "../../genModels/genEnums.js";
|
||||
import {
|
||||
CusProductResponseSchema,
|
||||
CusEntResponseV2Schema,
|
||||
} from "../cusResponseModels.js";
|
||||
|
||||
import { z } from "zod";
|
||||
import { InvoiceResponseSchema } from "../invoiceModels/invoiceResponseModels.js";
|
||||
import { CusProductResponseSchema } from "../cusResModels/cusProductResponse.js";
|
||||
import { CusEntResponseV2Schema } from "../cusResModels/cusFeatureResponse.js";
|
||||
|
||||
export const EntityResponseSchema = z.object({
|
||||
id: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
customer_id: z.string(),
|
||||
feature_id: z.string().nullish(),
|
||||
|
||||
created_at: z.number(),
|
||||
env: z.nativeEnum(AppEnv),
|
||||
|
||||
products: z.array(CusProductResponseSchema),
|
||||
|
||||
features: z.record(z.string(), CusEntResponseV2Schema),
|
||||
products: z.array(CusProductResponseSchema).optional(),
|
||||
features: z.record(z.string(), CusEntResponseV2Schema).optional(),
|
||||
invoices: z.array(InvoiceResponseSchema).optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export const FullProductSchema = ProductSchema.extend({
|
||||
),
|
||||
free_trial: FreeTrialSchema.nullish(),
|
||||
free_trials: z.array(FreeTrialSchema).nullish(),
|
||||
free_trial_ids: z.array(z.string()).nullish(),
|
||||
});
|
||||
|
||||
export type Product = z.infer<typeof ProductSchema>;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EntitlementWithFeature,
|
||||
PriceType,
|
||||
ProductItem,
|
||||
ProductItemInterval,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { FixedPriceConfig, Price, UsagePriceConfig } from "@autumn/shared";
|
||||
@@ -101,8 +102,6 @@ export const isOneOffProduct = (
|
||||
});
|
||||
};
|
||||
|
||||
export const isFreeProduct = (prices: Price[]) => {
|
||||
return prices.every((price) => {
|
||||
return price.config?.interval == BillingInterval.OneOff;
|
||||
});
|
||||
export const isFreeProduct = (items: ProductItem[]) => {
|
||||
return items.every((item) => isFeatureItem(item));
|
||||
};
|
||||
|
||||
@@ -124,46 +124,73 @@ function ProductView({ env }: { env: AppEnv }) {
|
||||
: `Save changes to product: ${product?.name}`,
|
||||
};
|
||||
|
||||
// Handle browser beforeunload event
|
||||
// Replace the current useBlocker call with a fixed useEffect
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (hasChanges) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
if (!hasChanges) return;
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}, [hasChanges]);
|
||||
|
||||
// Intercept in-app navigation
|
||||
useEffect(() => {
|
||||
const originalPushState = window.history.pushState;
|
||||
const originalReplaceState = window.history.replaceState;
|
||||
let currentUrl = window.location.href;
|
||||
let isRestoring = false; // Flag to prevent recursive popstate events
|
||||
|
||||
const handleNavigation = () => {
|
||||
const confirmed = window.confirm(
|
||||
"Are you sure you want to leave without updating the product? Click cancel to stay and save your changes, or click OK to leave without saving.",
|
||||
);
|
||||
return confirmed;
|
||||
};
|
||||
|
||||
// Handle programmatic navigation (pushState/replaceState)
|
||||
window.history.pushState = function (...args) {
|
||||
if (hasChanges) {
|
||||
const confirmed = window.confirm(
|
||||
"You have unsaved changes. Click Update Product to save your changes.",
|
||||
);
|
||||
if (!confirmed) return;
|
||||
if (handleNavigation()) {
|
||||
currentUrl = window.location.href;
|
||||
return originalPushState.apply(this, args);
|
||||
}
|
||||
return originalPushState.apply(this, args);
|
||||
};
|
||||
|
||||
window.history.replaceState = function (...args) {
|
||||
if (hasChanges) {
|
||||
const confirmed = window.confirm(
|
||||
"You have unsaved changes. Click Update Product to save your changes.",
|
||||
);
|
||||
if (!confirmed) return;
|
||||
if (handleNavigation()) {
|
||||
currentUrl = window.location.href;
|
||||
return originalReplaceState.apply(this, args);
|
||||
}
|
||||
return originalReplaceState.apply(this, args);
|
||||
};
|
||||
|
||||
// Handle back/forward button navigation
|
||||
const handlePopState = (event: PopStateEvent) => {
|
||||
if (isRestoring) return; // Prevent handling our own restore operation
|
||||
|
||||
const confirmed = window.confirm(
|
||||
"Are you sure you want to leave without updating the product? Click cancel to stay and save your changes, or click OK to leave without saving.",
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
// User clicked Cancel (wants to stay) - go forward to undo the back navigation
|
||||
isRestoring = true;
|
||||
window.history.go(1); // Go forward to undo the back navigation
|
||||
setTimeout(() => {
|
||||
isRestoring = false;
|
||||
}, 100); // Reset flag after navigation
|
||||
} else {
|
||||
currentUrl = window.location.href;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
|
||||
// Optional: Handle page unload/refresh as well
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = ""; // Required for some browsers
|
||||
return "";
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.history.pushState = originalPushState;
|
||||
window.history.replaceState = originalReplaceState;
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
}, [hasChanges]);
|
||||
|
||||
|
||||
@@ -5,7 +5,13 @@ import { useEffect, useState } from "react";
|
||||
import { Select, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
import { SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { keyToTitle, slugify } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { Reward, RewardType, Product, FullProduct } from "@autumn/shared";
|
||||
import {
|
||||
Reward,
|
||||
RewardType,
|
||||
Product,
|
||||
FullProduct,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { useProductsContext } from "../ProductsContext";
|
||||
import { DiscountConfig } from "./DiscountConfig";
|
||||
import { notNullish } from "@/utils/genUtils";
|
||||
@@ -20,6 +26,7 @@ export const RewardConfig = ({
|
||||
setReward: (reward: Reward) => void;
|
||||
}) => {
|
||||
const { products } = useProductsContext();
|
||||
|
||||
const [idChanged, setIdChanged] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -110,10 +117,8 @@ export const RewardConfig = ({
|
||||
>
|
||||
{(() => {
|
||||
const freeAddOns = products
|
||||
.filter((product: FullProduct) => product.is_add_on)
|
||||
.filter((product: FullProduct) =>
|
||||
isFreeProduct(product.prices),
|
||||
);
|
||||
.filter((product: ProductV2) => product.is_add_on)
|
||||
.filter((product: ProductV2) => isFreeProduct(product.items));
|
||||
|
||||
const empty = freeAddOns.length === 0;
|
||||
return (
|
||||
|
||||
@@ -13,8 +13,6 @@ export const RewardsTable = () => {
|
||||
const [selectedReward, setSelectedReward] = useState<Reward | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
console.log("products: ", products);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateReward
|
||||
|
||||
Reference in New Issue
Block a user