mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
feat: add catchup and unpaired reminder notifications
Add automatic push notifications for behind partners (escalating: 24h then every 3 days) and daily unpaired user reminders. Includes edge functions, migrations, notification preference toggles, and UI improvements to pairing and swipe screens.
This commit is contained in:
1 parent
8a44fef764
commit
c8aaa8967d
12 files changed
+1117
-266
No files matched your search
@@ -0,0 +1,164 @@
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0?target=deno";
|
||||
|
||||
/**
|
||||
* Send push notification reminders to unpaired users.
|
||||
* Called by pg_cron daily at 18:00 UTC.
|
||||
*
|
||||
* Targets users where:
|
||||
* - couple_id IS NULL (no partner yet)
|
||||
* - push_token IS NOT NULL (can receive notifications)
|
||||
* - last_unpaired_reminder_at IS NULL or older than 3 days
|
||||
* - unpaired_reminders_enabled is not false in notification_preferences
|
||||
*/
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
const REMINDER_COOLDOWN_DAYS = 3;
|
||||
|
||||
interface ExpoPushMessage {
|
||||
to: string;
|
||||
title: string;
|
||||
body: string;
|
||||
sound?: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function sendExpoPushNotification(message: ExpoPushMessage) {
|
||||
const response = await fetch("https://exp.host/--/api/v2/push/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
console.error("Expo push error:", error);
|
||||
throw new Error(`Failed to send push notification: ${error}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// Handle CORS preflight
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response("ok", { headers: corsHeaders });
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return new Response("Method not allowed", { status: 405, headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get("SUPABASE_URL")!,
|
||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
const cutoff = new Date(now.getTime() - REMINDER_COOLDOWN_DAYS * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Get unpaired users with push tokens who haven't been reminded recently
|
||||
const { data: users, error: usersError } = await supabase
|
||||
.from("profiles")
|
||||
.select("id, push_token, couple_id, last_unpaired_reminder_at")
|
||||
.is("couple_id", null)
|
||||
.not("push_token", "is", null)
|
||||
.or(`last_unpaired_reminder_at.is.null,last_unpaired_reminder_at.lt.${cutoff.toISOString()}`);
|
||||
|
||||
if (usersError) {
|
||||
console.error("Error fetching unpaired users:", usersError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to fetch users" }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, sent: 0, message: "No eligible users" }),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Get notification preferences for these users to check opt-out
|
||||
const userIds = users.map((u) => u.id);
|
||||
const { data: prefs } = await supabase
|
||||
.from("notification_preferences")
|
||||
.select("user_id, unpaired_reminders_enabled")
|
||||
.in("user_id", userIds);
|
||||
|
||||
const disabledUsers = new Set(
|
||||
(prefs || [])
|
||||
.filter((p) => p.unpaired_reminders_enabled === false)
|
||||
.map((p) => p.user_id)
|
||||
);
|
||||
|
||||
// Check which users have a couple (invite code created but no partner yet)
|
||||
const { data: couples } = await supabase
|
||||
.from("couples")
|
||||
.select("id, user1_id")
|
||||
.in("user1_id", userIds);
|
||||
|
||||
const usersWithCouple = new Set(
|
||||
(couples || []).map((c) => c.user1_id)
|
||||
);
|
||||
|
||||
let sent = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const user of users) {
|
||||
// Skip if user opted out
|
||||
if (disabledUsers.has(user.id)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasCouple = usersWithCouple.has(user.id);
|
||||
const title = "Sauci";
|
||||
const body = hasCouple
|
||||
? "Your invite code is waiting! Share it with your partner to start discovering what you have in common."
|
||||
: "Ready to get started? Create an invite code and share it with your partner!";
|
||||
|
||||
try {
|
||||
await sendExpoPushNotification({
|
||||
to: user.push_token,
|
||||
title,
|
||||
body,
|
||||
sound: "default",
|
||||
data: { type: "unpaired_reminder" },
|
||||
});
|
||||
|
||||
// Update last_unpaired_reminder_at
|
||||
await supabase
|
||||
.from("profiles")
|
||||
.update({ last_unpaired_reminder_at: now.toISOString() })
|
||||
.eq("id", user.id);
|
||||
|
||||
sent++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to send reminder to ${user.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Unpaired reminders: sent=${sent}, skipped=${skipped}, total=${users.length}`);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, sent, skipped, total: users.length }),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Unpaired reminder error:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Internal server error" }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user