From 994dd2a295a2099585bc3b286769c5a4c2352a49 Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sat, 3 Jan 2026 18:58:57 +0000 Subject: [PATCH] Add lifecycle push notifications and refine UI Send push notifications when partner disconnects, resets progress, or deletes relationship. Update Matches screen with new styling and remove legacy premium elements. Remove message content preview from admin User Activity page. --- .../src/pages/activity/UserActivityPage.tsx | 22 +-- apps/mobile/app/(app)/matches.tsx | 125 +++++------------- .../functions/delete-relationship/index.ts | 47 +++++++ .../supabase/functions/manage-couple/index.ts | 56 ++++++-- .../functions/reset-couple-progress/index.ts | 47 +++++++ 5 files changed, 174 insertions(+), 123 deletions(-) diff --git a/apps/admin/src/pages/activity/UserActivityPage.tsx b/apps/admin/src/pages/activity/UserActivityPage.tsx index 4013529..fd5d612 100644 --- a/apps/admin/src/pages/activity/UserActivityPage.tsx +++ b/apps/admin/src/pages/activity/UserActivityPage.tsx @@ -30,8 +30,6 @@ import { Heart, MessageCircle, UserPlus, - Image as ImageIcon, - Video as VideoIcon, ExternalLink, } from 'lucide-react'; @@ -546,7 +544,6 @@ export function UserActivityPage() { User - Message Context Time Actions @@ -555,13 +552,13 @@ export function UserActivityPage() { {loading.messages ? ( - +
) : messages.length === 0 ? ( - + No messages yet. @@ -569,21 +566,6 @@ export function UserActivityPage() { messages.map((item) => ( {renderUserLink(item.profile)} - - {item.media_path ? ( - - {item.media_type === 'video' ? ( - <> [Video] - ) : ( - <> [Image] - )} - - ) : ( - - {item.content || Empty message} - - )} - {item.match?.question?.text || 'Unknown match'} diff --git a/apps/mobile/app/(app)/matches.tsx b/apps/mobile/app/(app)/matches.tsx index 93b0f09..5953fb2 100755 --- a/apps/mobile/app/(app)/matches.tsx +++ b/apps/mobile/app/(app)/matches.tsx @@ -11,8 +11,8 @@ import Animated, { FadeInRight, FadeInUp, } from "react-native-reanimated"; -import { GradientBackground, GlassCard, GlassButton, DecorativeSeparator } from "../../src/components/ui"; -import { useAmbientOrbAnimation } from "../../src/hooks"; +import { GradientBackground, GlassCard, GlassButton, DecorativeSeparator } from "../../src/components/ui"; +import { useAmbientOrbAnimation } from "../../src/hooks"; import { colors, gradients, spacing, typography, radius, shadows } from "../../src/theme"; import { MatchesTutorial } from "../../src/components/tutorials"; import { hasSeenMatchesTutorial, markMatchesTutorialSeen } from "../../src/lib/matchesTutorialSeen"; @@ -29,11 +29,11 @@ export default function MatchesScreen() { const { matches, fetchMatches, markAllAsSeen } = useMatchStore(); const { user } = useAuthStore(); const router = useRouter(); - const { width } = useWindowDimensions(); - const isWideScreen = width > MAX_CONTENT_WIDTH; - const [showTutorial, setShowTutorial] = useState(false); - - // Ambient orb breathing animations + const { width } = useWindowDimensions(); + const isWideScreen = width > MAX_CONTENT_WIDTH; + const [showTutorial, setShowTutorial] = useState(false); + + // Ambient orb breathing animations const { orbStyle1, orbStyle2 } = useAmbientOrbAnimation(); // Check if tutorial should be shown when screen is focused or matches change @@ -101,15 +101,15 @@ export default function MatchesScreen() { start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} /> - {/* Top silk highlight */} - - - + {/* Top silk highlight */} + + + {/* Premium icon container */} - {/* Premium icon */} - - - - - {/* Title section */} - BEGIN - No Matches Yet - - {/* Decorative separator */} - - - {/* Status badge */} - - START EXPLORING - - {/* Description */} Answer questions together to discover what you both enjoy. Matches appear when you agree! @@ -268,15 +248,15 @@ export default function MatchesScreen() { {/* Feature hints */} - + Swipe right for yes - + Match when you both agree - + Chat about your matches @@ -295,13 +275,13 @@ export default function MatchesScreen() { /> - {/* Matches Tutorial Overlay */} - {showTutorial && ( - - )} - - ); -} + {/* Matches Tutorial Overlay */} + {showTutorial && ( + + )} + + ); +} const styles = StyleSheet.create({ container: { @@ -387,13 +367,13 @@ const styles = StyleSheet.create({ maxWidth: MAX_CONTENT_WIDTH, }, // Premium Match Card - matchCardPremium: { - marginBottom: spacing.sm, - borderRadius: radius.lg, - overflow: 'hidden', - padding: spacing.md, - }, - cardSilkHighlight: { + matchCardPremium: { + marginBottom: spacing.sm, + borderRadius: radius.lg, + overflow: 'hidden', + padding: spacing.md, + }, + cardSilkHighlight: { position: 'absolute', top: 0, left: 0, @@ -507,45 +487,6 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.md, paddingVertical: spacing.xl, }, - emptyIconContainerPremium: { - width: 72, - height: 72, - borderRadius: 36, - backgroundColor: `${ROSE_RGBA}0.1)`, - borderWidth: 1, - borderColor: `${ROSE_RGBA}0.2)`, - justifyContent: 'center', - alignItems: 'center', - marginBottom: spacing.xl, - }, - emptyLabel: { - ...typography.caption1, - fontWeight: '600', - letterSpacing: 3, - color: ROSE, - textAlign: 'center', - marginBottom: spacing.xs, - }, - emptyTitlePremium: { - ...typography.largeTitle, - color: colors.text, - textAlign: 'center', - }, - emptyBadge: { - backgroundColor: `${ROSE_RGBA}0.1)`, - paddingHorizontal: spacing.lg, - paddingVertical: spacing.sm, - borderRadius: radius.full, - borderWidth: 1, - borderColor: `${ROSE_RGBA}0.2)`, - marginBottom: spacing.xl, - }, - emptyBadgeText: { - ...typography.caption2, - fontWeight: '600', - letterSpacing: 2, - color: ROSE, - }, emptyDescription: { ...typography.body, color: colors.textSecondary, diff --git a/apps/supabase/functions/delete-relationship/index.ts b/apps/supabase/functions/delete-relationship/index.ts index 853ebdf..b236756 100644 --- a/apps/supabase/functions/delete-relationship/index.ts +++ b/apps/supabase/functions/delete-relationship/index.ts @@ -6,6 +6,34 @@ const corsHeaders = { "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; +interface ExpoPushMessage { + to: string; + title: string; + body: string; + sound?: string; + data?: Record; +} + +async function sendExpoPushNotifications(messages: ExpoPushMessage[]) { + if (messages.length === 0) return; + + const response = await fetch("https://exp.host/--/api/v2/push/send", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(messages), + }); + + if (!response.ok) { + const error = await response.text(); + console.error("Expo push error:", error); + } + + return response.json(); +} + Deno.serve(async (req) => { // Handle CORS preflight if (req.method === "OPTIONS") { @@ -62,6 +90,14 @@ Deno.serve(async (req) => { const coupleId = profile.couple_id; + // Get partner's push token BEFORE clearing data + const { data: partnerProfile } = await supabase + .from("profiles") + .select("push_token") + .eq("couple_id", coupleId) + .neq("id", user.id) + .maybeSingle(); + // Get all match IDs for this couple (needed for storage cleanup) const { data: matches } = await supabase .from("matches") @@ -112,6 +148,17 @@ Deno.serve(async (req) => { ); } + // Notify partner about the deletion + if (partnerProfile?.push_token) { + await sendExpoPushNotifications([{ + to: partnerProfile.push_token, + title: "Relationship Deleted", + body: "Your partner has deleted all shared data. You can start fresh with a new partner anytime.", + sound: "default", + data: { type: "relationship_deleted" }, + }]); + } + return new Response( JSON.stringify({ success: true, message: "Relationship data deleted successfully" }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } diff --git a/apps/supabase/functions/manage-couple/index.ts b/apps/supabase/functions/manage-couple/index.ts index 78ac358..dd87c5a 100755 --- a/apps/supabase/functions/manage-couple/index.ts +++ b/apps/supabase/functions/manage-couple/index.ts @@ -10,6 +10,34 @@ interface JoinCoupleBody { invite_code: string; } +interface ExpoPushMessage { + to: string; + title: string; + body: string; + sound?: string; + data?: Record; +} + +async function sendExpoPushNotifications(messages: ExpoPushMessage[]) { + if (messages.length === 0) return; + + const response = await fetch("https://exp.host/--/api/v2/push/send", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(messages), + }); + + if (!response.ok) { + const error = await response.text(); + console.error("Expo push error:", error); + } + + return response.json(); +} + Deno.serve(async (req) => { // Handle CORS preflight if (req.method === "OPTIONS") { @@ -183,18 +211,13 @@ Deno.serve(async (req) => { const coupleId = profile.couple_id; - // Count members in this couple - const { count, error: countError } = await supabase + // Get partner's push token BEFORE clearing couple_id + const { data: partnerProfile } = await supabase .from("profiles") - .select("*", { count: "exact", head: true }) - .eq("couple_id", coupleId); - - if (countError) { - return new Response( - JSON.stringify({ error: "Failed to check couple members" }), - { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } - ); - } + .select("push_token") + .eq("couple_id", coupleId) + .neq("id", user.id) + .maybeSingle(); // Remove ALL members from couple (both partners get unlinked) // This ensures the remaining partner isn't left in a "waiting" state @@ -221,6 +244,17 @@ Deno.serve(async (req) => { // Don't fail the request, users already unlinked } + // Notify partner about the unpair + if (partnerProfile?.push_token) { + await sendExpoPushNotifications([{ + to: partnerProfile.push_token, + title: "Partner Disconnected", + body: "Your partner has ended the connection. You can invite a new partner anytime.", + sound: "default", + data: { type: "unpair" }, + }]); + } + return new Response( JSON.stringify({ success: true, couple_deleted: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } diff --git a/apps/supabase/functions/reset-couple-progress/index.ts b/apps/supabase/functions/reset-couple-progress/index.ts index e961526..53dba63 100644 --- a/apps/supabase/functions/reset-couple-progress/index.ts +++ b/apps/supabase/functions/reset-couple-progress/index.ts @@ -6,6 +6,34 @@ const corsHeaders = { "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; +interface ExpoPushMessage { + to: string; + title: string; + body: string; + sound?: string; + data?: Record; +} + +async function sendExpoPushNotifications(messages: ExpoPushMessage[]) { + if (messages.length === 0) return; + + const response = await fetch("https://exp.host/--/api/v2/push/send", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(messages), + }); + + if (!response.ok) { + const error = await response.text(); + console.error("Expo push error:", error); + } + + return response.json(); +} + Deno.serve(async (req) => { // Handle CORS preflight if (req.method === "OPTIONS") { @@ -62,6 +90,14 @@ Deno.serve(async (req) => { const coupleId = profile.couple_id; + // Get partner's push token BEFORE clearing data + const { data: partnerProfile } = await supabase + .from("profiles") + .select("push_token") + .eq("couple_id", coupleId) + .neq("id", user.id) + .maybeSingle(); + // Get all match IDs for this couple (needed for storage cleanup) const { data: matches } = await supabase .from("matches") @@ -112,6 +148,17 @@ Deno.serve(async (req) => { ); } + // Notify partner about the progress reset + if (partnerProfile?.push_token) { + await sendExpoPushNotifications([{ + to: partnerProfile.push_token, + title: "Progress Reset", + body: "Your partner has reset all progress. Your matches and chats have been cleared to start fresh.", + sound: "default", + data: { type: "progress_reset" }, + }]); + } + return new Response( JSON.stringify({ success: true, message: "Progress reset successfully" }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }