diff --git a/apps/mobile/src/components/share/QuestionShareCard.test.tsx b/apps/mobile/src/components/share/QuestionShareCard.test.tsx
new file mode 100644
index 0000000..546a629
--- /dev/null
+++ b/apps/mobile/src/components/share/QuestionShareCard.test.tsx
@@ -0,0 +1,28 @@
+import { render } from "@/test/test-utils";
+import { QuestionShareCard } from "./QuestionShareCard";
+
+describe("QuestionShareCard", () => {
+ it("renders the question, pack badge, and Sauci branding", () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText("What's your idea of a perfect date night?")).toBeTruthy();
+ expect(getByText("Date Night")).toBeTruthy();
+ expect(getByText("Sauci")).toBeTruthy();
+ expect(getByText("sauci.app")).toBeTruthy();
+ expect(getByText(/play it with your partner/i)).toBeTruthy();
+ });
+
+ it("never renders a couple join link — the pairing code is private", () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText(/join\//)).toBeNull();
+ });
+});
diff --git a/apps/mobile/src/components/share/QuestionShareCard.tsx b/apps/mobile/src/components/share/QuestionShareCard.tsx
new file mode 100644
index 0000000..e74e526
--- /dev/null
+++ b/apps/mobile/src/components/share/QuestionShareCard.tsx
@@ -0,0 +1,158 @@
+import { Image, StyleSheet, Text, View } from "react-native";
+import { LinearGradient } from "expo-linear-gradient";
+import { colors, gradients, radius, spacing, typography } from "../../theme";
+
+const logo = require("../../../assets/logo.png");
+
+interface QuestionShareCardProps {
+ question: {
+ text: string;
+ };
+ packName?: string;
+ cardColor?: string;
+ cardWidth: number;
+}
+
+/**
+ * The story card captured for outbound shares (Instagram Stories, Messages,
+ * share sheet). Carries the question and Sauci branding only — never the
+ * sharer's couple join code, which is a private pairing credential, not a
+ * referral link.
+ */
+export function QuestionShareCard({
+ question,
+ packName,
+ cardColor,
+ cardWidth,
+}: QuestionShareCardProps) {
+ const useGradient = !cardColor;
+
+ return (
+
+ {/* Background - solid color matching what the user saw, or gradient fallback */}
+ {useGradient ? (
+
+ ) : (
+
+ )}
+
+ {/* Subtle overlay for depth */}
+
+
+ {/* Header: wordmark + pack badge */}
+
+
+
+ Sauci
+
+ {packName ? (
+
+ {packName}
+
+ ) : null}
+
+
+ {/* Question */}
+
+ {question.text}
+
+
+ {/* Branded footer */}
+
+ Play it with your partner
+
+ sauci.app
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ aspectRatio: 0.7,
+ borderRadius: radius.xl,
+ overflow: "hidden",
+ padding: spacing.lg,
+ },
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
+ },
+ header: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ gap: spacing.sm,
+ },
+ wordmark: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: spacing.xs,
+ },
+ wordmarkLogo: {
+ width: 22,
+ height: 22,
+ resizeMode: "contain",
+ },
+ wordmarkText: {
+ ...typography.subhead,
+ color: colors.text,
+ fontWeight: "700",
+ },
+ packBadge: {
+ flexShrink: 1,
+ backgroundColor: "rgba(255, 255, 255, 0.2)",
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.xs,
+ borderRadius: radius.full,
+ },
+ packBadgeText: {
+ ...typography.caption1,
+ color: colors.text,
+ fontWeight: "600",
+ },
+ content: {
+ flex: 1,
+ justifyContent: "center",
+ paddingVertical: spacing.xl,
+ },
+ questionText: {
+ ...typography.title1,
+ color: colors.text,
+ fontWeight: "700",
+ lineHeight: 36,
+ },
+ footer: {
+ alignItems: "center",
+ gap: spacing.sm,
+ },
+ footerPrompt: {
+ ...typography.caption1,
+ color: "rgba(255, 255, 255, 0.85)",
+ fontWeight: "600",
+ letterSpacing: 1,
+ textTransform: "uppercase",
+ },
+ linkPill: {
+ backgroundColor: "rgba(255, 255, 255, 0.95)",
+ paddingHorizontal: spacing.lg,
+ paddingVertical: spacing.sm,
+ borderRadius: radius.full,
+ },
+ linkText: {
+ ...typography.subhead,
+ color: "#1a1a2e",
+ fontWeight: "700",
+ },
+});
diff --git a/apps/mobile/src/components/share/SharePreviewModal.tsx b/apps/mobile/src/components/share/SharePreviewModal.tsx
index b3fd6fe..bff969a 100644
--- a/apps/mobile/src/components/share/SharePreviewModal.tsx
+++ b/apps/mobile/src/components/share/SharePreviewModal.tsx
@@ -8,7 +8,6 @@ import {
Platform,
Share,
useWindowDimensions,
- Image,
} from "react-native";
import { BlurView } from "expo-blur";
import { LinearGradient } from "expo-linear-gradient";
@@ -22,10 +21,10 @@ import { Ionicons } from "@expo/vector-icons";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import ViewShot from "react-native-view-shot";
import * as Sharing from "expo-sharing";
-import { colors, gradients, radius, typography, spacing, blur } from "../../theme";
+import { colors, typography, spacing, blur } from "../../theme";
import { shareToInstagramStories } from "../../lib/instagramShare";
-
-const logo = require("../../../assets/logo.png");
+import { Events } from "../../lib/analytics";
+import { QuestionShareCard } from "./QuestionShareCard";
const MAX_CARD_WIDTH = 340;
@@ -55,6 +54,8 @@ export function SharePreviewModal({
const [capturedUri, setCapturedUri] = useState(null);
const [isCapturing, setIsCapturing] = useState(false);
+ const shareMessage = `${question.text}\n\nPlay it with your partner on https://sauci.app`;
+
// Capture the card when modal becomes visible
useEffect(() => {
if (visible && shareViewRef.current) {
@@ -87,7 +88,9 @@ export function SharePreviewModal({
if (!capturedUri) return;
const shared = await shareToInstagramStories(capturedUri);
- if (!shared) {
+ if (shared) {
+ Events.questionShared("instagram");
+ } else {
// Instagram Stories sharing unavailable - fall back to the share sheet
await handleMoreShare();
}
@@ -96,9 +99,8 @@ export function SharePreviewModal({
const handleMessagesShare = async () => {
if (!capturedUri) {
// Fallback to text
- await Share.share({
- message: `${question.text}\n\nDiscover more on sauci.app`,
- });
+ await Share.share({ message: shareMessage });
+ Events.questionShared("messages");
return;
}
@@ -109,22 +111,21 @@ export function SharePreviewModal({
mimeType: 'image/png',
UTI: 'public.png',
});
+ Events.questionShared("messages");
} else {
await handleMoreShare();
}
} catch (error) {
console.error('Messages share error:', error);
- await Share.share({
- message: `${question.text}\n\nDiscover more on sauci.app`,
- });
+ await Share.share({ message: shareMessage });
+ Events.questionShared("messages");
}
};
const handleMoreShare = async () => {
if (!capturedUri) {
- await Share.share({
- message: `${question.text}\n\nDiscover more on sauci.app`,
- });
+ await Share.share({ message: shareMessage });
+ Events.questionShared("more");
return;
}
@@ -136,10 +137,9 @@ export function SharePreviewModal({
dialogTitle: 'Share this question',
});
} else {
- await Share.share({
- message: `${question.text}\n\nDiscover more on sauci.app`,
- });
+ await Share.share({ message: shareMessage });
}
+ Events.questionShared("more");
} catch (error) {
console.error('Share error:', error);
}
@@ -202,7 +202,7 @@ export function SharePreviewModal({
result: 'tmpfile',
}}
>
-
- {/* Background - solid color matching what the user saw, or gradient fallback */}
- {useGradient ? (
-
- ) : (
-
- )}
-
- {/* Subtle overlay for depth */}
-
-
- {/* Pack name badge */}
- {packName && (
-
- {packName}
-
- )}
-
- {/* Content */}
-
-
- {question.text}
-
-
-
- {/* Branding with logo */}
-
-
- sauci.app
-
-
- );
-}
-
const styles = StyleSheet.create({
overlay: {
flex: 1,
@@ -418,82 +355,4 @@ const styles = StyleSheet.create({
color: colors.text,
fontWeight: '500',
},
- // Shareable card styles
- shareableCard: {
- aspectRatio: 0.7,
- borderRadius: radius.xl,
- overflow: 'hidden',
- padding: spacing.lg,
- },
- cardOverlay: {
- ...StyleSheet.absoluteFillObject,
- backgroundColor: 'rgba(0, 0, 0, 0.1)',
- },
- packBadge: {
- alignSelf: 'flex-start',
- backgroundColor: 'rgba(255, 255, 255, 0.2)',
- paddingHorizontal: spacing.md,
- paddingVertical: spacing.xs,
- borderRadius: radius.full,
- },
- packBadgeText: {
- ...typography.caption1,
- color: colors.text,
- fontWeight: '600',
- },
- shareableContent: {
- flex: 1,
- justifyContent: 'center',
- paddingVertical: spacing.xl,
- },
- questionText: {
- ...typography.title1,
- color: colors.text,
- fontWeight: '700',
- lineHeight: 36,
- },
- questionTextSmaller: {
- ...typography.title2,
- lineHeight: 30,
- },
- partnerSection: {
- marginTop: spacing.lg,
- },
- partnerDivider: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: spacing.sm,
- gap: spacing.sm,
- },
- partnerDividerLine: {
- flex: 1,
- height: 1,
- backgroundColor: 'rgba(255, 255, 255, 0.3)',
- },
- partnerLabel: {
- ...typography.caption1,
- color: 'rgba(255, 255, 255, 0.7)',
- fontWeight: '500',
- },
- partnerText: {
- ...typography.subhead,
- color: 'rgba(255, 255, 255, 0.8)',
- fontStyle: 'italic',
- },
- branding: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: spacing.sm,
- },
- brandingLogo: {
- width: 24,
- height: 24,
- resizeMode: 'contain',
- },
- brandingText: {
- ...typography.subhead,
- color: colors.text,
- fontWeight: '600',
- },
});
diff --git a/apps/mobile/src/components/share/index.ts b/apps/mobile/src/components/share/index.ts
index 1b98015..3657f63 100644
--- a/apps/mobile/src/components/share/index.ts
+++ b/apps/mobile/src/components/share/index.ts
@@ -1 +1,2 @@
-export { SharePreviewModal } from './SharePreviewModal';
+export { SharePreviewModal } from './SharePreviewModal';
+export { QuestionShareCard } from './QuestionShareCard';
diff --git a/apps/mobile/src/lib/analytics.ts b/apps/mobile/src/lib/analytics.ts
index be45d04..bec04ab 100644
--- a/apps/mobile/src/lib/analytics.ts
+++ b/apps/mobile/src/lib/analytics.ts
@@ -214,6 +214,8 @@ export const Events = {
questionAnswered: (answer: string, packId?: string) =>
logEvent("question_answered", { answer, pack_id: packId ?? "unknown" }),
questionSkipped: () => logEvent("question_skipped"),
+ questionShared: (destination: "instagram" | "messages" | "more") =>
+ logEvent("question_shared", { destination }),
matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }),
allQuestionsExhausted: () => logEvent("all_questions_exhausted"),
pendingQuestionsPromptViewed: (count: number) =>