-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add email status change notifications
- Loading branch information
Showing
7 changed files
with
1,936 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
supabase/functions/_email/StatusChangeNotificationEmail.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import { | ||
Body, | ||
Column, | ||
Container, | ||
Head, | ||
Heading, | ||
Html, | ||
Link, | ||
Preview, | ||
Row, | ||
Section, | ||
Text, | ||
} from '@react-email/components' | ||
import { format } from 'date-fns' | ||
import * as React from 'react' | ||
|
||
interface StatusChangeNotificationEmailProps { | ||
domain_name: string | ||
previous_status: string | ||
new_status: string | ||
updated_at: Date | ||
} | ||
|
||
export const StatusChangeNotificationEmail = ({ | ||
domain_name, | ||
previous_status, | ||
new_status, | ||
updated_at, | ||
}: StatusChangeNotificationEmailProps) => { | ||
const previewText = `Status update for ${domain_name}: ${previous_status} → ${new_status}` | ||
|
||
return ( | ||
<Html> | ||
<Head /> | ||
<Preview>{previewText}</Preview> | ||
<Body style={main}> | ||
<Container style={container}> | ||
<Heading style={heading}> | ||
Domain Status Update for {domain_name} | ||
</Heading> | ||
|
||
<Section style={section}> | ||
<Text style={statusText}>Status changed from</Text> | ||
|
||
<Row style={statusRow}> | ||
<Column align="left"> | ||
<Text style={statusPill}>{previous_status}</Text> | ||
</Column> | ||
<Column align="center"> | ||
<Text style={statusTextTo}>to</Text> | ||
</Column> | ||
<Column align="right"> | ||
<Text style={statusPill}>{new_status}</Text> | ||
</Column> | ||
</Row> | ||
|
||
<Text style={timestamp}> | ||
Updated on {format(updated_at, "MMMM do, yyyy 'at' h:mm:ss a")} | ||
</Text> | ||
</Section> | ||
|
||
<Text style={footer}> | ||
This email was sent by{' '} | ||
<Link href="https://side.domains" style={link}> | ||
side.domains | ||
</Link> | ||
. If you don't want to receive status notifications for this domain,{' '} | ||
<Link href="https://app.side.domains" style={link}> | ||
visit the dashboard | ||
</Link> | ||
. | ||
</Text> | ||
</Container> | ||
</Body> | ||
</Html> | ||
) | ||
} | ||
|
||
const main = { | ||
backgroundColor: '#ffffff', | ||
fontFamily: '-apple-system, "Segoe UI", sans-serif', | ||
} | ||
|
||
const container = { | ||
maxWidth: '600px', | ||
margin: '0 auto', | ||
padding: '20px', | ||
} | ||
|
||
const heading = { | ||
color: '#111827', | ||
fontSize: '24px', | ||
fontWeight: '400', | ||
textAlign: 'left', | ||
margin: '16px 0', | ||
} | ||
|
||
const section = { | ||
backgroundColor: '#f3f4f6', | ||
borderRadius: '12px', | ||
padding: '24px', | ||
margin: '24px 0', | ||
} | ||
|
||
const statusRow = { | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: '12px', | ||
} | ||
|
||
const statusText = { | ||
color: '#4b5563', | ||
fontSize: '16px', | ||
margin: '12px 0', | ||
} | ||
|
||
const statusTextTo = { | ||
...statusText, | ||
margin: '12px', | ||
} | ||
|
||
const statusPill = { | ||
backgroundColor: '#6b7280', | ||
color: '#ffffff', | ||
padding: '8px 16px', | ||
borderRadius: '16px', | ||
fontSize: '14px', | ||
fontWeight: '500', | ||
display: 'inline-block', | ||
} | ||
|
||
const timestamp = { | ||
color: '#6b7280', | ||
fontSize: '14px', | ||
margin: '20px 0 0 0', | ||
} | ||
|
||
const footer = { | ||
color: '#6b7280', | ||
fontSize: '14px', | ||
lineHeight: '24px', | ||
margin: '32px 0', | ||
textAlign: 'center', | ||
} | ||
|
||
const link = { | ||
color: '#2563eb', | ||
textDecoration: 'underline', | ||
} | ||
|
||
export default StatusChangeNotificationEmail |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,10 @@ | ||
{ | ||
"imports": { | ||
"@supabase/supabase-js": "jsr:@supabase/supabase-js@^2.45.4" | ||
"@react-email/components": "npm:@react-email/components@^0.0.25", | ||
"@supabase/supabase-js": "jsr:@supabase/supabase-js@^2.45.4", | ||
"date-fns": "npm:date-fns@^4.1.0", | ||
"react": "npm:react@^18.3.1", | ||
"react-dom": "npm:react-dom@^18.3.1", | ||
"resend": "npm:resend@^4.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts' | ||
|
||
import { createClient } from '@supabase/supabase-js' | ||
import { Resend } from 'resend' | ||
import StatusChangeNotificationEmail from '../_email/StatusChangeNotificationEmail.tsx' | ||
|
||
const resend = new Resend(Deno.env.get('RESEND_API_KEY')!) | ||
|
||
Deno.serve(async (req) => { | ||
if (req.method !== 'POST') { | ||
return new Response(null, { status: 405 }) | ||
} | ||
|
||
const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') | ||
const token = req.headers.get('Authorization')?.replace('Bearer ', '') | ||
|
||
if (!serviceRoleKey || !token || token !== serviceRoleKey) { | ||
return new Response(null, { status: 401 }) | ||
} | ||
|
||
const supabaseAdmin = createClient( | ||
Deno.env.get('SUPABASE_URL')!, | ||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! | ||
) | ||
|
||
const { new_status, previous_status, domain_name_id } = await req.json() | ||
|
||
const { data } = await supabaseAdmin | ||
.from('domain_names') | ||
.select() | ||
.eq('id', domain_name_id) | ||
.maybeSingle() | ||
|
||
if (!data) { | ||
return new Response(null, { status: 404 }) | ||
} | ||
|
||
const { domain_name, updated_at } = data | ||
|
||
const { error } = await resend.emails.send({ | ||
from: 'side.domains Notifications <[email protected]>', | ||
to: '[email protected]', | ||
subject: `Status update for ${data.domain_name}: ${previous_status} → ${new_status}`, | ||
react: StatusChangeNotificationEmail({ | ||
domain_name, | ||
previous_status, | ||
new_status, | ||
updated_at, | ||
}), | ||
}) | ||
|
||
if (error) { | ||
console.error('error sending email:', error) | ||
return new Response(null, { status: 500 }) | ||
} | ||
|
||
return new Response(JSON.stringify({ sent: true }), { | ||
headers: { 'Content-Type': 'application/json' }, | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
supabase/migrations/20241026160057_notify_domain_status_change.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
create | ||
or replace function public.notify_domain_status_change () returns trigger as $$ | ||
declare | ||
v_supabase_url text; | ||
v_supabase_service_role_key text; | ||
begin | ||
select decrypted_secret into v_supabase_url | ||
from vault.decrypted_secrets | ||
where name = 'supabase-url'; | ||
|
||
select decrypted_secret into v_supabase_service_role_key | ||
from vault.decrypted_secrets | ||
where name = 'supabase-service-role-key'; | ||
|
||
perform net.http_post( | ||
url := concat(v_supabase_url, '/functions/v1/notify-domain-status-change'), | ||
headers := jsonb_build_object( | ||
'Content-Type','application/json', | ||
'Authorization', concat('Bearer ', v_supabase_service_role_key) | ||
), | ||
body := jsonb_build_object( | ||
'domain_name_id', new.id, | ||
'previous_status', old.status, | ||
'new_status', new.status | ||
) | ||
); | ||
|
||
return new; | ||
end; | ||
$$ language plpgsql volatile; | ||
|
||
create trigger notify_domain_status_change | ||
after | ||
update on public.domain_names for each row when ( | ||
new.status_change_notifications_enabled = true | ||
and old.status <> 'unknown'::domain_name_status | ||
and old.status is distinct | ||
from | ||
new.status | ||
) | ||
execute procedure public.notify_domain_status_change (); |