feat(backend,frontend): Send applicant email on review response (#10718)

### Changes 🏗️

This PR implements email notifications for agent creators when their
agent submissions are approved or rejected by an admin in the
marketplace.

Specifically, the changes include:
- Added `AGENT_APPROVED` and `AGENT_REJECTED` notification types to
`schema.prisma`.
- Created `AgentApprovalData` and `AgentRejectionData` Pydantic models
for notification data.
- Configured the notification system to use immediate queues and new
Jinja2 templates for these types.
- Designed two new email templates: `agent_approved.html.jinja2` and
`agent_rejected.html.jinja2`, with dynamic content for agent details,
reviewer feedback, and relevant action links.
- Modified the `review_store_submission` function to:
    - Include `User` and `Reviewer` data in the database query.
- Construct and queue the appropriate email notification based on the
approval/rejection status.
- Ensure email sending failures do not block the agent review process.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Approve an agent via the admin dashboard.
- [x] Verify the agent creator receives an "Agent Approved" email with
correct details and a link to the store.
  - [x] Reject an agent via the admin dashboard (providing a reason).
- [x] Verify the agent creator receives an "Agent Rejected" email with
correct details, the rejection reason, and a link to resubmit.
- [x] Verify that if email sending fails (e.g., misconfigured SMTP), the
agent approval/rejection process still completes successfully without
error.

<img width="664" height="975" alt="image"
src="https://github.com/user-attachments/assets/d397f2dc-56eb-45ab-877e-b17f1fc234d1"
/>
<img width="664" height="975" alt="image"
src="https://github.com/user-attachments/assets/25597752-f68c-46fe-8888-6c32f5dada01"
/>


---
Linear Issue: [SECRT-1168](https://linear.app/autogpt/issue/SECRT-1168)

<a
href="https://cursor.com/background-agent?bcId=bc-7394906c-0341-4bd0-8842-6d9d6f83c56c">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/open-in-cursor-dark.svg">
<source media="(prefers-color-scheme: light)"
srcset="https://cursor.com/open-in-cursor-light.svg">
<img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg">
  </picture>
</a>
<a
href="https://cursor.com/agents?id=bc-7394906c-0341-4bd0-8842-6d9d6f83c56c">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/open-in-web-dark.svg">
<source media="(prefers-color-scheme: light)"
srcset="https://cursor.com/open-in-web-light.svg">
    <img alt="Open in Web" src="https://cursor.com/open-in-web.svg">
  </picture>
</a>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Nicholas Tindle
2025-08-25 09:24:16 -05:00
committed by GitHub
parent 5b12e02c4e
commit 76090f0ba2
12 changed files with 539 additions and 3 deletions

View File

@@ -181,6 +181,42 @@ class RefundRequestData(BaseNotificationData):
balance: int
class AgentApprovalData(BaseNotificationData):
agent_name: str
agent_id: str
agent_version: int
reviewer_name: str
reviewer_email: str
comments: str
reviewed_at: datetime
store_url: str
@field_validator("reviewed_at")
@classmethod
def validate_timezone(cls, value: datetime):
if value.tzinfo is None:
raise ValueError("datetime must have timezone information")
return value
class AgentRejectionData(BaseNotificationData):
agent_name: str
agent_id: str
agent_version: int
reviewer_name: str
reviewer_email: str
comments: str
reviewed_at: datetime
resubmit_url: str
@field_validator("reviewed_at")
@classmethod
def validate_timezone(cls, value: datetime):
if value.tzinfo is None:
raise ValueError("datetime must have timezone information")
return value
NotificationData = Annotated[
Union[
AgentRunData,
@@ -240,6 +276,8 @@ def get_notif_data_type(
NotificationType.MONTHLY_SUMMARY: MonthlySummaryData,
NotificationType.REFUND_REQUEST: RefundRequestData,
NotificationType.REFUND_PROCESSED: RefundRequestData,
NotificationType.AGENT_APPROVED: AgentApprovalData,
NotificationType.AGENT_REJECTED: AgentRejectionData,
}[notification_type]
@@ -283,6 +321,8 @@ class NotificationTypeOverride:
NotificationType.MONTHLY_SUMMARY: QueueType.SUMMARY,
NotificationType.REFUND_REQUEST: QueueType.ADMIN,
NotificationType.REFUND_PROCESSED: QueueType.ADMIN,
NotificationType.AGENT_APPROVED: QueueType.IMMEDIATE,
NotificationType.AGENT_REJECTED: QueueType.IMMEDIATE,
}
return BATCHING_RULES.get(self.notification_type, QueueType.IMMEDIATE)
@@ -300,6 +340,8 @@ class NotificationTypeOverride:
NotificationType.MONTHLY_SUMMARY: "monthly_summary.html",
NotificationType.REFUND_REQUEST: "refund_request.html",
NotificationType.REFUND_PROCESSED: "refund_processed.html",
NotificationType.AGENT_APPROVED: "agent_approved.html",
NotificationType.AGENT_REJECTED: "agent_rejected.html",
}[self.notification_type]
@property
@@ -315,6 +357,8 @@ class NotificationTypeOverride:
NotificationType.MONTHLY_SUMMARY: "We did a lot this month!",
NotificationType.REFUND_REQUEST: "[ACTION REQUIRED] You got a ${{data.amount / 100}} refund request from {{data.user_name}}",
NotificationType.REFUND_PROCESSED: "Refund for ${{data.amount / 100}} to {{data.user_name}} has been processed",
NotificationType.AGENT_APPROVED: "🎉 Your agent '{{data.agent_name}}' has been approved!",
NotificationType.AGENT_REJECTED: "Your agent '{{data.agent_name}}' needs some updates",
}[self.notification_type]

View File

@@ -0,0 +1,151 @@
"""Tests for notification data models."""
from datetime import datetime, timezone
import pytest
from pydantic import ValidationError
from backend.data.notifications import AgentApprovalData, AgentRejectionData
class TestAgentApprovalData:
"""Test cases for AgentApprovalData model."""
def test_valid_agent_approval_data(self):
"""Test creating valid AgentApprovalData."""
data = AgentApprovalData(
agent_name="Test Agent",
agent_id="test-agent-123",
agent_version=1,
reviewer_name="John Doe",
reviewer_email="john@example.com",
comments="Great agent, approved!",
reviewed_at=datetime.now(timezone.utc),
store_url="https://app.autogpt.com/store/test-agent-123",
)
assert data.agent_name == "Test Agent"
assert data.agent_id == "test-agent-123"
assert data.agent_version == 1
assert data.reviewer_name == "John Doe"
assert data.reviewer_email == "john@example.com"
assert data.comments == "Great agent, approved!"
assert data.store_url == "https://app.autogpt.com/store/test-agent-123"
assert data.reviewed_at.tzinfo is not None
def test_agent_approval_data_without_timezone_raises_error(self):
"""Test that AgentApprovalData raises error without timezone."""
with pytest.raises(
ValidationError, match="datetime must have timezone information"
):
AgentApprovalData(
agent_name="Test Agent",
agent_id="test-agent-123",
agent_version=1,
reviewer_name="John Doe",
reviewer_email="john@example.com",
comments="Great agent, approved!",
reviewed_at=datetime.now(), # No timezone
store_url="https://app.autogpt.com/store/test-agent-123",
)
def test_agent_approval_data_with_empty_comments(self):
"""Test AgentApprovalData with empty comments."""
data = AgentApprovalData(
agent_name="Test Agent",
agent_id="test-agent-123",
agent_version=1,
reviewer_name="John Doe",
reviewer_email="john@example.com",
comments="", # Empty comments
reviewed_at=datetime.now(timezone.utc),
store_url="https://app.autogpt.com/store/test-agent-123",
)
assert data.comments == ""
class TestAgentRejectionData:
"""Test cases for AgentRejectionData model."""
def test_valid_agent_rejection_data(self):
"""Test creating valid AgentRejectionData."""
data = AgentRejectionData(
agent_name="Test Agent",
agent_id="test-agent-123",
agent_version=1,
reviewer_name="Jane Doe",
reviewer_email="jane@example.com",
comments="Please fix the security issues before resubmitting.",
reviewed_at=datetime.now(timezone.utc),
resubmit_url="https://app.autogpt.com/build/test-agent-123",
)
assert data.agent_name == "Test Agent"
assert data.agent_id == "test-agent-123"
assert data.agent_version == 1
assert data.reviewer_name == "Jane Doe"
assert data.reviewer_email == "jane@example.com"
assert data.comments == "Please fix the security issues before resubmitting."
assert data.resubmit_url == "https://app.autogpt.com/build/test-agent-123"
assert data.reviewed_at.tzinfo is not None
def test_agent_rejection_data_without_timezone_raises_error(self):
"""Test that AgentRejectionData raises error without timezone."""
with pytest.raises(
ValidationError, match="datetime must have timezone information"
):
AgentRejectionData(
agent_name="Test Agent",
agent_id="test-agent-123",
agent_version=1,
reviewer_name="Jane Doe",
reviewer_email="jane@example.com",
comments="Please fix the security issues.",
reviewed_at=datetime.now(), # No timezone
resubmit_url="https://app.autogpt.com/build/test-agent-123",
)
def test_agent_rejection_data_with_long_comments(self):
"""Test AgentRejectionData with long comments."""
long_comment = "A" * 1000 # Very long comment
data = AgentRejectionData(
agent_name="Test Agent",
agent_id="test-agent-123",
agent_version=1,
reviewer_name="Jane Doe",
reviewer_email="jane@example.com",
comments=long_comment,
reviewed_at=datetime.now(timezone.utc),
resubmit_url="https://app.autogpt.com/build/test-agent-123",
)
assert data.comments == long_comment
def test_model_serialization(self):
"""Test that models can be serialized and deserialized."""
original_data = AgentRejectionData(
agent_name="Test Agent",
agent_id="test-agent-123",
agent_version=1,
reviewer_name="Jane Doe",
reviewer_email="jane@example.com",
comments="Please fix the issues.",
reviewed_at=datetime.now(timezone.utc),
resubmit_url="https://app.autogpt.com/build/test-agent-123",
)
# Serialize to dict
data_dict = original_data.model_dump()
# Deserialize back
restored_data = AgentRejectionData.model_validate(data_dict)
assert restored_data.agent_name == original_data.agent_name
assert restored_data.agent_id == original_data.agent_id
assert restored_data.agent_version == original_data.agent_version
assert restored_data.reviewer_name == original_data.reviewer_name
assert restored_data.reviewer_email == original_data.reviewer_email
assert restored_data.comments == original_data.comments
assert restored_data.reviewed_at == original_data.reviewed_at
assert restored_data.resubmit_url == original_data.resubmit_url

View File

@@ -208,6 +208,8 @@ async def get_user_notification_preference(user_id: str) -> NotificationPreferen
NotificationType.DAILY_SUMMARY: user.notifyOnDailySummary or False,
NotificationType.WEEKLY_SUMMARY: user.notifyOnWeeklySummary or False,
NotificationType.MONTHLY_SUMMARY: user.notifyOnMonthlySummary or False,
NotificationType.AGENT_APPROVED: user.notifyOnAgentApproved or False,
NotificationType.AGENT_REJECTED: user.notifyOnAgentRejected or False,
}
daily_limit = user.maxEmailsPerDay or 3
notification_preference = NotificationPreference(
@@ -266,6 +268,14 @@ async def update_user_notification_preference(
update_data["notifyOnMonthlySummary"] = data.preferences[
NotificationType.MONTHLY_SUMMARY
]
if NotificationType.AGENT_APPROVED in data.preferences:
update_data["notifyOnAgentApproved"] = data.preferences[
NotificationType.AGENT_APPROVED
]
if NotificationType.AGENT_REJECTED in data.preferences:
update_data["notifyOnAgentRejected"] = data.preferences[
NotificationType.AGENT_REJECTED
]
if data.daily_limit:
update_data["maxEmailsPerDay"] = data.daily_limit
@@ -286,6 +296,8 @@ async def update_user_notification_preference(
NotificationType.DAILY_SUMMARY: user.notifyOnDailySummary or True,
NotificationType.WEEKLY_SUMMARY: user.notifyOnWeeklySummary or True,
NotificationType.MONTHLY_SUMMARY: user.notifyOnMonthlySummary or True,
NotificationType.AGENT_APPROVED: user.notifyOnAgentApproved or True,
NotificationType.AGENT_REJECTED: user.notifyOnAgentRejected or True,
}
notification_preference = NotificationPreference(
user_id=user.id,

View File

@@ -0,0 +1,73 @@
{# Agent Approved Notification Email Template #}
{#
Template variables:
data.agent_name: the name of the approved agent
data.agent_id: the ID of the agent
data.agent_version: the version of the agent
data.reviewer_name: the name of the reviewer who approved it
data.reviewer_email: the email of the reviewer
data.comments: comments from the reviewer
data.reviewed_at: when the agent was reviewed
data.store_url: URL to view the agent in the store
Subject: 🎉 Your agent '{{ data.agent_name }}' has been approved!
#}
{% block content %}
<h1 style="color: #28a745; font-size: 32px; font-weight: 700; margin: 0 0 24px 0; text-align: center;">
🎉 Congratulations!
</h1>
<p style="color: #586069; font-size: 18px; text-align: center; margin: 0 0 24px 0;">
Your agent <strong>'{{ data.agent_name }}'</strong> has been approved and is now live in the store!
</p>
<div style="height: 32px; background: transparent;"></div>
{% if data.comments %}
<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 20px; margin: 0;">
<h3 style="color: #155724; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">
💬 Creator feedback area
</h3>
<p style="color: #155724; margin: 0; font-size: 16px; line-height: 1.5;">
{{ data.comments }}
</p>
</div>
<div style="height: 40px; background: transparent;"></div>
{% endif %}
<div style="background: #d1ecf1; border: 1px solid #bee5eb; border-radius: 8px; padding: 20px; margin: 0;">
<h3 style="color: #0c5460; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">
What's Next?
</h3>
<ul style="color: #0c5460; margin: 0; padding-left: 18px; font-size: 16px; line-height: 1.6;">
<li>Your agent is now live and discoverable in the AutoGPT Store</li>
<li>Users can find, install, and run your agent</li>
<li>You can update your agent anytime by submitting a new version</li>
</ul>
</div>
<div style="height: 32px; background: transparent;"></div>
<div style="text-align: center; margin: 24px 0;">
<a href="{{ data.store_url }}" style="display: inline-block; background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); color: black; text-decoration: none; padding: 14px 28px; border-radius: 6px; font-weight: 600; font-size: 16px;">
View Your Agent in Store
</a>
</div>
<div style="height: 32px; background: transparent;"></div>
<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; padding: 16px; margin: 24px 0; text-align: center;">
<p style="margin: 0; color: #856404; font-size: 14px;">
<strong>💡 Pro Tip:</strong> Share your agent with the community! Post about it on social media, forums, or your blog to help more users discover and benefit from your creation.
</p>
</div>
<div style="height: 32px; background: transparent;"></div>
<p style="color: #6a737d; font-size: 14px; text-align: center; margin: 24px 0;">
Thank you for contributing to the AutoGPT ecosystem! 🚀
</p>
{% endblock %}

View File

@@ -0,0 +1,77 @@
{# Agent Rejected Notification Email Template #}
{#
Template variables:
data.agent_name: the name of the rejected agent
data.agent_id: the ID of the agent
data.agent_version: the version of the agent
data.reviewer_name: the name of the reviewer who rejected it
data.reviewer_email: the email of the reviewer
data.comments: comments from the reviewer explaining the rejection
data.reviewed_at: when the agent was reviewed
data.resubmit_url: URL to resubmit the agent
Subject: Your agent '{{ data.agent_name }}' needs some updates
#}
{% block content %}
<h1 style="color: #d73a49; font-size: 32px; font-weight: 700; margin: 0 0 24px 0; text-align: center;">
📝 Review Complete
</h1>
<p style="color: #586069; font-size: 18px; text-align: center; margin: 0 0 24px 0;">
Your agent <strong>'{{ data.agent_name }}'</strong> needs some updates before approval.
</p>
<div style="height: 32px; background: transparent;"></div>
<div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 8px; padding: 20px; margin: 0 0 24px 0;">
<h3 style="color: #721c24; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">
💬 Creator feedback area
</h3>
<p style="color: #721c24; margin: 0; font-size: 16px; line-height: 1.5;">
{{ data.comments }}
</p>
</div>
<div style="height: 40px; background: transparent;"></div>
<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 20px; margin: 0 0 24px 0;">
<h3 style="color: #155724; font-size: 16px; font-weight: 600; margin: 0 0 12px 0;">
☑ Steps to Resubmit:
</h3>
<ul style="color: #155724; margin: 0; padding-left: 18px; font-size: 16px; line-height: 1.6;">
<li>Review the feedback provided above carefully</li>
<li>Make the necessary updates to your agent</li>
<li>Test your agent thoroughly to ensure it works as expected</li>
<li>Submit your updated agent for review</li>
</ul>
</div>
<div style="height: 32px; background: transparent;"></div>
<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; padding: 12px; margin: 0 0 24px 0; text-align: center;">
<p style="margin: 0; color: #856404; font-size: 14px;">
<strong>💡 Tip:</strong> Address all the points mentioned in the feedback to increase your chances of approval in the next review.
</p>
</div>
<div style="text-align: center; margin: 32px 0;">
<a href="{{ data.resubmit_url }}" style="display: inline-block; background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); color: black; text-decoration: none; padding: 14px 28px; border-radius: 6px; font-weight: 600; font-size: 16px;">
Update & Resubmit Agent
</a>
</div>
<div style="background: #d1ecf1; border: 1px solid #bee5eb; border-radius: 6px; padding: 16px; margin: 24px 0;">
<p style="margin: 0; color: #0c5460; font-size: 14px; text-align: center;">
<strong>🌟 Don't Give Up!</strong> Many successful agents go through multiple iterations before approval. Our review team is here to help you succeed!
</p>
</div>
<div style="height: 32px; background: transparent;"></div>
<p style="color: #6a737d; font-size: 14px; text-align: center; margin: 32px 0 24px 0;">
We're excited to see your improved agent submission! 🚀
</p>
{% endblock %}

View File

@@ -17,8 +17,21 @@ from backend.data.graph import (
get_sub_graphs,
)
from backend.data.includes import AGENT_GRAPH_INCLUDE
from backend.data.notifications import (
AgentApprovalData,
AgentRejectionData,
NotificationEventModel,
)
from backend.notifications.notifications import queue_notification_async
from backend.util.settings import Settings
logger = logging.getLogger(__name__)
settings = Settings()
# Constants for default admin values
DEFAULT_ADMIN_NAME = "AutoGPT Admin"
DEFAULT_ADMIN_EMAIL = "admin@autogpt.co"
def sanitize_query(query: str | None) -> str | None:
@@ -1241,7 +1254,8 @@ async def review_store_submission(
where={"id": store_listing_version_id},
include={
"StoreListing": True,
"AgentGraph": {"include": AGENT_GRAPH_INCLUDE},
"AgentGraph": {"include": {**AGENT_GRAPH_INCLUDE, "User": True}},
"Reviewer": True,
},
)
)
@@ -1348,6 +1362,89 @@ async def review_store_submission(
f"Failed to update store listing version {store_listing_version_id}"
)
# Send email notification to the agent creator
if store_listing_version.AgentGraph and store_listing_version.AgentGraph.User:
agent_creator = store_listing_version.AgentGraph.User
reviewer = (
store_listing_version.Reviewer
if store_listing_version.Reviewer
else None
)
try:
base_url = (
settings.config.frontend_base_url
or settings.config.platform_base_url
)
if is_approved:
store_agent = (
await prisma.models.StoreAgent.prisma().find_first_or_raise(
where={"storeListingVersionId": submission.id}
)
)
# Send approval notification
notification_data = AgentApprovalData(
agent_name=submission.name,
agent_id=submission.agentGraphId,
agent_version=submission.agentGraphVersion,
reviewer_name=(
reviewer.name
if reviewer and reviewer.name
else DEFAULT_ADMIN_NAME
),
reviewer_email=(
reviewer.email if reviewer else DEFAULT_ADMIN_EMAIL
),
comments=external_comments,
reviewed_at=submission.reviewedAt
or datetime.now(tz=timezone.utc),
store_url=f"{base_url}/marketplace/agent/{store_agent.creator_username}/{store_agent.slug}",
)
notification_event = NotificationEventModel[AgentApprovalData](
user_id=agent_creator.id,
type=prisma.enums.NotificationType.AGENT_APPROVED,
data=notification_data,
)
else:
# Send rejection notification
notification_data = AgentRejectionData(
agent_name=submission.name,
agent_id=submission.agentGraphId,
agent_version=submission.agentGraphVersion,
reviewer_name=(
reviewer.name
if reviewer and reviewer.name
else DEFAULT_ADMIN_NAME
),
reviewer_email=(
reviewer.email if reviewer else DEFAULT_ADMIN_EMAIL
),
comments=external_comments,
reviewed_at=submission.reviewedAt
or datetime.now(tz=timezone.utc),
resubmit_url=f"{base_url}/build?flowID={submission.agentGraphId}",
)
notification_event = NotificationEventModel[AgentRejectionData](
user_id=agent_creator.id,
type=prisma.enums.NotificationType.AGENT_REJECTED,
data=notification_data,
)
# Queue the notification for immediate sending
await queue_notification_async(notification_event)
logger.info(
f"Queued {'approval' if is_approved else 'rejection'} notification for user {agent_creator.id} and agent {submission.name}"
)
except Exception as e:
logger.error(f"Failed to send email notification for agent review: {e}")
# Don't fail the review process if email sending fails
pass
# Convert to Pydantic model for consistency
return backend.server.v2.store.model.StoreSubmission(
agent_id=submission.agentGraphId,

View File

@@ -0,0 +1,14 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "NotificationType" ADD VALUE 'AGENT_APPROVED';
ALTER TYPE "NotificationType" ADD VALUE 'AGENT_REJECTED';
-- AlterTable
ALTER TABLE "User" ADD COLUMN "notifyOnAgentApproved" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "notifyOnAgentRejected" BOOLEAN NOT NULL DEFAULT true;

View File

@@ -33,6 +33,8 @@ model User {
notifyOnDailySummary Boolean @default(true)
notifyOnWeeklySummary Boolean @default(true)
notifyOnMonthlySummary Boolean @default(true)
notifyOnAgentApproved Boolean @default(true)
notifyOnAgentRejected Boolean @default(true)
// Relations
@@ -187,6 +189,8 @@ enum NotificationType {
MONTHLY_SUMMARY
REFUND_REQUEST
REFUND_PROCESSED
AGENT_APPROVED
AGENT_REJECTED
}
model NotificationEvent {

View File

@@ -106,6 +106,60 @@ export function NotificationForm({ preferences, user }: NotificationFormProps) {
/>
</div>
{/* Store Notifications */}
<div className="flex flex-col gap-6">
<Text variant="h4" size="body-medium" className="text-slate-400">
Store Notifications
</Text>
<FormField
control={form.control}
name="notifyOnAgentApproved"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<Text variant="h4" size="body-medium">
Agent Approved
</Text>
<Text variant="body">
Get notified when your submitted agent is approved for the
store
</Text>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="notifyOnAgentRejected"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<Text variant="h4" size="body-medium">
Agent Rejected
</Text>
<Text variant="body">
Receive notifications when your agent submission needs
updates
</Text>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
{/* Balance Notifications */}
<div className="flex flex-col gap-4">
<Text variant="h4" size="body-medium" className="text-slate-400">

View File

@@ -18,6 +18,8 @@ const notificationFormSchema = z.object({
notifyOnDailySummary: z.boolean(),
notifyOnWeeklySummary: z.boolean(),
notifyOnMonthlySummary: z.boolean(),
notifyOnAgentApproved: z.boolean(),
notifyOnAgentRejected: z.boolean(),
});
function createNotificationDefaultValues(preferences: {
@@ -34,6 +36,8 @@ function createNotificationDefaultValues(preferences: {
notifyOnDailySummary: preferences.preferences?.DAILY_SUMMARY,
notifyOnWeeklySummary: preferences.preferences?.WEEKLY_SUMMARY,
notifyOnMonthlySummary: preferences.preferences?.MONTHLY_SUMMARY,
notifyOnAgentApproved: preferences.preferences?.AGENT_APPROVED,
notifyOnAgentRejected: preferences.preferences?.AGENT_REJECTED,
};
}
@@ -80,6 +84,8 @@ export function useNotificationForm({
DAILY_SUMMARY: values.notifyOnDailySummary,
WEEKLY_SUMMARY: values.notifyOnWeeklySummary,
MONTHLY_SUMMARY: values.notifyOnMonthlySummary,
AGENT_APPROVED: values.notifyOnAgentApproved,
AGENT_REJECTED: values.notifyOnAgentRejected,
},
daily_limit: 0,
};

View File

@@ -5496,7 +5496,9 @@
"WEEKLY_SUMMARY",
"MONTHLY_SUMMARY",
"REFUND_REQUEST",
"REFUND_PROCESSED"
"REFUND_PROCESSED",
"AGENT_APPROVED",
"AGENT_REJECTED"
],
"title": "NotificationType"
},

View File

@@ -571,7 +571,9 @@ export type NotificationType =
| "CONTINUOUS_AGENT_ERROR"
| "DAILY_SUMMARY"
| "WEEKLY_SUMMARY"
| "MONTHLY_SUMMARY";
| "MONTHLY_SUMMARY"
| "AGENT_APPROVED"
| "AGENT_REJECTED";
// Mirror of backend/backend/data/notifications.py:NotificationPreference
export type NotificationPreferenceDTO = {