Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/unsubscriber/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,13 @@ async function performFallbackUnsubscribe(page: Page): Promise<boolean> {
return false;
}

export async function autoUnsubscribe(url: string): Promise<boolean> {
export async function autoUnsubscribe({
url,
email,
}: {
url: string;
email: string;
}): Promise<boolean> {
if (!isValidUrl(url)) {
console.error("Invalid URL provided:", url);
return false;
Expand Down
5 changes: 3 additions & 2 deletions apps/unsubscriber/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ if (env.CORS_ORIGIN) {

const unsubscribeSchema = z.object({
url: z.string().url(),
email: z.string().email(),
});

server.get("/", async (request, reply) => {
Expand All @@ -24,8 +25,8 @@ server.get("/", async (request, reply) => {

server.post("/unsubscribe", async (request, reply) => {
try {
const { url } = unsubscribeSchema.parse(request.body);
const success = await autoUnsubscribe(url);
const { url, email } = unsubscribeSchema.parse(request.body);
const success = await autoUnsubscribe({ url, email });
return {
success,
message: success
Expand Down
10 changes: 9 additions & 1 deletion apps/web/app/(app)/bulk-unsubscribe/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import React, { useCallback, useState } from "react";
import { toast } from "sonner";
import type { PostHog } from "posthog-js/react";
import { onAutoArchive, onDeleteFilter } from "@/utils/actions/client";
import { setNewsletterStatusAction } from "@/utils/actions/unsubscriber";
import {
setNewsletterStatusAction,
unsubscribeAction,
} from "@/utils/actions/unsubscriber";
import { decrementUnsubscribeCreditAction } from "@/utils/actions/premium";
import { NewsletterStatus } from "@prisma/client";
import { cleanUnsubscribeLink } from "@/utils/parse/parseHtml.client";
Expand Down Expand Up @@ -54,6 +57,10 @@ export function useUnsubscribe<T extends Row>({
posthog.capture("Clicked Unsubscribe");

if (item.status === NewsletterStatus.UNSUBSCRIBED) {
if (item.lastUnsubscribeLink) {
await unsubscribeAction({ url: item.lastUnsubscribeLink });
}

Comment on lines +60 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Added auto-unsubscribe functionality using the unsubscribe link

This change adds automatic unsubscription via API when a user has an existing unsubscribe link. However, there's a potential inconsistency between the implementation and expected behavior.

While the code passes only the URL to unsubscribeAction, the AI summary suggests the function should accept both url and email parameters. Let's verify the actual implementation:

Also, there's no handling for potential failures from the unsubscribe API call. Consider adding a check for the response and only setting the newsletter status to null if the unsubscribe action succeeds.


🏁 Script executed:

#!/bin/bash
# Check if unsubscribeAction accepts both url and email or just url
cat apps/web/utils/actions/unsubscriber.ts | grep -A 10 "unsubscribeAction"

Length of output: 451


Action Required: Improve error handling in auto-unsubscribe flow

  • The unsubscribeAction function is correctly implemented to accept only a url parameter, with the email automatically retrieved from the session. There’s no need to pass the email explicitly.
  • However, the current implementation in apps/web/app/(app)/bulk-unsubscribe/hooks.ts does not handle failures from the unsubscribe API call. Consider checking the response from unsubscribeAction and only updating the newsletter status to null if the action indicates success.

await setNewsletterStatusAction({
newsletterEmail: item.name,
status: null,
Expand All @@ -72,6 +79,7 @@ export function useUnsubscribe<T extends Row>({
hasUnsubscribeAccess,
item.name,
item.status,
item.lastUnsubscribeLink,
mutate,
refetchPremium,
posthog,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export const env = createEnv({
LICENSE_5_SEAT_VARIANT_ID: z.coerce.number().optional(),
LICENSE_10_SEAT_VARIANT_ID: z.coerce.number().optional(),
LICENSE_25_SEAT_VARIANT_ID: z.coerce.number().optional(),

UNSUBSCRIBER_API_URL: z.string().optional(),
},
client: {
NEXT_PUBLIC_LEMON_STORE_ID: z.string().nullish().default("inboxzero"),
Expand Down
21 changes: 21 additions & 0 deletions apps/web/utils/actions/unsubscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { auth } from "@/app/api/auth/[...nextauth]/auth";
import prisma from "@/utils/prisma";
import type { NewsletterStatus } from "@prisma/client";
import { withActionInstrumentation } from "@/utils/actions/middleware";
import { env } from "@/env";

export const setNewsletterStatusAction = withActionInstrumentation(
"setNewsletterStatus",
Expand All @@ -30,3 +31,23 @@ export const setNewsletterStatusAction = withActionInstrumentation(
});
},
);

export const unsubscribeAction = withActionInstrumentation(
"unsubscribe",
async (options: { url: string }) => {
const session = await auth();
if (!session?.user.email) return { error: "Not logged in" };

const { url } = options;

const response = await fetch(
`${env.UNSUBSCRIBER_API_URL}/unsubscribe?url=${url}&email=${session.user.email}`,
);

Comment on lines +43 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

API endpoint inconsistency with server implementation.

The server implementation in apps/unsubscriber/src/server.ts expects the URL and email in the request body, but this client is sending them as query parameters.

The request format should match what the server expects. The server is set up to parse the request body, not query parameters.

if (!response.ok) {
return { error: "Failed to unsubscribe" };
}

return { success: true };
},
);
Comment on lines +35 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implemented new server action for unsubscription.

The new unsubscribeAction server action correctly:

  1. Verifies user authentication
  2. Extracts the URL from options
  3. Makes a request to the unsubscriber API
  4. Handles the response appropriately

The URL parameters in the fetch request are not being encoded, which could lead to issues with special characters in URLs or emails:

- const response = await fetch(
-   `${env.UNSUBSCRIBER_API_URL}/unsubscribe?url=${url}&email=${session.user.email}`,
- );
+ const response = await fetch(
+   `${env.UNSUBSCRIBER_API_URL}/unsubscribe`, {
+     method: 'POST',
+     headers: {
+       'Content-Type': 'application/json',
+     },
+     body: JSON.stringify({ url, email: session.user.email }),
+   }
+ );

This change:

  1. Uses POST with a JSON body instead of query parameters
  2. Avoids URL encoding issues
  3. Aligns with how the server is expecting to receive the data (in the request body)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const unsubscribeAction = withActionInstrumentation(
"unsubscribe",
async (options: { url: string }) => {
const session = await auth();
if (!session?.user.email) return { error: "Not logged in" };
const { url } = options;
const response = await fetch(
`${env.UNSUBSCRIBER_API_URL}/unsubscribe?url=${url}&email=${session.user.email}`,
);
if (!response.ok) {
return { error: "Failed to unsubscribe" };
}
return { success: true };
},
);
export const unsubscribeAction = withActionInstrumentation(
"unsubscribe",
async (options: { url: string }) => {
const session = await auth();
if (!session?.user.email) return { error: "Not logged in" };
const { url } = options;
const response = await fetch(
`${env.UNSUBSCRIBER_API_URL}/unsubscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, email: session.user.email }),
}
);
if (!response.ok) {
return { error: "Failed to unsubscribe" };
}
return { success: true };
},
);