/**
* MatchNotificationModal - Premium celebration modal for new matches
*
* Features:
* - Glass-morphism styling following boutique design aesthetic
* - Animated floating hearts celebration effect
* - Shows what each partner answered (You / Partner)
* - Uses brand gradients and premium styling
*/
import React, { useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
Pressable,
Animated,
Keyboard,
Platform,
} from 'react-native';
import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur';
import { Ionicons } from '@expo/vector-icons';
import { colors, gradients, spacing, radius, typography, shadows } from '../theme';
import { DecorativeSeparator } from './ui';
import type { MatchType, AnswerType } from '../types';
export interface MatchNotificationData {
id: string;
match_type: MatchType;
question: {
id: string;
text: string;
partner_text?: string | null;
};
userResponse?: AnswerType;
partnerResponse?: AnswerType;
partnerName?: string | null;
}
interface Props {
visible: boolean;
match: MatchNotificationData | null;
onDismiss: () => void;
onChat: () => void;
}
// Helper to get response styling
const getResponseStyle = (response: AnswerType, isPartner: boolean = false) => {
switch (response) {
case 'yes':
return {
bg: isPartner ? 'rgba(46, 204, 113, 0.15)' : 'rgba(233, 69, 96, 0.15)',
border: isPartner ? 'rgba(46, 204, 113, 0.3)' : 'rgba(233, 69, 96, 0.3)',
color: isPartner ? colors.success : colors.primary,
text: 'YES',
icon: 'heart' as const,
};
case 'maybe':
return {
bg: 'rgba(243, 156, 18, 0.15)',
border: 'rgba(243, 156, 18, 0.3)',
color: colors.warning,
text: 'MAYBE',
icon: 'help-circle' as const,
};
default:
return {
bg: 'rgba(255, 255, 255, 0.1)',
border: 'rgba(255, 255, 255, 0.2)',
color: colors.textSecondary,
text: response.toUpperCase(),
icon: 'ellipse' as const,
};
}
};
// Compact response badge (for two-part questions)
const ResponseBadge: React.FC<{
response: AnswerType;
isPartner?: boolean;
}> = ({ response, isPartner = false }) => {
const style = getResponseStyle(response, isPartner);
return (
{style.text}
);
};
export const MatchNotificationModal: React.FC = ({
visible,
match,
onDismiss,
onChat,
}) => {
const modalScale = useRef(new Animated.Value(0.8)).current;
const modalOpacity = useRef(new Animated.Value(0)).current;
const overlayOpacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (visible) {
// Dismiss keyboard so it doesn't block the modal buttons
Keyboard.dismiss();
// Trigger celebration haptic pattern
if (Platform.OS !== 'web') {
// Success notification followed by a heavy impact for extra punch
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setTimeout(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
}, 150);
}
// Reset animations
modalScale.setValue(0.8);
modalOpacity.setValue(0);
overlayOpacity.setValue(0);
// Animate in
Animated.parallel([
Animated.timing(overlayOpacity, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.spring(modalScale, {
toValue: 1,
tension: 80,
friction: 10,
useNativeDriver: true,
}),
Animated.timing(modalOpacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
]).start();
}
}, [visible]);
const handleDismiss = () => {
Animated.parallel([
Animated.timing(overlayOpacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(modalScale, {
toValue: 0.8,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(modalOpacity, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}),
]).start(() => onDismiss());
};
const handleChat = () => {
Animated.parallel([
Animated.timing(overlayOpacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(modalScale, {
toValue: 0.8,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(modalOpacity, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}),
]).start(() => onChat());
};
if (!visible || !match || !match.question) return null;
// Determine responses based on match_type if not explicitly provided
const getUserResponse = (): AnswerType => {
if (match.userResponse) return match.userResponse;
// Infer from match_type (user typically triggers the match)
if (match.match_type === 'yes_yes') return 'yes';
if (match.match_type === 'maybe_maybe') return 'maybe';
// yes_maybe - user could be either
return 'yes';
};
const getPartnerResponse = (): AnswerType => {
if (match.partnerResponse) return match.partnerResponse;
// Infer from match_type
if (match.match_type === 'yes_yes') return 'yes';
if (match.match_type === 'maybe_maybe') return 'maybe';
// yes_maybe - partner could be either
return 'maybe';
};
const userResponse = getUserResponse();
const partnerResponse = getPartnerResponse();
// Get match type styling
const getMatchTypeLabel = () => {
switch (match.match_type) {
case 'yes_yes':
return { text: 'Perfect Match', color: colors.primary };
case 'yes_maybe':
return { text: 'Curious Match', color: colors.warning };
case 'maybe_maybe':
return { text: 'Exploring Together', color: colors.premium.rose };
default:
return { text: 'Match', color: colors.primary };
}
};
const matchTypeInfo = getMatchTypeLabel();
// Use partner's name or fallback to "Partner"
const partnerDisplayName = match.partnerName || 'Partner';
return (
{/* Background press to dismiss */}
{/* Modal card with glow */}
{/* Outer glow effect */}
{/* Glass background */}
{/* Content */}
{/* Title section */}
It's a Match!
{/* Match type badge */}
{matchTypeInfo.text}
{/* Question text and responses */}
{match.question.partner_text ? (
// Two-part question - show what each person saw
// The current user triggered the match (answered second), so they saw partner_text
// The partner answered first, so they saw text
You said
"{match.question.partner_text}"
{partnerDisplayName} said
"{match.question.text}"
) : (
// Single question - show shared text with inline responses
"{match.question.text}"
You said
{userResponse.toUpperCase()}
ยท {partnerDisplayName} said
{partnerResponse.toUpperCase()}
)}
{/* Action buttons */}
Chat
Continue
{/* Border */}
);
};
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.75)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
},
glowContainer: {
width: '88%',
maxWidth: 360,
alignItems: 'center',
justifyContent: 'center',
},
glowOuter: {
position: 'absolute',
top: -3,
left: -3,
right: -3,
bottom: -3,
borderRadius: radius.xl + 3,
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: 'rgba(233, 69, 96, 0.15)',
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.2,
shadowRadius: 20,
elevation: 8,
},
modalContainer: {
width: '100%',
borderRadius: radius.xl,
overflow: 'hidden',
},
blurContainer: {
...StyleSheet.absoluteFillObject,
},
gradientOverlay: {
...StyleSheet.absoluteFillObject,
},
content: {
padding: spacing.xl,
paddingTop: spacing.lg,
alignItems: 'center',
},
title: {
...typography.title2,
color: colors.text,
textAlign: 'center',
marginBottom: spacing.sm,
},
matchTypeBadge: {
backgroundColor: 'rgba(255, 255, 255, 0.05)',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm,
borderRadius: radius.full,
borderWidth: 1,
marginBottom: spacing.lg,
},
matchTypeText: {
...typography.caption1,
fontWeight: '600',
letterSpacing: 1,
},
questionContainer: {
alignItems: 'center',
},
questionText: {
...typography.body,
color: colors.text,
textAlign: 'center',
fontStyle: 'italic',
lineHeight: 24,
paddingHorizontal: spacing.sm,
marginBottom: spacing.md,
},
responsesInline: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
},
responseInlineText: {
...typography.caption1,
color: colors.textSecondary,
},
responseInlineAnswer: {
...typography.caption1,
fontWeight: '700',
},
responseBadgeCompact: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: radius.md,
borderWidth: 1,
},
responseBadgeTextCompact: {
...typography.caption1,
fontWeight: '600',
letterSpacing: 0.5,
},
// Two-part question styles
twoPartContainer: {
width: '100%',
marginBottom: spacing.md,
},
responseSection: {
alignItems: 'center',
paddingHorizontal: spacing.sm,
},
responseLabelRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.xs,
},
responsePersonLabel: {
...typography.caption1,
color: colors.textSecondary,
fontWeight: '500',
},
questionTextSmall: {
...typography.subhead,
color: colors.text,
textAlign: 'center',
fontStyle: 'italic',
lineHeight: 20,
},
matchConnector: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: spacing.md,
paddingHorizontal: spacing.xl,
},
connectorLine: {
flex: 1,
height: 1,
backgroundColor: 'rgba(233, 69, 96, 0.2)',
},
connectorHeart: {
marginHorizontal: spacing.sm,
opacity: 0.8,
},
buttonRow: {
flexDirection: 'row',
gap: spacing.md,
width: '100%',
},
chatButton: {
flex: 1,
borderRadius: radius.lg,
overflow: 'hidden',
...shadows.md,
},
chatButtonGradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: spacing.sm,
paddingVertical: spacing.md,
},
chatButtonText: {
...typography.headline,
color: colors.text,
},
dismissButton: {
flex: 1,
borderRadius: radius.lg,
backgroundColor: colors.glass.background,
borderWidth: 1,
borderColor: colors.glass.border,
paddingVertical: spacing.md,
alignItems: 'center',
justifyContent: 'center',
},
dismissButtonText: {
...typography.headline,
color: colors.textSecondary,
},
border: {
...StyleSheet.absoluteFillObject,
borderRadius: radius.xl,
borderWidth: 1,
borderColor: 'rgba(233, 69, 96, 0.2)',
},
});