feat: add catchup and unpaired reminder notifications

Add automatic push notifications for behind partners (escalating: 24h then
every 3 days) and daily unpaired user reminders. Includes edge functions,
migrations, notification preference toggles, and UI improvements to pairing
and swipe screens.
This commit is contained in:
jjbrunton committed 2026-01-31 23:38:27 +00:00
1 parent 8a44fef764
commit c8aaa8967d
12 files changed
+1117 -266

No files matched your search

+139 -65
View File
@@ -7,6 +7,7 @@ import {
Alert,
ActivityIndicator,
Share,
Linking,
KeyboardAvoidingView,
Platform,
TouchableOpacity,
@@ -141,12 +142,14 @@ export default function PairingScreen() {
}
};
const shareMessage = couple?.invite_code
? `Join me on Sauci! Download the app at https://sauci.app and use my invite code to pair up: ${couple.invite_code}`
: "";
const shareCode = async () => {
if (couple?.invite_code) {
try {
await Share.share({
message: `Join me on Sauci! Download the app at https://sauci.app and use my invite code to pair up: ${couple.invite_code}`,
});
await Share.share({ message: shareMessage });
Events.codeShared();
} catch (error) {
console.error(error);
@@ -154,6 +157,23 @@ export default function PairingScreen() {
}
};
const shareViaWhatsApp = async () => {
const url = `whatsapp://send?text=${encodeURIComponent(shareMessage)}`;
const canOpen = await Linking.canOpenURL(url);
if (canOpen) {
await Linking.openURL(url);
Events.codeShared();
} else {
shareCode();
}
};
const shareViaSMS = async () => {
const url = `sms:&body=${encodeURIComponent(shareMessage)}`;
await Linking.openURL(url);
Events.codeShared();
};
const handleCancelPairing = async () => {
Alert.alert(
"Cancel Pairing",
@@ -256,7 +276,15 @@ export default function PairingScreen() {
</GlassCard>
</Animated.View>
{/* Share Button */}
{/* CTA Text */}
<Animated.View
entering={FadeInDown.delay(350).duration(500)}
style={styles.ctaSection}
>
<Text style={styles.ctaText}>Send this to your partner to get started</Text>
</Animated.View>
{/* Primary Share Button */}
<Animated.View
entering={FadeInDown.delay(400).duration(500)}
style={styles.section}
@@ -264,12 +292,28 @@ export default function PairingScreen() {
<GlassButton
onPress={shareCode}
fullWidth
icon={<Ionicons name="share-outline" size={20} color={colors.text} />}
size="lg"
icon={<Ionicons name="share-outline" size={22} color={colors.text} />}
>
Share Code
Share Invite Code
</GlassButton>
</Animated.View>
{/* Quick share buttons */}
<Animated.View
entering={FadeInDown.delay(450).duration(500)}
style={styles.quickShareRow}
>
<TouchableOpacity style={styles.quickShareButton} onPress={shareViaSMS} activeOpacity={0.7}>
<Ionicons name="chatbubble-outline" size={20} color={colors.text} />
<Text style={styles.quickShareLabel}>Messages</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.quickShareButton} onPress={shareViaWhatsApp} activeOpacity={0.7}>
<Ionicons name="logo-whatsapp" size={20} color={colors.text} />
<Text style={styles.quickShareLabel}>WhatsApp</Text>
</TouchableOpacity>
</Animated.View>
{/* Waiting indicator */}
<Animated.View
entering={FadeInDown.delay(500).duration(500)}
@@ -414,61 +458,61 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
},
backButton: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.backgroundLight,
justifyContent: "center",
alignItems: "center",
marginRight: spacing.md,
borderWidth: 1,
borderColor: colors.border,
},
// ...
input: {
backgroundColor: colors.background, // Flat background
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.border,
padding: spacing.md,
...typography.headline,
color: colors.text,
textAlign: "center",
letterSpacing: 4,
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
marginBottom: spacing.md,
},
// ...
dividerLine: {
flex: 1,
height: 1,
backgroundColor: colors.border,
},
// ...
copyIcon: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.background,
justifyContent: "center",
alignItems: "center",
marginLeft: spacing.md,
borderWidth: 1,
borderColor: colors.border,
},
// ...
waitingBadge: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.backgroundLight,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.full,
gap: spacing.sm,
borderWidth: 1,
borderColor: colors.border,
},
backButton: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.backgroundLight,
justifyContent: "center",
alignItems: "center",
marginRight: spacing.md,
borderWidth: 1,
borderColor: colors.border,
},
// ...
input: {
backgroundColor: colors.background, // Flat background
borderRadius: radius.md,
borderWidth: 1,
borderColor: colors.border,
padding: spacing.md,
...typography.headline,
color: colors.text,
textAlign: "center",
letterSpacing: 4,
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
marginBottom: spacing.md,
},
// ...
dividerLine: {
flex: 1,
height: 1,
backgroundColor: colors.border,
},
// ...
copyIcon: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.background,
justifyContent: "center",
alignItems: "center",
marginLeft: spacing.md,
borderWidth: 1,
borderColor: colors.border,
},
// ...
waitingBadge: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.backgroundLight,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.full,
gap: spacing.sm,
borderWidth: 1,
borderColor: colors.border,
},
title: {
...typography.title1,
color: colors.text,
@@ -508,13 +552,13 @@ const styles = StyleSheet.create({
marginBottom: spacing.sm,
marginLeft: spacing.xs,
},
divider: {
flexDirection: "row",
alignItems: "center",
marginVertical: spacing.lg,
},
dividerText: {
...typography.subhead,
color: colors.textTertiary,
@@ -531,7 +575,7 @@ const styles = StyleSheet.create({
letterSpacing: 6,
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
},
tapToCopy: {
...typography.caption1,
color: colors.textTertiary,
@@ -542,7 +586,7 @@ const styles = StyleSheet.create({
alignItems: "center",
marginTop: spacing.xl,
},
waitingText: {
...typography.subhead,
color: colors.textTertiary,
@@ -559,4 +603,34 @@ const styles = StyleSheet.create({
...typography.subhead,
color: colors.error,
},
ctaSection: {
alignItems: "center",
marginBottom: spacing.md,
},
ctaText: {
...typography.callout,
color: colors.textSecondary,
textAlign: "center",
},
quickShareRow: {
flexDirection: "row",
justifyContent: "center",
gap: spacing.md,
marginBottom: spacing.md,
},
quickShareButton: {
flexDirection: "row",
alignItems: "center",
gap: spacing.sm,
backgroundColor: colors.backgroundLight,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.full,
borderWidth: 1,
borderColor: colors.border,
},
quickShareLabel: {
...typography.subhead,
color: colors.text,
},
});