Add question types, nudge feature, and share modal

- Add new question card components (photo, audio, text, who-likely)
- Add share preview modal for matches
- Add nudge notification system with rate limiting
- Add session-based notification improvements
- Add match confetti celebration animation
- Update onboarding flow with paywall tracking
- Remove deprecated swipe card and tutorial components
- Update GlassCard/GlassInput to use solid backgrounds
- Fix routing issues after packs screen removal
This commit is contained in:
jjbrunton committed 2026-01-25 19:17:39 +00:00
1 parent 8612b16b93
commit 1daf386765
114 files changed
+14877 -7911

No files matched your search

+8 -8
View File
@@ -60,14 +60,14 @@ export async function getCachedSignedUrl(
/**
* Extract storage path from media_path (handles both old full URLs and new path-only format)
*/
export function getStoragePath(mediaPath: string): string {
if (mediaPath.startsWith('http')) {
// Old format: extract path from full URL
const match = mediaPath.match(/\/chat-media\/(.+)$/);
return match ? match[1] : mediaPath;
}
return mediaPath;
}
export function getStoragePath(mediaPath: string): string {
if (mediaPath.startsWith('http')) {
// Old format: extract path from full URL
const match = mediaPath.match(/\/(chat-media|response-media)\/(.+)$/);
return match ? match[2] : mediaPath;
}
return mediaPath;
}
/**
* Clear expired URLs from the cache to free memory.
+44 -29
View File
@@ -5,6 +5,7 @@ import { router } from "expo-router";
import { supabase } from "./supabase";
import { captureError } from "./sentry";
import { Events } from "./analytics";
import { useMatchStore } from "../store/matchStore";
// Configure notification behavior for foreground
// We don't show system alerts since the app handles notifications with in-app toasts
@@ -17,13 +18,13 @@ Notifications.setNotificationHandler({
});
// Types
export interface NotificationData {
type: "match" | "message" | "match_digest";
match_id?: string;
message_id?: string;
count?: number;
}
export interface NotificationData {
type: "match" | "message" | "match_digest" | "partner_activity" | "nudge";
match_id?: string;
message_id?: string;
count?: number;
}
/**
* Request notification permissions and get Expo push token.
@@ -221,28 +222,42 @@ export async function checkAndRegisterPushToken(userId: string): Promise<boolean
/**
* Handle notification tap - navigate to appropriate screen.
*/
export function handleNotificationResponse(
response: Notifications.NotificationResponse
): void {
const data = response.notification.request.content.data as NotificationData;
if (!data) {
return;
}
if (data.type === "match_digest") {
router.push("/(app)/matches");
return;
}
if (!data.match_id) {
return;
}
// Both match and message notifications navigate to chat
router.push(`/(app)/chat/${data.match_id}`);
}
export function handleNotificationResponse(
response: Notifications.NotificationResponse
): void {
const data = response.notification.request.content.data as NotificationData;
if (!data) {
return;
}
if (data.type === "match_digest") {
router.push("/(app)/matches");
return;
}
if (data.type === "nudge") {
// Navigate to Matches > Your Turn tab
useMatchStore.getState().setCurrentView('pending');
router.push("/(app)/matches");
return;
}
if (data.type === "partner_activity") {
// Navigate to Matches > Your Turn tab (questions to answer)
useMatchStore.getState().setCurrentView('pending');
router.push("/(app)/matches");
return;
}
if (!data.match_id) {
return;
}
// Both match and message notifications navigate to chat
router.push(`/(app)/chat/${data.match_id}`);
}
/**
* Set up listener for notification taps (when app is running).
@@ -0,0 +1,49 @@
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";
const STORAGE_KEY_PREFIX = "onboarding_paywall_seen";
const buildKey = (userId: string): string => `${STORAGE_KEY_PREFIX}_${userId}`;
async function getStorage(key: string): Promise<boolean> {
try {
let data: string | null = null;
if (Platform.OS === "web") {
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
data = window.localStorage.getItem(key);
}
} else {
data = await SecureStore.getItemAsync(key);
}
return data === "true";
} catch (error) {
console.error("Error reading onboarding paywall state:", error);
}
return false;
}
async function setStorage(key: string, seen: boolean): Promise<void> {
try {
const data = seen ? "true" : "false";
if (Platform.OS === "web") {
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
window.localStorage.setItem(key, data);
}
} else {
await SecureStore.setItemAsync(key, data);
}
} catch (error) {
console.error("Error saving onboarding paywall state:", error);
}
}
export async function hasSeenOnboardingPaywall(userId: string): Promise<boolean> {
return getStorage(buildKey(userId));
}
export async function markOnboardingPaywallSeen(userId: string): Promise<void> {
await setStorage(buildKey(userId), true);
}
+11 -2
View File
@@ -145,7 +145,7 @@ class RevenueCatService {
return result.current;
}
async getOfferingsDebug(): Promise<{
async getOfferingsDebug(offeringIdentifier?: string): Promise<{
current: PurchasesOffering | null;
availableOfferings: string[] | null;
error: string | null;
@@ -162,8 +162,17 @@ class RevenueCatService {
const offerings = await this.Purchases.getOfferings();
const availableOfferings = Object.keys(offerings.all || {});
// If a specific offering is requested, try to get it
let targetOffering = offerings.current;
if (offeringIdentifier && offerings.all?.[offeringIdentifier]) {
console.log(`RevenueCat: Using offering "${offeringIdentifier}"`);
targetOffering = offerings.all[offeringIdentifier];
} else if (offeringIdentifier) {
console.log(`RevenueCat: Offering "${offeringIdentifier}" not found, falling back to current`);
}
return {
current: offerings.current,
current: targetOffering,
availableOfferings,
error: null
};
-51
View File
@@ -1,51 +0,0 @@
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";
const STORAGE_KEY = "swipe_tutorial_seen";
async function getStorage(): Promise<boolean> {
try {
let data: string | null = null;
if (Platform.OS === "web") {
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
data = window.localStorage.getItem(STORAGE_KEY);
}
} else {
data = await SecureStore.getItemAsync(STORAGE_KEY);
}
return data === "true";
} catch (error) {
console.error("Error reading swipe tutorial state:", error);
}
return false;
}
async function setStorage(seen: boolean): Promise<void> {
try {
const data = seen ? "true" : "false";
if (Platform.OS === "web") {
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
window.localStorage.setItem(STORAGE_KEY, data);
}
} else {
await SecureStore.setItemAsync(STORAGE_KEY, data);
}
} catch (error) {
console.error("Error saving swipe tutorial state:", error);
}
}
export async function hasSeenSwipeTutorial(): Promise<boolean> {
return getStorage();
}
export async function markSwipeTutorialSeen(): Promise<void> {
await setStorage(true);
}
export async function resetSwipeTutorial(): Promise<void> {
await setStorage(false);
}