feat: react native sdk v2 (#4616)

Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
Anshuman Pandey
2025-02-04 21:02:04 +05:30
committed by GitHub
parent 26cca5c2f8
commit bb6df783ab
91 changed files with 6021 additions and 1180 deletions
+2 -2
View File
@@ -1,2 +1,2 @@
EXPO_PUBLIC_API_HOST=http://192.168.178.20:3000
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=clzr04nkd000bcdl110j0ijyq
EXPO_PUBLIC_APP_URL=http://192.168.0.197:3000
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=cm5p0cs7r000819182b32j0a1
+1
View File
@@ -18,6 +18,7 @@
},
"jsEngine": "hermes",
"name": "react-native-demo",
"newArchEnabled": true,
"orientation": "portrait",
"slug": "react-native-demo",
"splash": {
+5 -4
View File
@@ -13,16 +13,17 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/react-native": "workspace:*",
"expo": "52.0.18",
"expo-status-bar": "2.0.0",
"@react-native-async-storage/async-storage": "2.1.0",
"expo": "52.0.28",
"expo-status-bar": "2.0.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.5",
"react-native": "0.76.6",
"react-native-webview": "13.12.5"
},
"devDependencies": {
"@babel/core": "7.26.0",
"@types/react": "19.0.1",
"@types/react": "18.3.18",
"typescript": "5.7.2"
},
"private": true
+87 -23
View File
@@ -1,7 +1,14 @@
import { StatusBar } from "expo-status-bar";
import React, { type JSX } from "react";
import { Button, LogBox, StyleSheet, Text, View } from "react-native";
import Formbricks, { track } from "@formbricks/react-native";
import Formbricks, {
logout,
setAttribute,
setAttributes,
setLanguage,
setUserId,
track,
} from "@formbricks/react-native";
LogBox.ignoreAllLogs();
@@ -10,35 +17,92 @@ export default function App(): JSX.Element {
throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required");
}
if (!process.env.EXPO_PUBLIC_API_HOST) {
throw new Error("EXPO_PUBLIC_API_HOST is required");
if (!process.env.EXPO_PUBLIC_APP_URL) {
throw new Error("EXPO_PUBLIC_APP_URL is required");
}
const config = {
environmentId: process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID as string,
apiHost: process.env.EXPO_PUBLIC_API_HOST as string,
userId: "random-user-id",
attributes: {
language: "en",
testAttr: "attr-test",
},
};
return (
<View style={styles.container}>
<Text>Formbricks React Native SDK Demo</Text>
<Button
title="Trigger Code Action"
onPress={() => {
track("code").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error tracking event:", error);
});
}}
/>
<View
style={{
display: "flex",
flexDirection: "column",
gap: 10,
}}>
<Button
title="Trigger Code Action"
onPress={() => {
track("code").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error tracking event:", error);
});
}}
/>
<Button
title="Set User Id"
onPress={() => {
setUserId("random-user-id").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user id:", error);
});
}}
/>
<Button
title="Set User Attributess (multiple)"
onPress={() => {
setAttributes({
testAttr: "attr-test",
testAttr2: "attr-test-2",
testAttr3: "attr-test-3",
testAttr4: "attr-test-4",
}).catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user attributes:", error);
});
}}
/>
<Button
title="Set User Attributes (single)"
onPress={() => {
setAttribute("testSingleAttr", "testSingleAttr").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user attributes:", error);
});
}}
/>
<Button
title="Logout"
onPress={() => {
logout().catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error logging out:", error);
});
}}
/>
<Button
title="Set Language (de)"
onPress={() => {
setLanguage("de").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting language:", error);
});
}}
/>
</View>
<StatusBar style="auto" />
<Formbricks initConfig={config} />
<Formbricks
appUrl={process.env.EXPO_PUBLIC_APP_URL as string}
environmentId={process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID as string}
/>
</View>
);
}
@@ -357,17 +357,11 @@ Now, update your App.js/App.tsx file to initialize Formbricks:
// other imports
import Formbricks from "@formbricks/react-native";
const config = {
environmentId: "<environment-id>",
apiHost: "<api-host>",
userId: "<user-id>", // optional
};
export default function App() {
return (
<>
{/* Your app content */}
<Formbricks initConfig={config} />
<Formbricks appUrl="https://app.formbricks.com" environmentId="your-environment-id" />
</>
);
}
@@ -381,7 +375,7 @@ export default function App() {
<Property name="environment-id" type="string">
Formbricks Environment ID.
</Property>
<Property name="api-host" type="string">
<Property name="app-url" type="string">
URL of the hosted Formbricks instance.
</Property>
</Properties>
@@ -29,7 +29,7 @@ import { getSurveysForEnvironmentState } from "./survey";
*/
export const getEnvironmentState = async (
environmentId: string
): Promise<{ state: TJsEnvironmentState["data"]; revalidateEnvironment?: boolean }> =>
): Promise<{ data: TJsEnvironmentState["data"]; revalidateEnvironment?: boolean }> =>
cache(
async () => {
let revalidateEnvironment = false;
@@ -102,14 +102,14 @@ export const getEnvironmentState = async (
(survey) => survey.type === "app" && survey.status === "inProgress"
);
const state: TJsEnvironmentState["data"] = {
const data: TJsEnvironmentState["data"] = {
surveys: !isMonthlyResponsesLimitReached ? filteredSurveys : [],
actionClasses,
project: project,
};
return {
state,
data,
revalidateEnvironment,
};
},
@@ -36,16 +36,20 @@ export const GET = async (
try {
const environmentState = await getEnvironmentState(params.environmentId);
const { data, revalidateEnvironment } = environmentState;
if (environmentState.revalidateEnvironment) {
if (revalidateEnvironment) {
environmentCache.revalidate({
id: inputValidation.data.environmentId,
projectId: environmentState.state.project.id,
projectId: data.project.id,
});
}
return responses.successResponse(
environmentState.state,
{
data,
expiresAt: new Date(Date.now() + 1000 * 60 * 30), // 30 minutes
},
true,
"public, s-maxage=600, max-age=840, stale-while-revalidate=600, stale-if-error=600"
);
@@ -0,0 +1,3 @@
import { OPTIONS, POST } from "@/modules/ee/contacts/api/client/[environmentId]/user/route";
export { POST, OPTIONS };
@@ -102,7 +102,6 @@ export const PricingTable = ({
throw new Error(t("common.something_went_wrong_please_try_again"));
}
} catch (err) {
console.log({ err });
toast.error(t("environments.settings.billing.unable_to_upgrade_plan"));
}
};
+2 -2
View File
@@ -45,12 +45,12 @@ export const getContactsAction = authenticatedActionClient
return getContacts(parsedInput.environmentId, parsedInput.offset, parsedInput.searchValue);
});
const ZPersonDeleteAction = z.object({
const ZContactDeleteAction = z.object({
contactId: ZId,
});
export const deleteContactAction = authenticatedActionClient
.schema(ZPersonDeleteAction)
.schema(ZContactDeleteAction)
.action(async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromContactId(parsedInput.contactId);
const projectId = await getProjectIdFromContactId(parsedInput.contactId);
@@ -35,7 +35,7 @@ export const getContactByUserIdWithAttributes = reactCache(
return contact;
},
[`getContactByUserId-${environmentId}-${userId}-${JSON.stringify(updatedAttributes)}`],
[`getContactByUserIdWithAttributes-${environmentId}-${userId}-${JSON.stringify(updatedAttributes)}`],
{
tags: [
contactCache.tag.byEnvironmentIdAndUserId(environmentId, userId),
@@ -1,10 +1,10 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { updateAttributes } from "@/modules/ee/contacts/lib/attributes";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { NextRequest } from "next/server";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZJsContactsUpdateAttributeInput } from "@formbricks/types/js";
import { updateAttributes } from "./lib/attributes";
import { getContactByUserIdWithAttributes } from "./lib/contact";
export const OPTIONS = async () => {
@@ -74,36 +74,15 @@ export const PUT = async (
);
}
const { details: updateAttrDetails } = await updateAttributes(
contact.id,
userId,
environmentId,
updatedAttributes
);
// if userIdAttr or idAttr was in the payload, we need to inform the user that it was ignored
const details: Record<string, string> = {};
if (userIdAttr) {
details.userId = "updating userId is ignored as it is a reserved field and cannot be updated.";
}
if (idAttr) {
details.id = "updating id is ignored as it is a reserved field and cannot be updated.";
}
if (updateAttrDetails && Object.keys(updateAttrDetails).length > 0) {
Object.entries(updateAttrDetails).forEach(([key, value]) => {
details[key] = value;
});
}
const { messages } = await updateAttributes(contact.id, userId, environmentId, updatedAttributes);
return responses.successResponse(
{
changed: true,
message: "The person was successfully updated.",
...(Object.keys(details).length > 0
...(messages && messages.length > 0
? {
details,
messages,
}
: {}),
},
@@ -4,7 +4,7 @@ import { contactCache } from "@/lib/cache/contact";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { NextRequest, userAgent } from "next/server";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZJsPersonIdentifyInput } from "@formbricks/types/js";
import { ZJsUserIdentifyInput } from "@formbricks/types/js";
import { getPersonState } from "./lib/personState";
export const OPTIONS = async (): Promise<Response> => {
@@ -21,7 +21,7 @@ export const GET = async (
const { environmentId, userId } = params;
// Validate input
const syncInputValidation = ZJsPersonIdentifyInput.safeParse({
const syncInputValidation = ZJsUserIdentifyInput.safeParse({
environmentId,
userId,
});
@@ -0,0 +1,39 @@
import { contactCache } from "@/lib/cache/contact";
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
export const getContactByUserIdWithAttributes = reactCache((environmentId: string, userId: string) =>
cache(
async () => {
const contact = await prisma.contact.findFirst({
where: {
environmentId,
attributes: { some: { attributeKey: { key: "userId", environmentId }, value: userId } },
},
select: {
id: true,
attributes: {
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
if (!contact) {
return null;
}
return contact;
},
[`getContactByUserIdWithAttributes-${environmentId}-${userId}`],
{
tags: [
contactCache.tag.byEnvironmentIdAndUserId(environmentId, userId),
contactAttributeCache.tag.byEnvironmentIdAndUserId(environmentId, userId),
contactAttributeKeyCache.tag.byEnvironmentId(environmentId),
],
}
)()
);
@@ -0,0 +1,82 @@
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
import { evaluateSegment } from "@/modules/ee/contacts/segments/lib/segments";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { segmentCache } from "@formbricks/lib/cache/segment";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId, ZString } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TBaseFilter } from "@formbricks/types/segment";
const getSegments = reactCache((environmentId: string) =>
cache(
async () => {
try {
return prisma.segment.findMany({
where: { environmentId },
select: { id: true, filters: true },
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getSegments-environmentId-${environmentId}`],
{
tags: [segmentCache.tag.byEnvironmentId(environmentId)],
}
)()
);
export const getPersonSegmentIds = (
environmentId: string,
contactId: string,
contactUserId: string,
attributes: Record<string, string>,
deviceType: "phone" | "desktop"
): Promise<string[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [contactId, ZId], [contactUserId, ZString]);
const segments = await getSegments(environmentId);
// fast path; if there are no segments, return an empty array
if (!segments) {
return [];
}
const personSegments: { id: string; filters: TBaseFilter[] }[] = [];
for (const segment of segments) {
const isIncluded = await evaluateSegment(
{
attributes,
deviceType,
environmentId,
contactId: contactId,
userId: contactUserId,
},
segment.filters
);
if (isIncluded) {
personSegments.push(segment);
}
}
return personSegments.map((segment) => segment.id);
},
[`getPersonSegmentIds-${environmentId}-${contactId}-${deviceType}`],
{
tags: [
segmentCache.tag.byEnvironmentId(environmentId),
contactAttributeCache.tag.byContactId(contactId),
],
}
)();
@@ -0,0 +1,129 @@
import { contactCache } from "@/lib/cache/contact";
import { updateAttributes } from "@/modules/ee/contacts/lib/attributes";
import { prisma } from "@formbricks/database";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TJsPersonState } from "@formbricks/types/js";
import { getContactByUserIdWithAttributes } from "./contact";
import { getUserState } from "./user-state";
export const updateUser = async (
environmentId: string,
userId: string,
device: "phone" | "desktop",
attributes?: Record<string, string>
): Promise<{ state: TJsPersonState; messages?: string[] }> => {
const environment = await getEnvironment(environmentId);
if (!environment) {
throw new ResourceNotFoundError(`environment`, environmentId);
}
let contact = await getContactByUserIdWithAttributes(environmentId, userId);
if (!contact) {
contact = await prisma.contact.create({
data: {
environment: {
connect: {
id: environmentId,
},
},
attributes: {
create: [
{
attributeKey: {
connect: { key_environmentId: { key: "userId", environmentId } },
},
value: userId,
},
],
},
},
select: {
id: true,
attributes: {
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
contactCache.revalidate({
environmentId,
userId,
id: contact.id,
});
}
let contactAttributes = contact.attributes.reduce(
(acc, ctx) => {
acc[ctx.attributeKey.key] = ctx.value;
return acc;
},
{} as Record<string, string>
);
// update the contact attributes if needed:
let messages: string[] = [];
let language = contactAttributes.language;
if (attributes && Object.keys(attributes).length > 0) {
let shouldUpdate = false;
const oldAttributes = contact.attributes.reduce(
(acc, ctx) => {
acc[ctx.attributeKey.key] = ctx.value;
return acc;
},
{} as Record<string, string>
);
for (const [key, value] of Object.entries(attributes)) {
if (value !== oldAttributes[key]) {
shouldUpdate = true;
break;
}
}
if (shouldUpdate) {
const { success, messages: updateAttrMessages } = await updateAttributes(
contact.id,
userId,
environmentId,
attributes
);
messages = updateAttrMessages ?? [];
// If the attributes update was successful and the language attribute was provided, set the language
if (success) {
contactAttributes = {
...contactAttributes,
...attributes,
};
if (attributes.language) {
language = attributes.language;
}
}
}
}
const userState = await getUserState({
environmentId,
userId,
contactId: contact.id,
attributes: contactAttributes,
device,
});
return {
state: {
data: {
...userState,
language,
},
expiresAt: new Date(Date.now() + 1000 * 60 * 30), // 30 minutes
},
messages,
};
};
@@ -0,0 +1,89 @@
import { contactCache } from "@/lib/cache/contact";
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { segmentCache } from "@formbricks/lib/cache/segment";
import { displayCache } from "@formbricks/lib/display/cache";
import { environmentCache } from "@formbricks/lib/environment/cache";
import { organizationCache } from "@formbricks/lib/organization/cache";
import { responseCache } from "@formbricks/lib/response/cache";
import { TJsPersonState } from "@formbricks/types/js";
import { getPersonSegmentIds } from "./segments";
/**
*
* @param environmentId - The environment id
* @param userId - The user id
* @param device - The device type
* @param attributes - The contact attributes
* @returns The person state
* @throws {ValidationError} - If the input is invalid
* @throws {ResourceNotFoundError} - If the environment or organization is not found
*/
export const getUserState = async ({
environmentId,
userId,
contactId,
device,
attributes,
}: {
environmentId: string;
userId: string;
contactId: string;
device: "phone" | "desktop";
attributes: Record<string, string>;
}): Promise<TJsPersonState["data"]> =>
cache(
async () => {
const contactResponses = await prisma.response.findMany({
where: {
contactId,
},
select: {
surveyId: true,
},
});
const contactDisplays = await prisma.display.findMany({
where: {
contactId,
},
select: {
surveyId: true,
createdAt: true,
},
});
const segments = await getPersonSegmentIds(environmentId, contactId, userId, attributes, device);
// If the person exists, return the persons's state
const userState: TJsPersonState["data"] = {
userId,
segments,
displays:
contactDisplays?.map((display) => ({
surveyId: display.surveyId,
createdAt: display.createdAt,
})) ?? [],
responses: contactResponses?.map((response) => response.surveyId) ?? [],
lastDisplayAt:
contactDisplays.length > 0
? contactDisplays.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0].createdAt
: null,
};
return userState;
},
[`personState-${environmentId}-${userId}-${device}`],
{
tags: [
environmentCache.tag.byId(environmentId),
organizationCache.tag.byEnvironmentId(environmentId),
contactCache.tag.byEnvironmentIdAndUserId(environmentId, userId),
contactAttributeCache.tag.byEnvironmentIdAndUserId(environmentId, userId),
displayCache.tag.byEnvironmentIdAndUserId(environmentId, userId),
responseCache.tag.byEnvironmentIdAndUserId(environmentId, userId),
segmentCache.tag.byEnvironmentId(environmentId),
],
}
)();
@@ -0,0 +1,92 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { NextRequest, userAgent } from "next/server";
import { TContactAttributes } from "@formbricks/types/contact-attribute";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TJsPersonState, ZJsUserIdentifyInput, ZJsUserUpdateInput } from "@formbricks/types/js";
import { updateUser } from "./lib/update-user";
export const OPTIONS = async (): Promise<Response> => {
return responses.successResponse({}, true);
};
export const POST = async (
request: NextRequest,
props: { params: Promise<{ environmentId: string }> }
): Promise<Response> => {
const params = await props.params;
try {
const { environmentId } = params;
const jsonInput = await request.json();
// Validate input
const syncInputValidation = ZJsUserIdentifyInput.pick({ environmentId: true }).safeParse({
environmentId,
});
if (!syncInputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(syncInputValidation.error),
true
);
}
const parsedInput = ZJsUserUpdateInput.safeParse(jsonInput);
if (!parsedInput.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(parsedInput.error),
true
);
}
const { userId, attributes } = parsedInput.data;
const isContactsEnabled = await getIsContactsEnabled();
if (!isContactsEnabled) {
return responses.forbiddenResponse("User identification is only available for enterprise users.", true);
}
let attributeUpdatesToSend: TContactAttributes | null = null;
if (attributes) {
// remove userId and id from attributes
const { userId: userIdAttr, id: idAttr, ...updatedAttributes } = attributes;
attributeUpdatesToSend = updatedAttributes;
}
const { device } = userAgent(request);
const deviceType = device ? "phone" : "desktop";
try {
const { state: userState, messages } = await updateUser(
environmentId,
userId,
deviceType,
attributeUpdatesToSend ?? undefined
);
let responseJson: { state: TJsPersonState; messages?: string[] } = {
state: userState,
};
if (messages && messages.length > 0) {
responseJson.messages = messages;
}
return responses.successResponse(responseJson, true);
} catch (err) {
if (err instanceof ResourceNotFoundError) {
return responses.notFoundResponse(err.resourceType, err.resourceId);
}
console.error(err);
return responses.internalServerErrorResponse(err.message ?? "Unable to fetch person state", true);
}
} catch (error) {
console.error(error);
return responses.internalServerErrorResponse(`Unable to complete response: ${error.message}`, true);
}
};
@@ -1,44 +1,18 @@
import "server-only";
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@formbricks/lib/constants";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId, ZString } from "@formbricks/types/common";
import { TContactAttributes, ZContactAttributes } from "@formbricks/types/contact-attribute";
export const getContactAttributeKeys = reactCache((environmentId: string) =>
cache(
async () => {
validateInputs([environmentId, ZId]);
const contactAttributes = await prisma.contactAttributeKey.findMany({
where: {
environmentId,
},
select: {
id: true,
key: true,
},
});
return contactAttributes;
},
[`getContactAttributeKeys-attributes-api-${environmentId}`],
{
tags: [contactAttributeKeyCache.tag.byEnvironmentId(environmentId)],
}
)()
);
import { getContactAttributeKeys } from "./contacts";
export const updateAttributes = async (
contactId: string,
userId: string,
environmentId: string,
contactAttributesParam: TContactAttributes
): Promise<{ success: boolean; details?: Record<string, string> }> => {
): Promise<{ success: boolean; messages?: string[] }> => {
validateInputs(
[contactId, ZId],
[userId, ZString],
@@ -96,9 +70,9 @@ export const updateAttributes = async (
}
);
let details: Record<string, string> = emailExists
? { email: "The email already exists for this environment and was not updated." }
: {};
let messages: string[] = emailExists
? ["The email already exists for this environment and was not updated."]
: [];
// First, update all existing attributes
if (existingAttributes.length > 0) {
@@ -122,9 +96,9 @@ export const updateAttributes = async (
);
// Revalidate cache for existing attributes
existingAttributes.map(({ key }) =>
contactAttributeCache.revalidate({ environmentId, contactId, userId, key })
);
for (const attribute of existingAttributes) {
contactAttributeCache.revalidate({ environmentId, contactId, userId, key: attribute.key });
}
}
// Then, try to create new attributes if any exist
@@ -133,10 +107,9 @@ export const updateAttributes = async (
if (totalAttributeClassesLength > MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT) {
// Add warning to details about skipped attributes
details = {
...details,
newAttributes: `Could not create ${newAttributes.length} new attribute(s) as it would exceed the maximum limit of ${MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT} attribute classes. Existing attributes were updated successfully.`,
};
messages.push(
`Could not create ${newAttributes.length} new attribute(s) as it would exceed the maximum limit of ${MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT} attribute classes. Existing attributes were updated successfully.`
);
} else {
// Create new attributes since we're under the limit
await prisma.$transaction(
@@ -155,16 +128,17 @@ export const updateAttributes = async (
);
// Batch revalidate caches for new attributes
newAttributes.forEach(({ key }) => {
contactAttributeKeyCache.revalidate({ environmentId, key });
contactAttributeCache.revalidate({ environmentId, contactId, userId, key });
});
for (const attribute of newAttributes) {
contactAttributeKeyCache.revalidate({ environmentId, key: attribute.key });
contactAttributeCache.revalidate({ environmentId, contactId, userId, key: attribute.key });
}
contactAttributeKeyCache.revalidate({ environmentId });
}
}
return {
success: true,
...(Object.keys(details).length > 0 ? { details } : {}),
messages,
};
};
@@ -148,9 +148,12 @@ export const deleteContact = async (contactId: string): Promise<TContact | null>
select: selectContact,
});
const contactUserId = contact.attributes.find((attr) => attr.attributeKey.key === "userId")?.value;
contactCache.revalidate({
id: contact.id,
environmentId: contact.environmentId,
userId: contactUserId,
});
return contact;
@@ -56,7 +56,6 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
setIsDeleting(false);
router.refresh();
} catch (err) {
console.log({ err });
setIsDeleting(false);
toast.error(t("common.something_went_wrong_please_try_again"));
}