This commit is contained in:
jjbrunton committed 2025-12-30 17:05:08 +00:00
1 parent 5a7e62a578
commit 09b7646787
14 files changed
+1421 -732

No files matched your search

+43
View File
@@ -96,9 +96,52 @@ Edge functions in `apps/supabase/functions/` handle complex operations:
- `revenuecat-webhook` - Subscription webhook handler - `revenuecat-webhook` - Subscription webhook handler
- `sync-subscription` - Syncs RevenueCat subscription status - `sync-subscription` - Syncs RevenueCat subscription status
#### Edge Function Deployment Guidelines
When deploying edge functions via MCP tools (`mcp__sauci-*__deploy_edge_function`):
**IMPORTANT: Always use `verify_jwt: false`** for all edge functions. Supabase's built-in JWT verification causes 401 errors even with valid tokens. Instead, handle authentication manually in the function code using:
```typescript
const { data: { user }, error: authError } = await supabase.auth.getUser(
authHeader.replace("Bearer ", "")
);
```
This approach:
- Gives better error messages
- Allows custom auth handling
- Avoids mysterious 401 failures
**Current function settings (all should be verify_jwt: false):**
| Function | verify_jwt | Auth Method |
|----------|------------|-------------|
| `manage-couple` | false | Manual via getUser() |
| `submit-response` | false | Manual via getUser() |
| `sync-subscription` | false | Manual via getUser() |
| `delete-relationship` | false | Manual via getUser() |
| `revenuecat-webhook` | false | Webhook signature validation |
| `send-notification` | false | Internal trigger (no auth) |
| `send-message-notification` | false | Internal trigger (no auth) |
### Database Best Practices
- **Always use `.maybeSingle()` instead of `.single()`** when the row might not exist. `.single()` throws an error if 0 or >1 rows are returned.
- **Profile creation trigger**: The `on_auth_user_created` trigger automatically creates a profile when a user signs up. Never assume a profile exists without checking.
- **Check for null profiles** in edge functions before performing operations.
### Key Patterns ### Key Patterns
- Responses use UPSERT with `onConflict: "user_id,question_id"` - Responses use UPSERT with `onConflict: "user_id,question_id"`
- Match detection happens in `submit-response` edge function after each response - Match detection happens in `submit-response` edge function after each response
- Real-time subscriptions used for chat messages - Real-time subscriptions used for chat messages
- RevenueCat handles in-app purchases and subscription management - RevenueCat handles in-app purchases and subscription management
- RLS policies enforce couple-level data isolation - RLS policies enforce couple-level data isolation
## Mobile UI - DO NOT CHANGE
### Tab Bar Background
**NEVER set `backgroundColor: 'transparent'` on the tab bar.** Always use a semi-transparent solid color:
```typescript
backgroundColor: 'rgba(18, 18, 18, 0.85)',
```
The `BlurView` in `tabBarBackground` is unreliable and can fail during re-renders/transitions, causing a fully transparent tab bar. The solid rgba background acts as a fallback while still allowing the blur effect on top.
**Location:** `apps/mobile/app/(app)/_layout.tsx` - `tabBarStyle`
+43 -4
View File
@@ -19,13 +19,21 @@ function TabBarBackground() {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
return ( return (
<BlurView <BlurView
intensity={80} intensity={100}
tint="systemChromeMaterialDark" tint="dark"
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
/> />
); );
} }
return <View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(22, 33, 62, 0.95)' }]} />; // Android fallback - use experimental blur method
return (
<BlurView
intensity={80}
tint="dark"
style={StyleSheet.absoluteFill}
experimentalBlurMethod="dimezisBlurView"
/>
);
} }
interface PlayTabButtonProps { interface PlayTabButtonProps {
@@ -191,6 +199,37 @@ export default function AppLayout() {
} }
}, [user?.id, user?.onboarding_completed]); }, [user?.id, user?.onboarding_completed]);
// Subscribe to realtime changes on the current user's profile
// This detects when partner disconnects (sets our couple_id to NULL)
useEffect(() => {
if (!user?.id) return;
const channel = supabase
.channel(`profile:${user.id}`)
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "profiles",
filter: `id=eq.${user.id}`,
},
async (payload) => {
const newProfile = payload.new as { couple_id: string | null };
// If couple_id changed (especially to NULL when partner disconnects)
if (newProfile.couple_id !== user.couple_id) {
console.log("Profile couple_id changed, refreshing user data...");
await useAuthStore.getState().fetchUser();
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [user?.id, user?.couple_id]);
// Subscribe to realtime match notifications // Subscribe to realtime match notifications
useEffect(() => { useEffect(() => {
if (!user?.couple_id) return; if (!user?.couple_id) return;
@@ -373,7 +412,7 @@ export default function AppLayout() {
tabBarBackground: () => <TabBarBackground />, tabBarBackground: () => <TabBarBackground />,
tabBarStyle: shouldHideTabBar ? { display: 'none' } : { tabBarStyle: shouldHideTabBar ? { display: 'none' } : {
position: 'absolute', position: 'absolute',
backgroundColor: 'transparent', backgroundColor: 'rgba(18, 18, 18, 0.85)',
borderTopColor: colors.glass.border, borderTopColor: colors.glass.border,
borderTopWidth: 1, borderTopWidth: 1,
paddingTop: 8, paddingTop: 8,
+11
View File
@@ -94,7 +94,9 @@ export default function OnboardingScreen() {
}; };
const handleComplete = async () => { const handleComplete = async () => {
console.log('[Onboarding] handleComplete called, user:', user?.id, 'name:', name);
if (!user?.id) { if (!user?.id) {
console.error('[Onboarding] No user ID available');
setError('Not logged in. Please restart the app.'); setError('Not logged in. Please restart the app.');
return; return;
} }
@@ -115,10 +117,13 @@ export default function OnboardingScreen() {
.eq('id', user.id); .eq('id', user.id);
if (updateError) { if (updateError) {
console.error('[Onboarding] Profile update error:', updateError);
throw updateError; throw updateError;
} }
console.log('[Onboarding] Profile updated successfully, fetching user...');
await fetchUser(); await fetchUser();
console.log('[Onboarding] User fetched, navigating to home...');
router.replace('/'); router.replace('/');
} catch (err: any) { } catch (err: any) {
console.error('Onboarding error:', err); console.error('Onboarding error:', err);
@@ -401,6 +406,12 @@ export default function OnboardingScreen() {
</Text> </Text>
</View> </View>
</View> </View>
{error && (
<View style={styles.errorContainer}>
<Ionicons name="alert-circle" size={16} color={colors.error} />
<Text style={styles.errorText}>{error}</Text>
</View>
)}
</GlassCard> </GlassCard>
<View style={styles.footer}> <View style={styles.footer}>
+323 -211
View File
@@ -3,22 +3,26 @@ import {
View, View,
Text, Text,
TextInput, TextInput,
TouchableOpacity,
StyleSheet, StyleSheet,
Alert, Alert,
ActivityIndicator, ActivityIndicator,
Share, Share,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
TouchableOpacity,
} from "react-native"; } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import Animated, { FadeInDown } from "react-native-reanimated";
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
import { useAuthStore } from "../../src/store"; import { useAuthStore } from "../../src/store";
import { supabase } from "../../src/lib/supabase"; import { supabase } from "../../src/lib/supabase";
import { router } from "expo-router"; import { router } from "expo-router";
import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme";
export default function PairingScreen() { export default function PairingScreen() {
const { user, fetchCouple, fetchUser, couple, partner, isLoading: isAuthLoading, signOut } = useAuthStore(); const { user, fetchCouple, fetchUser, couple, partner, isLoading: isAuthLoading } = useAuthStore();
const [inviteCode, setInviteCode] = useState(""); const [inviteCode, setInviteCode] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -105,13 +109,17 @@ export default function PairingScreen() {
body: { invite_code: sanitizedCode }, body: { invite_code: sanitizedCode },
}); });
if (error) throw error; if (error) {
if (data.error) throw new Error(data.error); // Extract user-friendly error message
const errorMessage = data?.error || "Unable to join couple. Please try again.";
throw new Error(errorMessage);
}
if (data?.error) throw new Error(data.error);
await fetchUser(); // Refresh user to get couple_id await fetchUser(); // Refresh user to get couple_id
await fetchCouple(); // Fetch couple data await fetchCouple(); // Fetch couple data
Alert.alert("Success", "You are now paired! 💕", [ Alert.alert("Success", "You are now paired!", [
{ text: "Let's Go", onPress: () => router.replace("/(app)/") } { text: "Let's Go", onPress: () => router.replace("/(app)/") }
]); ]);
} catch (error: any) { } catch (error: any) {
@@ -140,16 +148,33 @@ export default function PairingScreen() {
} }
}; };
const handleSignOut = () => { const handleCancelPairing = async () => {
Alert.alert( Alert.alert(
"Sign Out", "Cancel Pairing",
"Are you sure you want to sign out?", "Are you sure you want to cancel? Your invite code will be deleted.",
[ [
{ text: "Cancel", style: "cancel" }, { text: "Keep Waiting", style: "cancel" },
{ {
text: "Sign Out", text: "Cancel Pairing",
style: "destructive", style: "destructive",
onPress: signOut, onPress: async () => {
setIsSubmitting(true);
try {
const { data, error } = await supabase.functions.invoke("manage-couple", {
method: "DELETE",
});
if (error) throw error;
if (data?.error) throw new Error(data.error);
await fetchUser();
await fetchCouple();
} catch (error: any) {
Alert.alert("Error", error.message);
} finally {
setIsSubmitting(false);
}
},
}, },
] ]
); );
@@ -157,273 +182,360 @@ export default function PairingScreen() {
if (isAuthLoading) { if (isAuthLoading) {
return ( return (
<View style={[styles.container, styles.centerContent]}> <GradientBackground>
<ActivityIndicator size="large" color="#e94560" /> <View style={styles.loadingContainer}>
</View> <ActivityIndicator size="large" color={colors.primary} />
</View>
</GradientBackground>
); );
} }
// If user has a couple but no partner, they are waiting // If user has a couple but no partner, they are waiting
if (couple && !partner) { if (couple && !partner) {
return ( return (
<View style={styles.container}> <GradientBackground>
<TouchableOpacity <View style={styles.container}>
style={styles.backButton} {/* Header */}
onPress={() => router.back()} <Animated.View
> entering={FadeInDown.delay(100).duration(500)}
<Ionicons name="arrow-back" size={24} color="#fff" /> style={styles.header}
</TouchableOpacity> >
<View style={styles.content}> <TouchableOpacity
<View style={styles.iconContainer}> style={styles.backButton}
<Ionicons name="heart" size={48} color="#e94560" /> onPress={() => router.back()}
activeOpacity={0.7}
>
<Ionicons name="arrow-back" size={24} color={colors.text} />
</TouchableOpacity>
<Text style={styles.title}>Partner Code</Text>
</Animated.View>
<View style={styles.content}>
{/* Icon */}
<Animated.View
entering={FadeInDown.delay(200).duration(500)}
style={styles.iconSection}
>
<LinearGradient
colors={gradients.primary as [string, string]}
style={styles.iconGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name="heart" size={40} color={colors.text} />
</LinearGradient>
<Text style={styles.subtitle}>
Share this code with your partner to link your accounts
</Text>
</Animated.View>
{/* Code Card */}
<Animated.View
entering={FadeInDown.delay(300).duration(500)}
style={styles.section}
>
<GlassCard variant="elevated">
<TouchableOpacity
style={styles.codeContainer}
onPress={copyToClipboard}
activeOpacity={0.7}
>
<Text style={styles.code}>{couple.invite_code.toUpperCase()}</Text>
<View style={styles.copyIcon}>
<Ionicons name="copy-outline" size={20} color={colors.textSecondary} />
</View>
</TouchableOpacity>
<Text style={styles.tapToCopy}>Tap to copy</Text>
</GlassCard>
</Animated.View>
{/* Share Button */}
<Animated.View
entering={FadeInDown.delay(400).duration(500)}
style={styles.section}
>
<GlassButton
onPress={shareCode}
fullWidth
icon={<Ionicons name="share-outline" size={20} color={colors.text} />}
>
Share Code
</GlassButton>
</Animated.View>
{/* Waiting indicator */}
<Animated.View
entering={FadeInDown.delay(500).duration(500)}
style={styles.waitingSection}
>
<View style={styles.waitingBadge}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={styles.waitingText}>Waiting for your partner to join...</Text>
</View>
</Animated.View>
{/* Cancel Button */}
<Animated.View
entering={FadeInDown.delay(600).duration(500)}
style={styles.cancelSection}
>
<TouchableOpacity
onPress={handleCancelPairing}
disabled={isSubmitting}
style={styles.cancelButton}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel Pairing</Text>
</TouchableOpacity>
</Animated.View>
</View> </View>
<Text style={styles.title}>Partner Code</Text>
<Text style={styles.subtitle}>
Share this code with your partner to link your accounts
</Text>
<TouchableOpacity style={styles.codeContainer} onPress={copyToClipboard}>
<Text style={styles.code}>{couple.invite_code.toUpperCase()}</Text>
<Ionicons name="copy-outline" size={24} color="#666" />
</TouchableOpacity>
<TouchableOpacity style={styles.shareButton} onPress={shareCode}>
<Ionicons name="share-outline" size={24} color="#fff" style={{ marginRight: 8 }} />
<Text style={styles.buttonText}>Share Code</Text>
</TouchableOpacity>
<Text style={styles.waitingText}>
Waiting for your partner to join...
</Text>
<ActivityIndicator size="small" color="#e94560" style={{ marginTop: 16 }} />
</View> </View>
</View> </GradientBackground>
); );
} }
return ( return (
<KeyboardAvoidingView <GradientBackground>
style={styles.container} <KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"} style={styles.container}
> behavior={Platform.OS === "ios" ? "padding" : "height"}
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
> >
<Ionicons name="arrow-back" size={24} color="#fff" /> {/* Header */}
</TouchableOpacity> <Animated.View
<View style={styles.content}> entering={FadeInDown.delay(100).duration(500)}
<Text style={styles.emoji}>🔗</Text> style={styles.header}
<Text style={styles.title}>Pair Up</Text>
<Text style={styles.subtitle}>
Link with your partner to start matching
</Text>
<View style={styles.card}>
<Text style={styles.cardTitle}>Have a code?</Text>
<TextInput
style={styles.input}
placeholder="Enter invite code"
placeholderTextColor="#666"
value={inviteCode}
onChangeText={setInviteCode}
autoCapitalize="characters"
maxLength={8}
/>
<TouchableOpacity
style={[styles.button, isSubmitting && styles.buttonDisabled]}
onPress={handleJoinCouple}
disabled={isSubmitting}
>
<Text style={styles.buttonText}>
{isSubmitting ? "Joining..." : "Join Partner"}
</Text>
</TouchableOpacity>
</View>
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>or</Text>
<View style={styles.dividerLine} />
</View>
<TouchableOpacity
style={styles.secondaryButton}
onPress={handleCreateCouple}
disabled={isSubmitting}
> >
<Text style={styles.secondaryButtonText}> <TouchableOpacity
{isSubmitting ? "Creating..." : "Create New Code"} style={styles.backButton}
</Text> onPress={() => router.back()}
</TouchableOpacity> activeOpacity={0.7}
>
<Ionicons name="arrow-back" size={24} color={colors.text} />
</TouchableOpacity>
<Text style={styles.title}>Pair Up</Text>
</Animated.View>
<TouchableOpacity style={styles.signOutButton} onPress={handleSignOut}> <View style={styles.content}>
<Ionicons name="log-out-outline" size={20} color="#888" /> {/* Icon */}
<Text style={styles.signOutText}>Sign Out</Text> <Animated.View
</TouchableOpacity> entering={FadeInDown.delay(200).duration(500)}
</View> style={styles.iconSection}
</KeyboardAvoidingView> >
<LinearGradient
colors={gradients.primary as [string, string]}
style={styles.iconGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name="link" size={40} color={colors.text} />
</LinearGradient>
<Text style={styles.subtitle}>
Link with your partner to start matching
</Text>
</Animated.View>
{/* Join Card */}
<Animated.View
entering={FadeInDown.delay(300).duration(500)}
style={styles.section}
>
<Text style={styles.sectionTitle}>Have a code?</Text>
<GlassCard>
<TextInput
style={styles.input}
placeholder="Enter invite code"
placeholderTextColor={colors.textTertiary}
value={inviteCode}
onChangeText={setInviteCode}
autoCapitalize="characters"
maxLength={8}
/>
<GlassButton
onPress={handleJoinCouple}
disabled={isSubmitting}
loading={isSubmitting}
fullWidth
>
Join Partner
</GlassButton>
</GlassCard>
</Animated.View>
{/* Divider */}
<Animated.View
entering={FadeInDown.delay(400).duration(500)}
style={styles.divider}
>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>or</Text>
<View style={styles.dividerLine} />
</Animated.View>
{/* Create Code Button */}
<Animated.View
entering={FadeInDown.delay(500).duration(500)}
style={styles.section}
>
<GlassButton
variant="secondary"
onPress={handleCreateCouple}
disabled={isSubmitting}
fullWidth
>
Create New Code
</GlassButton>
</Animated.View>
</View>
</KeyboardAvoidingView>
</GradientBackground>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: "#1a1a2e", },
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
header: {
paddingTop: Platform.OS === "ios" ? 60 : 40,
paddingHorizontal: spacing.lg,
paddingBottom: spacing.lg,
flexDirection: "row",
alignItems: "center",
}, },
backButton: { backButton: {
position: "absolute",
top: Platform.OS === "ios" ? 60 : 40,
left: 20,
zIndex: 10,
width: 44, width: 44,
height: 44, height: 44,
borderRadius: 22, borderRadius: 22,
backgroundColor: "rgba(255, 255, 255, 0.1)", backgroundColor: colors.glass.background,
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
marginRight: spacing.md,
}, },
centerContent: { title: {
justifyContent: "center", ...typography.title1,
alignItems: "center", color: colors.text,
}, },
content: { content: {
flex: 1, flex: 1,
padding: 24, paddingHorizontal: spacing.lg,
justifyContent: "center",
},
iconSection: {
alignItems: "center",
marginBottom: spacing.xl,
},
iconGradient: {
width: 88,
height: 88,
borderRadius: 44,
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}, marginBottom: spacing.lg,
emoji: { ...shadows.lg,
fontSize: 64,
marginBottom: 16,
},
title: {
fontSize: 32,
fontWeight: "bold",
color: "#fff",
marginBottom: 8,
}, },
subtitle: { subtitle: {
fontSize: 16, ...typography.body,
color: "#888", color: colors.textSecondary,
marginBottom: 48,
textAlign: "center", textAlign: "center",
maxWidth: 280,
}, },
card: { section: {
width: "100%", marginBottom: spacing.md,
backgroundColor: "#16213e",
borderRadius: 16,
padding: 24,
borderWidth: 1,
borderColor: "#0f3460",
}, },
cardTitle: { sectionTitle: {
fontSize: 18, ...typography.caption1,
fontWeight: "600", color: colors.textTertiary,
color: "#fff", textTransform: "uppercase",
marginBottom: 16, letterSpacing: 1.5,
marginBottom: spacing.sm,
marginLeft: spacing.xs,
}, },
input: { input: {
backgroundColor: "#1a1a2e", backgroundColor: colors.glass.background,
borderRadius: 12, borderRadius: radius.md,
padding: 16,
fontSize: 18,
color: "#fff",
marginBottom: 16,
borderWidth: 1, borderWidth: 1,
borderColor: "#0f3460", borderColor: colors.glass.border,
padding: spacing.md,
...typography.headline,
color: colors.text,
textAlign: "center", textAlign: "center",
letterSpacing: 2, letterSpacing: 4,
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace", fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
}, marginBottom: spacing.md,
button: {
backgroundColor: "#e94560",
borderRadius: 12,
padding: 16,
alignItems: "center",
},
buttonDisabled: {
opacity: 0.7,
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
}, },
divider: { divider: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
marginVertical: 32, marginVertical: spacing.lg,
width: "100%",
}, },
dividerLine: { dividerLine: {
flex: 1, flex: 1,
height: 1, height: 1,
backgroundColor: "#333", backgroundColor: colors.glass.border,
}, },
dividerText: { dividerText: {
color: "#666", ...typography.subhead,
paddingHorizontal: 16, color: colors.textTertiary,
}, paddingHorizontal: spacing.md,
secondaryButton: {
padding: 16,
},
secondaryButtonText: {
color: "#e94560",
fontSize: 16,
fontWeight: "600",
},
iconContainer: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: "rgba(233, 69, 96, 0.1)",
justifyContent: "center",
alignItems: "center",
marginBottom: 24,
}, },
codeContainer: { codeContainer: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
backgroundColor: "#16213e", justifyContent: "center",
padding: 20,
borderRadius: 16,
marginTop: 32,
marginBottom: 24,
borderWidth: 1,
borderColor: "#e94560",
borderStyle: "dashed",
}, },
code: { code: {
fontSize: 32, ...typography.largeTitle,
fontWeight: "bold", color: colors.text,
color: "#fff", letterSpacing: 6,
letterSpacing: 4,
marginRight: 16,
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace", fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
}, },
shareButton: { copyIcon: {
flexDirection: "row", width: 36,
backgroundColor: "#e94560", height: 36,
paddingHorizontal: 32, borderRadius: 18,
paddingVertical: 16, backgroundColor: colors.glass.background,
borderRadius: 12, justifyContent: "center",
alignItems: "center", alignItems: "center",
marginLeft: spacing.md,
},
tapToCopy: {
...typography.caption1,
color: colors.textTertiary,
textAlign: "center",
marginTop: spacing.sm,
},
waitingSection: {
alignItems: "center",
marginTop: spacing.xl,
},
waitingBadge: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.glass.background,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.full,
gap: spacing.sm,
}, },
waitingText: { waitingText: {
marginTop: 48, ...typography.subhead,
color: "#666", color: colors.textTertiary,
fontSize: 14,
}, },
signOutButton: { cancelSection: {
flexDirection: "row",
alignItems: "center", alignItems: "center",
marginTop: 48, marginTop: spacing.xl,
padding: 12,
}, },
signOutText: { cancelButton: {
color: "#888", paddingVertical: spacing.sm,
fontSize: 16, paddingHorizontal: spacing.md,
marginLeft: 8, },
cancelButtonText: {
...typography.subhead,
color: colors.error,
}, },
}); });
+30 -9
View File
@@ -9,7 +9,7 @@ import {
Platform, Platform,
ScrollView, ScrollView,
} from "react-native"; } from "react-native";
import { Redirect } from "expo-router"; import { Redirect, router } from "expo-router";
import * as Linking from "expo-linking"; import * as Linking from "expo-linking";
import Animated, { import Animated, {
FadeIn, FadeIn,
@@ -472,14 +472,26 @@ export default function LoginScreen() {
/> />
{authMode === "password" && ( {authMode === "password" && (
<GlassInput <>
placeholder="Enter your password" <GlassInput
value={password} placeholder="Enter your password"
onChangeText={(text) => { clearError(); setPassword(text); }} value={password}
secureTextEntry onChangeText={(text) => { clearError(); setPassword(text); }}
autoCapitalize="none" secureTextEntry
icon={<Ionicons name="lock-closed-outline" size={20} color={colors.textTertiary} />} autoCapitalize="none"
/> icon={<Ionicons name="lock-closed-outline" size={20} color={colors.textTertiary} />}
/>
{!isSignUp && (
<TouchableOpacity
style={styles.forgotPassword}
onPress={() => router.push("/(auth)/forgot-password")}
>
<Text style={styles.forgotPasswordText}>
Forgot password?
</Text>
</TouchableOpacity>
)}
</>
)} )}
{authMode === "magic-link" ? ( {authMode === "magic-link" ? (
@@ -673,6 +685,15 @@ const styles = StyleSheet.create({
...typography.subhead, ...typography.subhead,
color: colors.primary, color: colors.primary,
}, },
forgotPassword: {
alignSelf: "flex-end",
marginTop: -spacing.xs,
marginBottom: spacing.xs,
},
forgotPasswordText: {
...typography.subhead,
color: colors.primary,
},
socialContainer: { socialContainer: {
marginTop: spacing.md, marginTop: spacing.md,
}, },
+7 -1
View File
@@ -29,7 +29,7 @@ if (Platform.OS === "web" && typeof window === "undefined") {
}; };
} }
import { Stack } from "expo-router"; import { Stack, router } from "expo-router";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
// GestureHandlerRootView is only needed on native platforms // GestureHandlerRootView is only needed on native platforms
// On web, we use a simple View to avoid findNodeHandle errors // On web, we use a simple View to avoid findNodeHandle errors
@@ -103,6 +103,12 @@ export default function RootLayout() {
type: type as 'signup' | 'magiclink' | 'recovery' | 'invite' | 'email', type: type as 'signup' | 'magiclink' | 'recovery' | 'invite' | 'email',
}); });
console.log('[DeepLink] verifyOtp result:', { data: !!data?.session, error: error?.message }); console.log('[DeepLink] verifyOtp result:', { data: !!data?.session, error: error?.message });
// If this is a password recovery flow, navigate to reset password screen
if (type === 'recovery' && data?.session && !error) {
console.log('[DeepLink] Password recovery flow - navigating to reset-password');
router.replace("/(auth)/reset-password");
}
} else { } else {
// Fallback for web or other scenarios // Fallback for web or other scenarios
console.log('[DeepLink] No auth params found, falling back to getSession'); console.log('[DeepLink] No auth params found, falling back to getSession');
+5
View File
@@ -19,6 +19,11 @@ export default function Index() {
return <Redirect href="/(auth)/login" />; return <Redirect href="/(auth)/login" />;
} }
// If user hasn't completed onboarding, go to onboarding first
if (!user?.onboarding_completed) {
return <Redirect href="/(app)/onboarding" />;
}
// If user has no couple, go to pairing // If user has no couple, go to pairing
if (!user?.couple_id) { if (!user?.couple_id) {
return <Redirect href="/(app)/pairing" />; return <Redirect href="/(app)/pairing" />;
+172 -85
View File
@@ -1,9 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Confirm Your Email - Sauci</title> <title>Confirm Your Email - Sauci</title>
<!--[if mso]> <!--[if mso]>
<noscript> <noscript>
@@ -13,107 +15,192 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
</noscript> </noscript>
<style>
table { border-collapse: collapse; }
td { font-family: Arial, sans-serif; }
</style>
<![endif]--> <![endif]-->
<style>
body { margin: 0 !important; padding: 0 !important; }
table { border-spacing: 0 !important; border-collapse: collapse !important; }
td { padding: 0; }
img { border: 0; line-height: 100%; outline: none; text-decoration: none; }
a { text-decoration: none; }
@media only screen and (max-width: 600px) {
.email-container { width: 100% !important; }
.content-padding { padding-left: 20px !important; padding-right: 20px !important; }
}
</style>
</head> </head>
<body style="margin: 0; padding: 0; background-color: #0d0d1a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;"> <body style="margin: 0; padding: 0; word-spacing: normal; background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #0d0d1a;"> <div role="article" aria-roledescription="email" lang="en" style="text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; background-color: #0d0d1a;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#0d0d1a">
<tr> <tr>
<td align="center" style="padding: 40px 20px;"> <td align="center">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 520px;"> <![endif]-->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 auto;" bgcolor="#0d0d1a">
<tr>
<td align="center" valign="top" bgcolor="#0d0d1a" style="background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="520" class="email-container" style="margin: 0 auto;">
<!-- Logo Section --> <!-- Spacer -->
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
<div style="font-size: 36px; font-weight: 700; background: linear-gradient(135deg, #e94560, #9b59b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;"> </tr>
Sauci
</div>
</td>
</tr>
<!-- Main Card --> <!-- Logo Section -->
<tr> <tr>
<td> <td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 0 20px 24px 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%); border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.08);"> <!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#e94560">
<!-- Top Gradient Accent -->
<tr> <tr>
<td style="height: 4px; background: linear-gradient(90deg, #e94560, #9b59b6); border-radius: 16px 16px 0 0;"></td> <td align="center" style="padding: 16px 40px; background-color: #e94560;">
<![endif]-->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto;">
<tr>
<td align="center" bgcolor="#e94560" style="background-color: #e94560; padding: 16px 60px; border-radius: 8px;">
<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 32px; font-weight: 700; color: #ffffff; letter-spacing: -0.5px;">Sauci</span>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr> </tr>
</table>
<![endif]-->
</td>
</tr>
<!-- Card Content --> <!-- Main Card -->
<tr> <tr>
<td style="padding: 40px 32px;"> <td class="content-padding" style="padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#1a1a2e" style="background-color: #1a1a2e; border-radius: 16px;">
<!-- Icon --> <!-- Top Accent Bar -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <tr>
<tr> <td height="4" bgcolor="#e94560" style="background-color: #e94560; font-size: 0; line-height: 0;">&nbsp;</td>
<td align="center" style="padding-bottom: 24px;"> </tr>
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, rgba(233, 69, 96, 0.15), rgba(155, 89, 182, 0.15)); border-radius: 50%; display: inline-block; line-height: 64px; font-size: 28px;">
&#128140;
</div>
</td>
</tr>
</table>
<!-- Heading --> <!-- Card Content -->
<h1 style="margin: 0 0 16px 0; font-size: 24px; font-weight: 600; color: #ffffff; text-align: center; line-height: 1.3;"> <tr>
Welcome to Sauci <td style="padding: 40px 32px;" bgcolor="#1a1a2e">
</h1>
<!-- Subtext --> <!-- Icon -->
<p style="margin: 0 0 32px 0; font-size: 15px; color: rgba(255, 255, 255, 0.6); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
You're one step away from deepening your connection. Confirm your email to start your journey together. <tr>
</p> <td align="center" style="padding-bottom: 24px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" valign="middle" width="56" height="56" bgcolor="#2a2a3e" style="background-color: #2a2a3e; border-radius: 28px; font-size: 24px;">
&#128140;
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- CTA Button --> <!-- Heading -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td align="center" style="padding-bottom: 16px;">
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background: linear-gradient(135deg, #e94560, #9b59b6); color: #ffffff; text-decoration: none; font-size: 15px; font-weight: 600; border-radius: 12px; box-shadow: 0 8px 24px rgba(233, 69, 96, 0.3);"> <h1 style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 24px; font-weight: 600; color: #ffffff; line-height: 1.3;">
Confirm Email Welcome to Sauci
</a> </h1>
</td> </td>
</tr> </tr>
</table> </table>
<!-- Divider --> <!-- Subtext -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td style="padding-bottom: 24px;"> <td align="center" style="padding-bottom: 32px;">
<div style="height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);"></div> <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; color: #9999aa; text-align: center; line-height: 1.6;">
</td> You're one step away from deepening your connection. Confirm your email to start your journey together.
</tr> </p>
</table> </td>
</tr>
</table>
<!-- Alternative Link --> <!-- CTA Button -->
<p style="margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.4); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
If the button doesn't work, copy and paste this link into your browser: <tr>
</p> <td align="center" style="padding-bottom: 32px;">
<p style="margin: 8px 0 0 0; font-size: 12px; color: rgba(233, 69, 96, 0.8); text-align: center; word-break: break-all; line-height: 1.5;"> <!--[if mso]>
{{ .ConfirmationURL }} <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ .ConfirmationURL }}" style="height:48px;v-text-anchor:middle;width:180px;" arcsize="25%" strokecolor="#e94560" fillcolor="#e94560">
</p> <w:anchorlock/>
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:15px;font-weight:bold;">Confirm Email</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background-color: #e94560; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; font-weight: 600; border-radius: 12px; text-decoration: none;">
Confirm Email
</a>
<!--<![endif]-->
</td>
</tr>
</table>
</td> <!-- Divider -->
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
</table> <tr>
</td> <td style="padding-bottom: 24px;">
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td height="1" bgcolor="#333344" style="background-color: #333344; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Footer --> <!-- Alternative Link -->
<tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<td style="padding: 32px 16px;"> <tr>
<p style="margin: 0 0 8px 0; font-size: 13px; color: rgba(255, 255, 255, 0.4); text-align: center;"> <td align="center">
This link will expire in 24 hours. <p style="margin: 0 0 8px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #666677; text-align: center; line-height: 1.6;">
</p> If the button doesn't work, copy and paste this link into your browser:
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.3); text-align: center;"> </p>
If you didn't create an account, you can safely ignore this email. <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; text-align: center; word-break: break-all; line-height: 1.5;">
</p> <a href="{{ .ConfirmationURL }}" style="color: #e94560; text-decoration: underline;">{{ .ConfirmationURL }}</a>
</td> </p>
</tr> </td>
</tr>
</table>
</table> </td>
</td> </tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 32px 20px;">
<p style="margin: 0 0 8px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #666677; text-align: center;">
This link will expire in 24 hours.
</p>
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; color: #555566; text-align: center;">
If you didn't create an account, you can safely ignore this email.
</p>
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr> </tr>
</table> </table>
<![endif]-->
</div>
</body> </body>
</html> </html>
+173 -96
View File
@@ -1,9 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Confirm Email Change - Sauci</title> <title>Confirm Email Change - Sauci</title>
<!--[if mso]> <!--[if mso]>
<noscript> <noscript>
@@ -13,118 +15,193 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
</noscript> </noscript>
<style>
table { border-collapse: collapse; }
td { font-family: Arial, sans-serif; }
</style>
<![endif]--> <![endif]-->
<style>
body { margin: 0 !important; padding: 0 !important; }
table { border-spacing: 0 !important; border-collapse: collapse !important; }
td { padding: 0; }
img { border: 0; line-height: 100%; outline: none; text-decoration: none; }
a { text-decoration: none; }
@media only screen and (max-width: 600px) {
.email-container { width: 100% !important; }
.content-padding { padding-left: 20px !important; padding-right: 20px !important; }
}
</style>
</head> </head>
<body style="margin: 0; padding: 0; background-color: #0d0d1a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;"> <body style="margin: 0; padding: 0; word-spacing: normal; background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #0d0d1a;"> <div role="article" aria-roledescription="email" lang="en" style="text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; background-color: #0d0d1a;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#0d0d1a">
<tr> <tr>
<td align="center" style="padding: 40px 20px;"> <td align="center">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 520px;"> <![endif]-->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 auto;" bgcolor="#0d0d1a">
<tr>
<td align="center" valign="top" bgcolor="#0d0d1a" style="background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="520" class="email-container" style="margin: 0 auto;">
<!-- Logo Section --> <!-- Spacer -->
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
<div style="font-size: 36px; font-weight: 700; background: linear-gradient(135deg, #e94560, #9b59b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;"> </tr>
Sauci
</div>
</td>
</tr>
<!-- Main Card --> <!-- Logo Section -->
<tr> <tr>
<td> <td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 0 20px 24px 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%); border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.08);"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto;">
<tr>
<td align="center" bgcolor="#e94560" style="background-color: #e94560; padding: 16px 60px; border-radius: 8px;">
<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 32px; font-weight: 700; color: #ffffff; letter-spacing: -0.5px;">Sauci</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- Top Gradient Accent --> <!-- Main Card -->
<tr> <tr>
<td style="height: 4px; background: linear-gradient(90deg, #e94560, #9b59b6); border-radius: 16px 16px 0 0;"></td> <td class="content-padding" style="padding: 0 20px;">
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#1a1a2e" style="background-color: #1a1a2e; border-radius: 16px;">
<!-- Card Content --> <!-- Top Accent Bar -->
<tr> <tr>
<td style="padding: 40px 32px;"> <td height="4" bgcolor="#e94560" style="background-color: #e94560; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
<!-- Icon --> <!-- Card Content -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <tr>
<tr> <td style="padding: 40px 32px;" bgcolor="#1a1a2e">
<td align="center" style="padding-bottom: 24px;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, rgba(233, 69, 96, 0.15), rgba(155, 89, 182, 0.15)); border-radius: 50%; display: inline-block; line-height: 64px; font-size: 28px;">
&#9993;
</div>
</td>
</tr>
</table>
<!-- Heading --> <!-- Icon -->
<h1 style="margin: 0 0 16px 0; font-size: 24px; font-weight: 600; color: #ffffff; text-align: center; line-height: 1.3;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
Confirm Email Change <tr>
</h1> <td align="center" style="padding-bottom: 24px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" valign="middle" width="56" height="56" bgcolor="#2a2a3e" style="background-color: #2a2a3e; border-radius: 28px; font-size: 24px;">
&#9993;
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Subtext --> <!-- Heading -->
<p style="margin: 0 0 32px 0; font-size: 15px; color: rgba(255, 255, 255, 0.6); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
You requested to change your email address. Click the button below to confirm this change. <tr>
</p> <td align="center" style="padding-bottom: 16px;">
<h1 style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 24px; font-weight: 600; color: #ffffff; line-height: 1.3;">
Confirm Email Change
</h1>
</td>
</tr>
</table>
<!-- CTA Button --> <!-- Subtext -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td align="center" style="padding-bottom: 32px;">
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background: linear-gradient(135deg, #e94560, #9b59b6); color: #ffffff; text-decoration: none; font-size: 15px; font-weight: 600; border-radius: 12px; box-shadow: 0 8px 24px rgba(233, 69, 96, 0.3);"> <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; color: #9999aa; text-align: center; line-height: 1.6;">
Confirm New Email You requested to change your email address. Click the button below to confirm this change.
</a> </p>
</td> </td>
</tr> </tr>
</table> </table>
<!-- Info Notice --> <!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: rgba(155, 89, 182, 0.08); border-radius: 8px; border: 1px solid rgba(155, 89, 182, 0.15);"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td style="padding: 16px;"> <td align="center" style="padding-bottom: 32px;">
<p style="margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.5); text-align: center; line-height: 1.5;"> <!--[if mso]>
&#128233; After confirming, you'll use your new email address to sign in to Sauci. <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ .ConfirmationURL }}" style="height:48px;v-text-anchor:middle;width:200px;" arcsize="25%" strokecolor="#e94560" fillcolor="#e94560">
</p> <w:anchorlock/>
</td> <center style="color:#ffffff;font-family:Arial,sans-serif;font-size:15px;font-weight:bold;">Confirm New Email</center>
</tr> </v:roundrect>
</table> <![endif]-->
<!--[if !mso]><!-->
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background-color: #e94560; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; font-weight: 600; border-radius: 12px; text-decoration: none;">
Confirm New Email
</a>
<!--<![endif]-->
</td>
</tr>
</table>
<!-- Divider --> <!-- Info Notice -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#2a2a3e" style="background-color: #2a2a3e; border-radius: 8px;">
<tr> <tr>
<td style="padding: 24px 0;"> <td style="padding: 16px;">
<div style="height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);"></div> <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #8888aa; text-align: center; line-height: 1.5;">
</td> &#128233; After confirming, you'll use your new email address to sign in to Sauci.
</tr> </p>
</table> </td>
</tr>
</table>
<!-- Alternative Link --> <!-- Divider -->
<p style="margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.4); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
If the button doesn't work, copy and paste this link into your browser: <tr>
</p> <td style="padding: 24px 0;">
<p style="margin: 8px 0 0 0; font-size: 12px; color: rgba(233, 69, 96, 0.8); text-align: center; word-break: break-all; line-height: 1.5;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
{{ .ConfirmationURL }} <tr>
</p> <td height="1" bgcolor="#333344" style="background-color: #333344; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td> <!-- Alternative Link -->
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
</table> <tr>
</td> <td align="center">
</tr> <p style="margin: 0 0 8px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #666677; text-align: center; line-height: 1.6;">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; text-align: center; word-break: break-all; line-height: 1.5;">
<a href="{{ .ConfirmationURL }}" style="color: #e94560; text-decoration: underline;">{{ .ConfirmationURL }}</a>
</p>
</td>
</tr>
</table>
<!-- Footer --> </td>
<tr> </tr>
<td style="padding: 32px 16px;"> </table>
<p style="margin: 0 0 8px 0; font-size: 13px; color: rgba(255, 255, 255, 0.4); text-align: center;"> </td>
Didn't request this change? </tr>
</p>
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.3); text-align: center;">
If you didn't request an email change, please secure your account by resetting your password immediately.
</p>
</td>
</tr>
</table> <!-- Footer -->
</td> <tr>
<td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 32px 20px;">
<p style="margin: 0 0 8px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #666677; text-align: center;">
Didn't request this change?
</p>
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; color: #555566; text-align: center;">
If you didn't request an email change, please secure your account by resetting your password immediately.
</p>
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr> </tr>
</table> </table>
<![endif]-->
</div>
</body> </body>
</html> </html>
+193 -116
View File
@@ -1,9 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>You're Invited to Sauci</title> <title>You're Invited to Sauci</title>
<!--[if mso]> <!--[if mso]>
<noscript> <noscript>
@@ -13,138 +15,213 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
</noscript> </noscript>
<style>
table { border-collapse: collapse; }
td { font-family: Arial, sans-serif; }
</style>
<![endif]--> <![endif]-->
<style>
body { margin: 0 !important; padding: 0 !important; }
table { border-spacing: 0 !important; border-collapse: collapse !important; }
td { padding: 0; }
img { border: 0; line-height: 100%; outline: none; text-decoration: none; }
a { text-decoration: none; }
@media only screen and (max-width: 600px) {
.email-container { width: 100% !important; }
.content-padding { padding-left: 20px !important; padding-right: 20px !important; }
}
</style>
</head> </head>
<body style="margin: 0; padding: 0; background-color: #0d0d1a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;"> <body style="margin: 0; padding: 0; word-spacing: normal; background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #0d0d1a;"> <div role="article" aria-roledescription="email" lang="en" style="text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; background-color: #0d0d1a;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#0d0d1a">
<tr> <tr>
<td align="center" style="padding: 40px 20px;"> <td align="center">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 520px;"> <![endif]-->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 auto;" bgcolor="#0d0d1a">
<tr>
<td align="center" valign="top" bgcolor="#0d0d1a" style="background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="520" class="email-container" style="margin: 0 auto;">
<!-- Logo Section --> <!-- Spacer -->
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
<div style="font-size: 36px; font-weight: 700; background: linear-gradient(135deg, #e94560, #9b59b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;"> </tr>
Sauci
</div>
</td>
</tr>
<!-- Main Card --> <!-- Logo Section -->
<tr> <tr>
<td> <td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 0 20px 24px 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%); border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.08);"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto;">
<tr>
<td align="center" bgcolor="#e94560" style="background-color: #e94560; padding: 16px 60px; border-radius: 8px;">
<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 32px; font-weight: 700; color: #ffffff; letter-spacing: -0.5px;">Sauci</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- Top Gradient Accent --> <!-- Main Card -->
<tr> <tr>
<td style="height: 4px; background: linear-gradient(90deg, #e94560, #9b59b6); border-radius: 16px 16px 0 0;"></td> <td class="content-padding" style="padding: 0 20px;">
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#1a1a2e" style="background-color: #1a1a2e; border-radius: 16px;">
<!-- Card Content --> <!-- Top Accent Bar -->
<tr> <tr>
<td style="padding: 40px 32px;"> <td height="4" bgcolor="#e94560" style="background-color: #e94560; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
<!-- Icon --> <!-- Card Content -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <tr>
<tr> <td style="padding: 40px 32px;" bgcolor="#1a1a2e">
<td align="center" style="padding-bottom: 24px;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, rgba(233, 69, 96, 0.15), rgba(155, 89, 182, 0.15)); border-radius: 50%; display: inline-block; line-height: 64px; font-size: 28px;">
&#128149;
</div>
</td>
</tr>
</table>
<!-- Heading --> <!-- Icon -->
<h1 style="margin: 0 0 16px 0; font-size: 24px; font-weight: 600; color: #ffffff; text-align: center; line-height: 1.3;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
You've Been Invited <tr>
</h1> <td align="center" style="padding-bottom: 24px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" valign="middle" width="56" height="56" bgcolor="#2a2a3e" style="background-color: #2a2a3e; border-radius: 28px; font-size: 24px;">
&#128149;
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Subtext --> <!-- Heading -->
<p style="margin: 0 0 32px 0; font-size: 15px; color: rgba(255, 255, 255, 0.6); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
Someone special has invited you to join Sauci, the app designed to help couples deepen their connection and discover new things about each other. <tr>
</p> <td align="center" style="padding-bottom: 16px;">
<h1 style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 24px; font-weight: 600; color: #ffffff; line-height: 1.3;">
You've Been Invited
</h1>
</td>
</tr>
</table>
<!-- CTA Button --> <!-- Subtext -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td align="center" style="padding-bottom: 32px;">
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background: linear-gradient(135deg, #e94560, #9b59b6); color: #ffffff; text-decoration: none; font-size: 15px; font-weight: 600; border-radius: 12px; box-shadow: 0 8px 24px rgba(233, 69, 96, 0.3);"> <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; color: #9999aa; text-align: center; line-height: 1.6;">
Accept Invitation Someone special has invited you to join Sauci, the app designed to help couples deepen their connection and discover new things about each other.
</a> </p>
</td> </td>
</tr> </tr>
</table> </table>
<!-- Features Section --> <!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: rgba(22, 33, 62, 0.6); border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.05);"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td style="padding: 20px;"> <td align="center" style="padding-bottom: 32px;">
<p style="margin: 0 0 12px 0; font-size: 13px; font-weight: 600; color: rgba(255, 255, 255, 0.7); text-align: center; text-transform: uppercase; letter-spacing: 1px;"> <!--[if mso]>
What awaits you <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ .ConfirmationURL }}" style="height:48px;v-text-anchor:middle;width:200px;" arcsize="25%" strokecolor="#e94560" fillcolor="#e94560">
</p> <w:anchorlock/>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <center style="color:#ffffff;font-family:Arial,sans-serif;font-size:15px;font-weight:bold;">Accept Invitation</center>
<tr> </v:roundrect>
<td style="padding: 8px 0;"> <![endif]-->
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.5); text-align: center;"> <!--[if !mso]><!-->
&#128293; Thoughtful questions to spark conversations <a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background-color: #e94560; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; font-weight: 600; border-radius: 12px; text-decoration: none;">
</p> Accept Invitation
</td> </a>
</tr> <!--<![endif]-->
<tr> </td>
<td style="padding: 8px 0;"> </tr>
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.5); text-align: center;"> </table>
&#128140; Discover mutual interests with your partner
</p>
</td>
</tr>
<tr>
<td style="padding: 8px 0;">
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.5); text-align: center;">
&#128172; Private chats when you both match
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Divider --> <!-- Features Section -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#222240" style="background-color: #222240; border-radius: 12px;">
<tr> <tr>
<td style="padding: 24px 0;"> <td style="padding: 20px;">
<div style="height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);"></div> <p style="margin: 0 0 12px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; font-weight: 600; color: #9999aa; text-align: center; text-transform: uppercase; letter-spacing: 1px;">
</td> What awaits you
</tr> </p>
</table> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 8px 0;">
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 14px; color: #8888aa; text-align: center;">
&#128293; Thoughtful questions to spark conversations
</p>
</td>
</tr>
<tr>
<td style="padding: 8px 0;">
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 14px; color: #8888aa; text-align: center;">
&#128140; Discover mutual interests with your partner
</p>
</td>
</tr>
<tr>
<td style="padding: 8px 0;">
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 14px; color: #8888aa; text-align: center;">
&#128172; Private chats when you both match
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Alternative Link --> <!-- Divider -->
<p style="margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.4); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
If the button doesn't work, copy and paste this link into your browser: <tr>
</p> <td style="padding: 24px 0;">
<p style="margin: 8px 0 0 0; font-size: 12px; color: rgba(233, 69, 96, 0.8); text-align: center; word-break: break-all; line-height: 1.5;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
{{ .ConfirmationURL }} <tr>
</p> <td height="1" bgcolor="#333344" style="background-color: #333344; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td> <!-- Alternative Link -->
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
</table> <tr>
</td> <td align="center">
</tr> <p style="margin: 0 0 8px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #666677; text-align: center; line-height: 1.6;">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; text-align: center; word-break: break-all; line-height: 1.5;">
<a href="{{ .ConfirmationURL }}" style="color: #e94560; text-decoration: underline;">{{ .ConfirmationURL }}</a>
</p>
</td>
</tr>
</table>
<!-- Footer --> </td>
<tr> </tr>
<td style="padding: 32px 16px;"> </table>
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.3); text-align: center;"> </td>
This invitation link will expire in 7 days. If you didn't expect this invitation, you can safely ignore this email. </tr>
</p>
</td>
</tr>
</table> <!-- Footer -->
</td> <tr>
<td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 32px 20px;">
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; color: #555566; text-align: center;">
This invitation link will expire in 7 days. If you didn't expect this invitation, you can safely ignore this email.
</p>
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr> </tr>
</table> </table>
<![endif]-->
</div>
</body> </body>
</html> </html>
+170 -93
View File
@@ -1,9 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Sign In to Sauci</title> <title>Sign In to Sauci</title>
<!--[if mso]> <!--[if mso]>
<noscript> <noscript>
@@ -13,115 +15,190 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
</noscript> </noscript>
<style>
table { border-collapse: collapse; }
td { font-family: Arial, sans-serif; }
</style>
<![endif]--> <![endif]-->
<style>
body { margin: 0 !important; padding: 0 !important; }
table { border-spacing: 0 !important; border-collapse: collapse !important; }
td { padding: 0; }
img { border: 0; line-height: 100%; outline: none; text-decoration: none; }
a { text-decoration: none; }
@media only screen and (max-width: 600px) {
.email-container { width: 100% !important; }
.content-padding { padding-left: 20px !important; padding-right: 20px !important; }
}
</style>
</head> </head>
<body style="margin: 0; padding: 0; background-color: #0d0d1a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;"> <body style="margin: 0; padding: 0; word-spacing: normal; background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #0d0d1a;"> <div role="article" aria-roledescription="email" lang="en" style="text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; background-color: #0d0d1a;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#0d0d1a">
<tr> <tr>
<td align="center" style="padding: 40px 20px;"> <td align="center">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 520px;"> <![endif]-->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 auto;" bgcolor="#0d0d1a">
<tr>
<td align="center" valign="top" bgcolor="#0d0d1a" style="background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="520" class="email-container" style="margin: 0 auto;">
<!-- Logo Section --> <!-- Spacer -->
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
<div style="font-size: 36px; font-weight: 700; background: linear-gradient(135deg, #e94560, #9b59b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;"> </tr>
Sauci
</div>
</td>
</tr>
<!-- Main Card --> <!-- Logo Section -->
<tr> <tr>
<td> <td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 0 20px 24px 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%); border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.08);"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto;">
<tr>
<td align="center" bgcolor="#e94560" style="background-color: #e94560; padding: 16px 60px; border-radius: 8px;">
<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 32px; font-weight: 700; color: #ffffff; letter-spacing: -0.5px;">Sauci</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- Top Gradient Accent --> <!-- Main Card -->
<tr> <tr>
<td style="height: 4px; background: linear-gradient(90deg, #e94560, #9b59b6); border-radius: 16px 16px 0 0;"></td> <td class="content-padding" style="padding: 0 20px;">
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#1a1a2e" style="background-color: #1a1a2e; border-radius: 16px;">
<!-- Card Content --> <!-- Top Accent Bar -->
<tr> <tr>
<td style="padding: 40px 32px;"> <td height="4" bgcolor="#e94560" style="background-color: #e94560; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
<!-- Icon --> <!-- Card Content -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <tr>
<tr> <td style="padding: 40px 32px;" bgcolor="#1a1a2e">
<td align="center" style="padding-bottom: 24px;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, rgba(233, 69, 96, 0.15), rgba(155, 89, 182, 0.15)); border-radius: 50%; display: inline-block; line-height: 64px; font-size: 28px;">
&#10024;
</div>
</td>
</tr>
</table>
<!-- Heading --> <!-- Icon -->
<h1 style="margin: 0 0 16px 0; font-size: 24px; font-weight: 600; color: #ffffff; text-align: center; line-height: 1.3;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
Your Magic Link <tr>
</h1> <td align="center" style="padding-bottom: 24px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" valign="middle" width="56" height="56" bgcolor="#2a2a3e" style="background-color: #2a2a3e; border-radius: 28px; font-size: 24px;">
&#10024;
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Subtext --> <!-- Heading -->
<p style="margin: 0 0 32px 0; font-size: 15px; color: rgba(255, 255, 255, 0.6); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
Click the button below to securely sign in to your Sauci account. No password needed. <tr>
</p> <td align="center" style="padding-bottom: 16px;">
<h1 style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 24px; font-weight: 600; color: #ffffff; line-height: 1.3;">
Your Magic Link
</h1>
</td>
</tr>
</table>
<!-- CTA Button --> <!-- Subtext -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td align="center" style="padding-bottom: 32px;">
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background: linear-gradient(135deg, #e94560, #9b59b6); color: #ffffff; text-decoration: none; font-size: 15px; font-weight: 600; border-radius: 12px; box-shadow: 0 8px 24px rgba(233, 69, 96, 0.3);"> <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; color: #9999aa; text-align: center; line-height: 1.6;">
Sign In to Sauci Click the button below to securely sign in to your Sauci account. No password needed.
</a> </p>
</td> </td>
</tr> </tr>
</table> </table>
<!-- Security Notice --> <!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: rgba(233, 69, 96, 0.08); border-radius: 8px; border: 1px solid rgba(233, 69, 96, 0.15);"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td style="padding: 16px;"> <td align="center" style="padding-bottom: 32px;">
<p style="margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.5); text-align: center; line-height: 1.5;"> <!--[if mso]>
&#128274; This link is unique to you and expires in 1 hour for your security. <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ .ConfirmationURL }}" style="height:48px;v-text-anchor:middle;width:180px;" arcsize="25%" strokecolor="#e94560" fillcolor="#e94560">
</p> <w:anchorlock/>
</td> <center style="color:#ffffff;font-family:Arial,sans-serif;font-size:15px;font-weight:bold;">Sign In to Sauci</center>
</tr> </v:roundrect>
</table> <![endif]-->
<!--[if !mso]><!-->
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background-color: #e94560; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; font-weight: 600; border-radius: 12px; text-decoration: none;">
Sign In to Sauci
</a>
<!--<![endif]-->
</td>
</tr>
</table>
<!-- Divider --> <!-- Security Notice -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#2a2a3e" style="background-color: #2a2a3e; border-radius: 8px;">
<tr> <tr>
<td style="padding: 24px 0;"> <td style="padding: 16px;">
<div style="height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);"></div> <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #8888aa; text-align: center; line-height: 1.5;">
</td> &#128274; This link is unique to you and expires in 1 hour for your security.
</tr> </p>
</table> </td>
</tr>
</table>
<!-- Alternative Link --> <!-- Divider -->
<p style="margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.4); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
If the button doesn't work, copy and paste this link into your browser: <tr>
</p> <td style="padding: 24px 0;">
<p style="margin: 8px 0 0 0; font-size: 12px; color: rgba(233, 69, 96, 0.8); text-align: center; word-break: break-all; line-height: 1.5;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
{{ .ConfirmationURL }} <tr>
</p> <td height="1" bgcolor="#333344" style="background-color: #333344; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td> <!-- Alternative Link -->
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
</table> <tr>
</td> <td align="center">
</tr> <p style="margin: 0 0 8px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #666677; text-align: center; line-height: 1.6;">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; text-align: center; word-break: break-all; line-height: 1.5;">
<a href="{{ .ConfirmationURL }}" style="color: #e94560; text-decoration: underline;">{{ .ConfirmationURL }}</a>
</p>
</td>
</tr>
</table>
<!-- Footer --> </td>
<tr> </tr>
<td style="padding: 32px 16px;"> </table>
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.3); text-align: center;"> </td>
If you didn't request this sign in link, you can safely ignore this email. </tr>
</p>
</td>
</tr>
</table> <!-- Footer -->
</td> <tr>
<td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 32px 20px;">
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; color: #555566; text-align: center;">
If you didn't request this sign in link, you can safely ignore this email.
</p>
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr> </tr>
</table> </table>
<![endif]-->
</div>
</body> </body>
</html> </html>
+173 -96
View File
@@ -1,9 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Reset Your Password - Sauci</title> <title>Reset Your Password - Sauci</title>
<!--[if mso]> <!--[if mso]>
<noscript> <noscript>
@@ -13,118 +15,193 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
</noscript> </noscript>
<style>
table { border-collapse: collapse; }
td { font-family: Arial, sans-serif; }
</style>
<![endif]--> <![endif]-->
<style>
body { margin: 0 !important; padding: 0 !important; }
table { border-spacing: 0 !important; border-collapse: collapse !important; }
td { padding: 0; }
img { border: 0; line-height: 100%; outline: none; text-decoration: none; }
a { text-decoration: none; }
@media only screen and (max-width: 600px) {
.email-container { width: 100% !important; }
.content-padding { padding-left: 20px !important; padding-right: 20px !important; }
}
</style>
</head> </head>
<body style="margin: 0; padding: 0; background-color: #0d0d1a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;"> <body style="margin: 0; padding: 0; word-spacing: normal; background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #0d0d1a;"> <div role="article" aria-roledescription="email" lang="en" style="text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; background-color: #0d0d1a;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#0d0d1a">
<tr> <tr>
<td align="center" style="padding: 40px 20px;"> <td align="center">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 520px;"> <![endif]-->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 auto;" bgcolor="#0d0d1a">
<tr>
<td align="center" valign="top" bgcolor="#0d0d1a" style="background-color: #0d0d1a;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="520" class="email-container" style="margin: 0 auto;">
<!-- Logo Section --> <!-- Spacer -->
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
<div style="font-size: 36px; font-weight: 700; background: linear-gradient(135deg, #e94560, #9b59b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;"> </tr>
Sauci
</div>
</td>
</tr>
<!-- Main Card --> <!-- Logo Section -->
<tr> <tr>
<td> <td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 0 20px 24px 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%); border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.08);"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 0 auto;">
<tr>
<td align="center" bgcolor="#e94560" style="background-color: #e94560; padding: 16px 60px; border-radius: 8px;">
<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 32px; font-weight: 700; color: #ffffff; letter-spacing: -0.5px;">Sauci</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- Top Gradient Accent --> <!-- Main Card -->
<tr> <tr>
<td style="height: 4px; background: linear-gradient(90deg, #e94560, #9b59b6); border-radius: 16px 16px 0 0;"></td> <td class="content-padding" style="padding: 0 20px;">
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#1a1a2e" style="background-color: #1a1a2e; border-radius: 16px;">
<!-- Card Content --> <!-- Top Accent Bar -->
<tr> <tr>
<td style="padding: 40px 32px;"> <td height="4" bgcolor="#e94560" style="background-color: #e94560; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
<!-- Icon --> <!-- Card Content -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <tr>
<tr> <td style="padding: 40px 32px;" bgcolor="#1a1a2e">
<td align="center" style="padding-bottom: 24px;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, rgba(233, 69, 96, 0.15), rgba(155, 89, 182, 0.15)); border-radius: 50%; display: inline-block; line-height: 64px; font-size: 28px;">
&#128272;
</div>
</td>
</tr>
</table>
<!-- Heading --> <!-- Icon -->
<h1 style="margin: 0 0 16px 0; font-size: 24px; font-weight: 600; color: #ffffff; text-align: center; line-height: 1.3;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
Reset Your Password <tr>
</h1> <td align="center" style="padding-bottom: 24px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" valign="middle" width="56" height="56" bgcolor="#2a2a3e" style="background-color: #2a2a3e; border-radius: 28px; font-size: 24px;">
&#128272;
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Subtext --> <!-- Heading -->
<p style="margin: 0 0 32px 0; font-size: 15px; color: rgba(255, 255, 255, 0.6); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
We received a request to reset your password. Click the button below to choose a new one. <tr>
</p> <td align="center" style="padding-bottom: 16px;">
<h1 style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 24px; font-weight: 600; color: #ffffff; line-height: 1.3;">
Reset Your Password
</h1>
</td>
</tr>
</table>
<!-- CTA Button --> <!-- Subtext -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td align="center" style="padding-bottom: 32px;"> <td align="center" style="padding-bottom: 32px;">
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background: linear-gradient(135deg, #e94560, #9b59b6); color: #ffffff; text-decoration: none; font-size: 15px; font-weight: 600; border-radius: 12px; box-shadow: 0 8px 24px rgba(233, 69, 96, 0.3);"> <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; color: #9999aa; text-align: center; line-height: 1.6;">
Reset Password We received a request to reset your password. Click the button below to choose a new one.
</a> </p>
</td> </td>
</tr> </tr>
</table> </table>
<!-- Security Notice --> <!-- CTA Button -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: rgba(155, 89, 182, 0.08); border-radius: 8px; border: 1px solid rgba(155, 89, 182, 0.15);"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td style="padding: 16px;"> <td align="center" style="padding-bottom: 32px;">
<p style="margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.5); text-align: center; line-height: 1.5;"> <!--[if mso]>
&#9200; This link expires in 1 hour. After resetting, you'll need to sign in again on all devices. <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ .ConfirmationURL }}" style="height:48px;v-text-anchor:middle;width:180px;" arcsize="25%" strokecolor="#e94560" fillcolor="#e94560">
</p> <w:anchorlock/>
</td> <center style="color:#ffffff;font-family:Arial,sans-serif;font-size:15px;font-weight:bold;">Reset Password</center>
</tr> </v:roundrect>
</table> <![endif]-->
<!--[if !mso]><!-->
<a href="{{ .ConfirmationURL }}" target="_blank" style="display: inline-block; padding: 14px 40px; background-color: #e94560; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 15px; font-weight: 600; border-radius: 12px; text-decoration: none;">
Reset Password
</a>
<!--<![endif]-->
</td>
</tr>
</table>
<!-- Divider --> <!-- Security Notice -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" bgcolor="#2a2a3e" style="background-color: #2a2a3e; border-radius: 8px;">
<tr> <tr>
<td style="padding: 24px 0;"> <td style="padding: 16px;">
<div style="height: 1px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);"></div> <p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #8888aa; text-align: center; line-height: 1.5;">
</td> &#9200; This link expires in 1 hour. After resetting, you'll need to sign in again on all devices.
</tr> </p>
</table> </td>
</tr>
</table>
<!-- Alternative Link --> <!-- Divider -->
<p style="margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.4); text-align: center; line-height: 1.6;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
If the button doesn't work, copy and paste this link into your browser: <tr>
</p> <td style="padding: 24px 0;">
<p style="margin: 8px 0 0 0; font-size: 12px; color: rgba(233, 69, 96, 0.8); text-align: center; word-break: break-all; line-height: 1.5;"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
{{ .ConfirmationURL }} <tr>
</p> <td height="1" bgcolor="#333344" style="background-color: #333344; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td> <!-- Alternative Link -->
</tr> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
</table> <tr>
</td> <td align="center">
</tr> <p style="margin: 0 0 8px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #666677; text-align: center; line-height: 1.6;">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; text-align: center; word-break: break-all; line-height: 1.5;">
<a href="{{ .ConfirmationURL }}" style="color: #e94560; text-decoration: underline;">{{ .ConfirmationURL }}</a>
</p>
</td>
</tr>
</table>
<!-- Footer --> </td>
<tr> </tr>
<td style="padding: 32px 16px;"> </table>
<p style="margin: 0 0 8px 0; font-size: 13px; color: rgba(255, 255, 255, 0.4); text-align: center;"> </td>
Didn't request this? </tr>
</p>
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.3); text-align: center;">
If you didn't request a password reset, you can safely ignore this email. Your password won't change until you click the link above.
</p>
</td>
</tr>
</table> <!-- Footer -->
</td> <tr>
<td align="center" bgcolor="#0d0d1a" style="background-color: #0d0d1a; padding: 32px 20px;">
<p style="margin: 0 0 8px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 13px; color: #666677; text-align: center;">
Didn't request this?
</p>
<p style="margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 12px; color: #555566; text-align: center;">
If you didn't request a password reset, you can safely ignore this email. Your password won't change until you click the link above.
</p>
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="40" style="font-size: 0; line-height: 0;" bgcolor="#0d0d1a">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr> </tr>
</table> </table>
<![endif]-->
</div>
</body> </body>
</html> </html>
+74 -17
View File
@@ -47,13 +47,28 @@ Deno.serve(async (req) => {
if (method === "POST") { if (method === "POST") {
// Check if user is already in a couple // Check if user is already in a couple
const { data: existingProfile } = await supabase const { data: existingProfile, error: profileError } = await supabase
.from("profiles") .from("profiles")
.select("couple_id") .select("couple_id")
.eq("id", user.id) .eq("id", user.id)
.single(); .maybeSingle();
if (existingProfile?.couple_id) { if (profileError) {
console.error("Profile lookup error:", profileError);
return new Response(
JSON.stringify({ error: "Failed to check profile" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
if (!existingProfile) {
return new Response(
JSON.stringify({ error: "Profile not found. Please complete signup first." }),
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
if (existingProfile.couple_id) {
return new Response( return new Response(
JSON.stringify({ error: "You are already in a couple" }), JSON.stringify({ error: "You are already in a couple" }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
@@ -67,27 +82,28 @@ Deno.serve(async (req) => {
const { invite_code }: JoinCoupleBody = body; const { invite_code }: JoinCoupleBody = body;
// Validate invite code format (alphanumeric, 8 chars) // Validate invite code format (alphanumeric, 8 chars)
const sanitizedCode = invite_code.trim().toUpperCase(); const sanitizedCode = invite_code.trim().toLowerCase();
if (!/^[A-Z0-9]{8}$/.test(sanitizedCode)) { if (!/^[a-z0-9]{8}$/.test(sanitizedCode)) {
return new Response( return new Response(
JSON.stringify({ error: "Invalid invite code format" }), JSON.stringify({ error: "Invalid invite code format" }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
); );
} }
// Use secure RPC function for invite code verification // Find couple by invite code (stored as lowercase in DB)
const { data: verifyResult, error: verifyError } = await supabase const { data: couple, error: coupleError } = await supabase
.rpc("verify_invite_code", { code: sanitizedCode }); .from("couples")
.select("id")
.eq("invite_code", sanitizedCode)
.maybeSingle();
if (verifyError || !verifyResult?.valid) { if (coupleError || !couple) {
return new Response( return new Response(
JSON.stringify({ error: verifyResult?.error || "Invalid invite code" }), JSON.stringify({ error: "Invalid invite code" }),
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } } { status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }
); );
} }
const couple = { id: verifyResult.couple_id };
// Check if couple already has 2 members // Check if couple already has 2 members
const { count } = await supabase const { count } = await supabase
.from("profiles") .from("profiles")
@@ -151,21 +167,62 @@ Deno.serve(async (req) => {
} }
if (method === "DELETE") { if (method === "DELETE") {
// Leave couple // Get user's current couple
const { error } = await supabase const { data: profile, error: profileFetchError } = await supabase
.from("profiles")
.select("couple_id")
.eq("id", user.id)
.maybeSingle();
if (profileFetchError || !profile?.couple_id) {
return new Response(
JSON.stringify({ error: "You are not in a couple" }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const coupleId = profile.couple_id;
// Count members in this couple
const { count, error: countError } = 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" } }
);
}
// Remove ALL members from couple (both partners get unlinked)
// This ensures the remaining partner isn't left in a "waiting" state
const { error: updateError } = await supabase
.from("profiles") .from("profiles")
.update({ couple_id: null }) .update({ couple_id: null })
.eq("id", user.id); .eq("couple_id", coupleId);
if (error) { if (updateError) {
return new Response( return new Response(
JSON.stringify({ error: "Failed to leave couple" }), JSON.stringify({ error: "Failed to leave couple" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
); );
} }
// Delete the couple entirely (cascades to all related data)
const { error: deleteError } = await supabase
.from("couples")
.delete()
.eq("id", coupleId);
if (deleteError) {
console.error("Failed to delete couple:", deleteError);
// Don't fail the request, users already unlinked
}
return new Response( return new Response(
JSON.stringify({ success: true }), JSON.stringify({ success: true, couple_deleted: true }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } } { headers: { ...corsHeaders, "Content-Type": "application/json" } }
); );
} }
@@ -673,10 +673,10 @@ CREATE TRIGGER on_new_message
AFTER INSERT ON messages AFTER INSERT ON messages
FOR EACH ROW EXECUTE FUNCTION notify_new_message(); FOR EACH ROW EXECUTE FUNCTION notify_new_message();
-- Auth trigger (in auth schema - handled separately by Supabase) -- Auth trigger to create profile on signup
-- CREATE TRIGGER on_auth_user_created CREATE TRIGGER on_auth_user_created
-- AFTER INSERT ON auth.users AFTER INSERT ON auth.users
-- FOR EACH ROW EXECUTE FUNCTION handle_new_user(); FOR EACH ROW EXECUTE FUNCTION handle_new_user();
-- ============================================================================ -- ============================================================================
-- ENABLE ROW LEVEL SECURITY -- ENABLE ROW LEVEL SECURITY