From ab652c741720d00510af31c96dcc5f315e17a080 Mon Sep 17 00:00:00 2001 From: mattcasey Date: Sun, 29 Jun 2025 21:32:41 -0600 Subject: [PATCH 1/2] remove loops email --- .ebstalk.apps.env/webapp.env | 1 - apps/webapp/pages/api/profile/index.ts | 15 -- .../pages/api/profile/onboarding-email.ts | 27 --- apps/webapp/scripts/loopsEmail.js | 201 ------------------ apps/webapp/scripts/loopsEmail.ts | 37 ---- packages/lib/src/loopsEmail/client.ts | 97 --------- .../lib/src/loopsEmail/deleteLoopsContact.ts | 8 - .../src/loopsEmail/registerLoopsContact.ts | 34 --- .../lib/src/loopsEmail/sendSignupEvent.ts | 29 --- 9 files changed, 449 deletions(-) delete mode 100644 apps/webapp/scripts/loopsEmail.js delete mode 100644 apps/webapp/scripts/loopsEmail.ts delete mode 100644 packages/lib/src/loopsEmail/client.ts delete mode 100644 packages/lib/src/loopsEmail/deleteLoopsContact.ts delete mode 100644 packages/lib/src/loopsEmail/registerLoopsContact.ts delete mode 100644 packages/lib/src/loopsEmail/sendSignupEvent.ts diff --git a/.ebstalk.apps.env/webapp.env b/.ebstalk.apps.env/webapp.env index 8c4cdbd8c4..79142f4090 100644 --- a/.ebstalk.apps.env/webapp.env +++ b/.ebstalk.apps.env/webapp.env @@ -19,7 +19,6 @@ GOOGLE_OAUTH_CLIENT_SECRET_SENSITIVE="{{pull:secretsmanager:/io.cv.app/prd/googl GOOGLE_OAUTH_ENCRYPTION_SECRET="{{pull:secretsmanager:/io.cv.app/prd/google-auth:SecretString:google_oauth_encryption_secret}}" FIREBASE_PRIVATE_KEY="{{pull:secretsmanager:/io.cv.app/prd/firebase:SecretString:private_key}}" FIREBASE_CLIENT_EMAIL="{{pull:secretsmanager:/io.cv.app/prd/firebase:SecretString:client_email}}" -LOOPS_EMAIL_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/loops-email:SecretString:loops_email_api_key}}" MAILGUN_DOMAIN="{{pull:secretsmanager:/io.cv.app/prd/mailgun:SecretString:mailgun_domain}}" MAILGUN_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/mailgun:SecretString:mailgun_api_key}}" MIXPANEL_API_KEY="{{pull:secretsmanager:/io.cv.app/prd/mixpanel:SecretString:webapp_api_key}}" diff --git a/apps/webapp/pages/api/profile/index.ts b/apps/webapp/pages/api/profile/index.ts index 92c7d5aee5..ebd2cce05e 100644 --- a/apps/webapp/pages/api/profile/index.ts +++ b/apps/webapp/pages/api/profile/index.ts @@ -2,8 +2,6 @@ import { log } from '@charmverse/core/log'; import { prisma } from '@charmverse/core/prisma-client'; import type { SignatureVerificationPayload } from '@packages/lib/blockchain/signAndVerify'; import { updateGuildRolesForUser } from '@packages/lib/guild-xyz/server/updateGuildRolesForUser'; -import { deleteLoopsContact } from '@packages/lib/loopsEmail/deleteLoopsContact'; -import { registerLoopsContact } from '@packages/lib/loopsEmail/registerLoopsContact'; import { onError, onNoMatch, requireUser } from '@packages/lib/middleware'; import { requireWalletSignature } from '@packages/lib/middleware/requireWalletSignature'; import { withSessionRoute } from '@packages/lib/session/withSession'; @@ -100,19 +98,6 @@ async function updateUser(req: NextApiRequest, res: NextApiResponse role.spaceId === payload.spaceId && role.isAdmin); - const result = await registerLoopsContact(updatedUser); - if (result.isNewContact) { - await sendLoopSignupEvent({ - email: updatedUser.email, - isAdmin, - spaceName: space.name, - spaceTemplate - }); - log.info('Sent signup to Loop', { userId }); - } - } catch (error) { - log.error('Could not register user with Loop', { error, userId }); - } - } if (updatedUser.email) { const verifiedEmail = await prisma.verifiedEmail.count({ where: { diff --git a/apps/webapp/scripts/loopsEmail.js b/apps/webapp/scripts/loopsEmail.js deleted file mode 100644 index e7856a3dcc..0000000000 --- a/apps/webapp/scripts/loopsEmail.js +++ /dev/null @@ -1,201 +0,0 @@ -'use strict'; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator['throw'](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __generator = - (this && this.__generator) || - function (thisArg, body) { - var _ = { - label: 0, - sent: function () { - if (t[0] & 1) throw t[1]; - return t[1]; - }, - trys: [], - ops: [] - }, - f, - y, - t, - g; - return ( - (g = { next: verb(0), throw: verb(1), return: verb(2) }), - typeof Symbol === 'function' && - (g[Symbol.iterator] = function () { - return this; - }), - g - ); - function verb(n) { - return function (v) { - return step([n, v]); - }; - } - function step(op) { - if (f) throw new TypeError('Generator is already executing.'); - while ((g && ((g = 0), op[0] && (_ = 0)), _)) - try { - if ( - ((f = 1), - y && - (t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) && - !(t = t.call(y, op[1])).done) - ) - return t; - if (((y = 0), t)) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: - case 1: - t = op; - break; - case 4: - _.label++; - return { value: op[1], done: false }; - case 5: - _.label++; - y = op[1]; - op = [0]; - continue; - case 7: - op = _.ops.pop(); - _.trys.pop(); - continue; - default: - if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) { - _ = 0; - continue; - } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { - _.label = op[1]; - break; - } - if (op[0] === 6 && _.label < t[1]) { - _.label = t[1]; - t = op; - break; - } - if (t && _.label < t[2]) { - _.label = t[2]; - _.ops.push(op); - break; - } - if (t[2]) _.ops.pop(); - _.trys.pop(); - continue; - } - op = body.call(thisArg, _); - } catch (e) { - op = [6, e]; - y = 0; - } finally { - f = t = 0; - } - if (op[0] & 5) throw op[1]; - return { value: op[0] ? op[1] : void 0, done: true }; - } - }; -Object.defineProperty(exports, '__esModule', { value: true }); -var deleteLoopsContact_1 = require('lib/loopsEmail/deleteLoopsContact'); -var sendSignupEvent_1 = require('lib/loopsEmail/sendSignupEvent'); -var prisma_client_1 = require('@charmverse/core/prisma-client'); -var email = 'mattwad@gmail.com'; -function register() { - return __awaiter(this, void 0, void 0, function () { - var users; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - return [ - 4 /*yield*/, - prisma_client_1.prisma.user.findMany({ - where: { - email: { - not: null - }, - emailNewsletter: true - } - }) - ]; - case 1: - users = _a.sent(); - console.log('reigstering users', users.length); - return [2 /*return*/]; - } - }); - }); -} -function deleteUser() { - return __awaiter(this, void 0, void 0, function () { - var result; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - return [ - 4 /*yield*/, - (0, deleteLoopsContact_1.deleteLoopsContact)({ - email: email - }) - ]; - case 1: - result = _a.sent(); - console.log('result', result); - return [2 /*return*/]; - } - }); - }); -} -function signup() { - return __awaiter(this, void 0, void 0, function () { - var result; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - return [ - 4 /*yield*/, - (0, sendSignupEvent_1.sendSignupEvent)({ - email: email, - isAdmin: true, - spaceName: 'Matts bungalo', - spaceTemplate: 'Test' - }) - ]; - case 1: - result = _a.sent(); - console.log('result', result); - return [2 /*return*/]; - } - }); - }); -} -register().catch(function (error) { - console.error('Error', error); - process.exit(1); -}); diff --git a/apps/webapp/scripts/loopsEmail.ts b/apps/webapp/scripts/loopsEmail.ts deleted file mode 100644 index 068098c8a2..0000000000 --- a/apps/webapp/scripts/loopsEmail.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { registerLoopsContact } from 'lib/loopsEmail/registerLoopsContact'; -import { deleteLoopsContact } from 'lib/loopsEmail/deleteLoopsContact'; -import { sendSignupEvent } from 'lib/loopsEmail/sendSignupEvent'; - -const email = 'mattwad@gmail.com'; - -async function register() { - const result = await registerLoopsContact({ - createdAt: new Date(2022, 1), - username: 'Matt the I', - email: email, - emailNewsletter: true - }); - console.log(result); -} - -async function deleteUser() { - const result = await deleteLoopsContact({ - email: email - }); - console.log('result', result); -} - -async function signup() { - const result = await sendSignupEvent({ - email: email, - isAdmin: true, - spaceName: 'Matts bungalo', - spaceTemplate: 'Test' - }); - console.log('result', result); -} - -signup().catch((error) => { - console.error('Error', error); - process.exit(1); -}); diff --git a/packages/lib/src/loopsEmail/client.ts b/packages/lib/src/loopsEmail/client.ts deleted file mode 100644 index 2bc8c6f25a..0000000000 --- a/packages/lib/src/loopsEmail/client.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { GET, POST, PUT } from '@charmverse/core/http'; -import { RateLimit } from 'async-sema'; - -// Loop manages onboarding emails -// Visit the dashboard: https://app.loops.so/settings?page=api -// API docs: https://loops.so/docs/api - -const apiBaseUrl = 'https://app.loops.so/api/v1'; -const apiToken = process.env.LOOPS_EMAIL_API_KEY as string | undefined; -export const isEnabled = !!apiToken; - -// 10 requests/second - https://loops.so/docs/api -const rateLimiter = RateLimit(10); - -// these properties can be sent as part of events but will be applied to the LoopUser profile -export type SignupEventProperties = { - spaceName: string; - spaceTemplate?: string; - spaceRole: 'Admin' | 'Member'; -}; - -export type SignupEvent = { - email: string; - eventName: 'signup'; -} & SignupEventProperties; - -export type LoopsUser = { - id: string; - firstName: string; - email: string; - createdAt: string; - source: 'Web App'; // TODO: what should this be? - subscribed?: boolean; -} & Partial; - -const headers = { - Authorization: `Bearer ${apiToken}` -}; - -// for testing api keys -export function testApiKey() { - return GET<{ success: true }>(`${apiBaseUrl}/api-key`, null, { headers }); -} - -type CreateContactResponse = { - success: boolean; - id: string; // user id - status?: 409; // 409 means already exists -}; - -export async function createContact(payload: Omit) { - await rateLimiter(); - return POST(`${apiBaseUrl}/contacts/create`, payload, { - headers - }); -} - -type UpdateContactResponse = { - success: boolean; - id: string; // user id -}; - -export async function updateContact(payload: Omit) { - await rateLimiter(); - return PUT(`${apiBaseUrl}/contacts/update`, payload, { - headers - }); -} - -// Note: teh docs say that update will create a user if they don't exist, but it doesn't seem to work -export async function createOrUpdateContact(payload: Omit) { - const [existing] = await findContact(payload); - const result = await (existing ? updateContact(payload) : createContact(payload)); - return { ...result, isNewContact: !existing }; -} - -// when a user deletes their email, delete them from Loops as well -export async function deleteContact(payload: { email: string }) { - await rateLimiter(); - return POST<{ success: boolean }>(`${apiBaseUrl}/contacts/delete`, payload, { - headers - }); -} - -export async function findContact(payload: { email: string }) { - await rateLimiter(); - return GET(`${apiBaseUrl}/contacts/find`, payload, { - headers - }); -} - -export async function sendEvent(payload: T) { - await rateLimiter(); - return POST<{ success: boolean }>(`${apiBaseUrl}/events/send`, payload, { - headers - }); -} diff --git a/packages/lib/src/loopsEmail/deleteLoopsContact.ts b/packages/lib/src/loopsEmail/deleteLoopsContact.ts deleted file mode 100644 index b068e5f26a..0000000000 --- a/packages/lib/src/loopsEmail/deleteLoopsContact.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isEnabled, deleteContact } from './client'; - -export async function deleteLoopsContact({ email }: { email: string }) { - if (!isEnabled) { - return { success: false }; - } - return deleteContact({ email }); -} diff --git a/packages/lib/src/loopsEmail/registerLoopsContact.ts b/packages/lib/src/loopsEmail/registerLoopsContact.ts deleted file mode 100644 index 5f02eb1f96..0000000000 --- a/packages/lib/src/loopsEmail/registerLoopsContact.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { User } from '@charmverse/core/prisma'; - -import type { LoopsUser } from './client'; -import { isEnabled, createOrUpdateContact } from './client'; -import { deleteLoopsContact } from './deleteLoopsContact'; - -type UserFields = Pick; - -// Creates a user if one does not exist -// Call this whenever a user toggles subscriptions, ie. "emailNewsletter", or update their email -export async function registerLoopsContact(user: UserFields, oldEmail?: string | null) { - if (!isEnabled) { - return { success: false, isNewContact: false }; - } - if (oldEmail && oldEmail !== user.email) { - await deleteLoopsContact({ email: oldEmail }); - } - return createOrUpdateContact({ - ...getLoopsUser(user), - source: 'Web App' - }); -} - -function getLoopsUser(user: UserFields): Pick { - if (!user.email) { - throw new Error('User does not have an email'); - } - return { - firstName: user.username, - email: user.email, - createdAt: user.createdAt.toISOString(), - subscribed: !!user.emailNewsletter - }; -} diff --git a/packages/lib/src/loopsEmail/sendSignupEvent.ts b/packages/lib/src/loopsEmail/sendSignupEvent.ts deleted file mode 100644 index 645bbb55c9..0000000000 --- a/packages/lib/src/loopsEmail/sendSignupEvent.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { stringToHumanFormat } from '@packages/metrics/mixpanel/utils'; - -import type { SignupEvent } from './client'; -import { isEnabled, sendEvent } from './client'; - -// Create a user if one does not exist -export async function sendSignupEvent({ - email, - isAdmin, - spaceName, - spaceTemplate -}: { - email: string; - isAdmin: boolean; - spaceName: string; - spaceTemplate?: string; -}) { - if (!isEnabled) { - return { success: false }; - } - const event: SignupEvent = { - email, - eventName: 'signup', - spaceName, - spaceRole: isAdmin ? 'Admin' : 'Member', - spaceTemplate: spaceTemplate ? stringToHumanFormat(spaceTemplate) : undefined - }; - return sendEvent(event); -} From 665cde7788aede7b96119db117895c1efef9499b Mon Sep 17 00:00:00 2001 From: mattcasey Date: Sun, 29 Jun 2025 21:34:58 -0600 Subject: [PATCH 2/2] remove checkbo x --- .../_app/components/OnboardingEmailForm.tsx | 15 ++++----------- .../account/components/AccountPreferences.tsx | 12 +----------- apps/webapp/pages/api/profile/onboarding-email.ts | 1 - .../src/lib/profile/onboardingAction.ts | 3 +-- .../users/src/__tests__/updateUserProfile.spec.ts | 2 -- packages/users/src/updateUserProfile.ts | 1 - 6 files changed, 6 insertions(+), 28 deletions(-) diff --git a/apps/webapp/components/_app/components/OnboardingEmailForm.tsx b/apps/webapp/components/_app/components/OnboardingEmailForm.tsx index 1cdf1e8a15..8f5ab55bff 100644 --- a/apps/webapp/components/_app/components/OnboardingEmailForm.tsx +++ b/apps/webapp/components/_app/components/OnboardingEmailForm.tsx @@ -13,13 +13,12 @@ import { useUser } from 'hooks/useUser'; const emailSchema = yup.string().email().ensure().trim(); export const schema = yup.object({ - email: emailSchema.when(['emailNewsletter', 'emailNotifications'], { - is: (emailNewsletter: boolean, emailNotifications: boolean) => emailNewsletter || emailNotifications, + email: emailSchema.when(['emailNotifications'], { + is: (emailNotifications: boolean) => emailNotifications, then: () => emailSchema.required('Unselect email options to proceed without email'), otherwise: () => emailSchema }), emailNotifications: yup.boolean(), - emailNewsletter: yup.boolean(), agreeTermsConditions: yup.boolean().oneOf([true], 'You must agree to the terms and privacy policy to continue') }); @@ -46,7 +45,6 @@ export function OnboardingEmailForm({ onClick, spaceId }: { onClick: VoidFunctio resolver: yupResolver(schema) }); - const emailNewsletter = getValues('emailNewsletter'); const emailNotifications = getValues('emailNotifications'); const agreeTermsConditions = getValues('agreeTermsConditions'); @@ -58,14 +56,13 @@ export function OnboardingEmailForm({ onClick, spaceId }: { onClick: VoidFunctio const onSubmit = async (values: FormValues) => { // eslint-disable-next-line @typescript-eslint/no-shadow - const { email, emailNewsletter, emailNotifications } = values; + const { email, emailNotifications } = values; await saveForm({ email: email || undefined, - emailNewsletter, emailNotifications, spaceId }); - updateUser({ ...user, email, emailNewsletter, emailNotifications }); + updateUser({ ...user, email, emailNotifications }); onClick(); }; @@ -97,10 +94,6 @@ export function OnboardingEmailForm({ onClick, spaceId }: { onClick: VoidFunctio } label='Notify me about key activities (e.g., proposal feedback, reward status, mentions, comments)' /> - } - label='Receive tips and examples how to use CharmVerse.' - />