mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 18:19:44 +00:00
Compare commits
7
Commits
v1.0.1
..
12f312421b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12f312421b | ||
|
|
a558f5e67e | ||
|
|
0be833bba2 | ||
|
|
1911f8f058 | ||
|
|
23e24aa4c2 | ||
|
|
e671ae9d1c | ||
|
|
a7a13c9614 |
No files matched your search
@@ -25,7 +25,7 @@
|
|||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@sauci/shared": "workspace:*",
|
"@sauci/shared": "file:../../packages/shared",
|
||||||
"@supabase/supabase-js": "^2.39.0",
|
"@supabase/supabase-js": "^2.39.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const PERMISSION_KEYS = {
|
|||||||
VIEW_RESPONSES: 'view_responses',
|
VIEW_RESPONSES: 'view_responses',
|
||||||
VIEW_ACTIVITY: 'view_activity',
|
VIEW_ACTIVITY: 'view_activity',
|
||||||
MANAGE_FEEDBACK: 'manage_feedback',
|
MANAGE_FEEDBACK: 'manage_feedback',
|
||||||
|
GIFT_PREMIUM: 'gift_premium',
|
||||||
// System permissions
|
// System permissions
|
||||||
MANAGE_CODES: 'manage_codes',
|
MANAGE_CODES: 'manage_codes',
|
||||||
MANAGE_ADMINS: 'manage_admins',
|
MANAGE_ADMINS: 'manage_admins',
|
||||||
@@ -42,6 +43,7 @@ export const PERMISSION_METADATA: Record<PermissionKey, { label: string; descrip
|
|||||||
[PERMISSION_KEYS.VIEW_RESPONSES]: { label: 'Can view responses', description: 'View user question responses', group: 'users' },
|
[PERMISSION_KEYS.VIEW_RESPONSES]: { label: 'Can view responses', description: 'View user question responses', group: 'users' },
|
||||||
[PERMISSION_KEYS.VIEW_ACTIVITY]: { label: 'Can view activity', description: 'View user activity feed (responses, matches, messages, signups)', group: 'users' },
|
[PERMISSION_KEYS.VIEW_ACTIVITY]: { label: 'Can view activity', description: 'View user activity feed (responses, matches, messages, signups)', group: 'users' },
|
||||||
[PERMISSION_KEYS.MANAGE_FEEDBACK]: { label: 'Can manage feedback', description: 'View and respond to user feedback', group: 'users' },
|
[PERMISSION_KEYS.MANAGE_FEEDBACK]: { label: 'Can manage feedback', description: 'View and respond to user feedback', group: 'users' },
|
||||||
|
[PERMISSION_KEYS.GIFT_PREMIUM]: { label: 'Can gift premium', description: 'Manually upgrade users to premium status', group: 'users' },
|
||||||
[PERMISSION_KEYS.MANAGE_CODES]: { label: 'Can manage redemption codes', description: 'Create and view redemption codes', group: 'system' },
|
[PERMISSION_KEYS.MANAGE_CODES]: { label: 'Can manage redemption codes', description: 'Create and view redemption codes', group: 'system' },
|
||||||
[PERMISSION_KEYS.MANAGE_ADMINS]: { label: 'Can manage admins', description: 'Add/remove admin users and change permissions', group: 'system' },
|
[PERMISSION_KEYS.MANAGE_ADMINS]: { label: 'Can manage admins', description: 'Add/remove admin users and change permissions', group: 'system' },
|
||||||
[PERMISSION_KEYS.MANAGE_AI_CONFIG]: { label: 'Can manage AI settings', description: 'Configure AI models, API keys, and council mode', group: 'system' },
|
[PERMISSION_KEYS.MANAGE_AI_CONFIG]: { label: 'Can manage AI settings', description: 'Configure AI models, API keys, and council mode', group: 'system' },
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ interface ResponseActivity {
|
|||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
pack: {
|
pack: {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
} | null;
|
} | null;
|
||||||
} | null;
|
} | null;
|
||||||
@@ -155,7 +156,7 @@ export function UserActivityPage() {
|
|||||||
profile:profiles!responses_user_id_fkey(id, name, email),
|
profile:profiles!responses_user_id_fkey(id, name, email),
|
||||||
question:questions(
|
question:questions(
|
||||||
id, text,
|
id, text,
|
||||||
pack:question_packs(name)
|
pack:question_packs(id, name)
|
||||||
)
|
)
|
||||||
`, { count: 'exact' })
|
`, { count: 'exact' })
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
@@ -317,18 +318,22 @@ export function UserActivityPage() {
|
|||||||
const fetchSignups = useCallback(async () => {
|
const fetchSignups = useCallback(async () => {
|
||||||
setLoading(prev => ({ ...prev, signups: true }));
|
setLoading(prev => ({ ...prev, signups: true }));
|
||||||
try {
|
try {
|
||||||
const from = (signupsPage - 1) * signupsPageSize;
|
// Use RPC to get profiles with auth info (includes email from auth.users)
|
||||||
const to = from + signupsPageSize - 1;
|
const { data: allData, error } = await supabase
|
||||||
|
.rpc('get_profiles_with_auth_info');
|
||||||
const { data, error, count } = await supabase
|
|
||||||
.from('profiles')
|
|
||||||
.select('id, name, email, avatar_url, created_at, onboarding_completed, couple_id', { count: 'exact' })
|
|
||||||
.order('created_at', { ascending: false })
|
|
||||||
.range(from, to);
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setSignups(data || []);
|
|
||||||
setSignupsTotal(count || 0);
|
// Sort by created_at descending and apply pagination
|
||||||
|
const sorted = (allData || []).sort((a: SignupActivity, b: SignupActivity) =>
|
||||||
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
const from = (signupsPage - 1) * signupsPageSize;
|
||||||
|
const to = from + signupsPageSize;
|
||||||
|
|
||||||
|
setSignups(sorted.slice(from, to));
|
||||||
|
setSignupsTotal(sorted.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load signups:', error);
|
console.error('Failed to load signups:', error);
|
||||||
toast.error("Failed to load signups");
|
toast.error("Failed to load signups");
|
||||||
@@ -502,14 +507,31 @@ export function UserActivityPage() {
|
|||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>
|
||||||
<TableCell>{renderUserLink(item.profile)}</TableCell>
|
<TableCell>{renderUserLink(item.profile)}</TableCell>
|
||||||
<TableCell className="max-w-[300px]">
|
<TableCell className="max-w-[300px]">
|
||||||
<span className="truncate block">
|
{item.question?.pack?.id ? (
|
||||||
{item.question?.text || 'Unknown question'}
|
<Link
|
||||||
</span>
|
to={`/packs/${item.question.pack.id}/questions`}
|
||||||
|
className="truncate block text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{item.question?.text || 'Unknown question'}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="truncate block">
|
||||||
|
{item.question?.text || 'Unknown question'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline">
|
{item.question?.pack?.id ? (
|
||||||
{item.question?.pack?.name || 'Unknown'}
|
<Link to={`/packs/${item.question.pack.id}/questions`}>
|
||||||
</Badge>
|
<Badge variant="outline" className="hover:bg-primary/10 cursor-pointer">
|
||||||
|
{item.question.pack.name}
|
||||||
|
</Badge>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">
|
||||||
|
{item.question?.pack?.name || 'Unknown'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{renderAnswerBadge(item.answer)}</TableCell>
|
<TableCell>{renderAnswerBadge(item.answer)}</TableCell>
|
||||||
<TableCell className="text-muted-foreground text-sm whitespace-nowrap">
|
<TableCell className="text-muted-foreground text-sm whitespace-nowrap">
|
||||||
|
|||||||
@@ -38,14 +38,17 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Crown, Users, MessageCircle, ChevronRight, ThumbsUp, ThumbsDown, Minus, Gift, Image, Video as VideoIcon, Target, User, Eye, EyeOff, CheckCircle } from 'lucide-react';
|
import { Crown, Users, MessageCircle, ChevronRight, ThumbsUp, ThumbsDown, Minus, Gift, Image, Video as VideoIcon, Target, User, Eye, EyeOff, CheckCircle, Package, Heart, Sparkles } from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { IconPreview } from '@/components/ui/icon-picker';
|
||||||
|
|
||||||
type UsageReason = 'improve_communication' | 'spice_up_intimacy' | 'deeper_connection' | 'have_fun' | 'strengthen_relationship';
|
type UsageReason = 'improve_communication' | 'spice_up_intimacy' | 'deeper_connection' | 'have_fun' | 'strengthen_relationship';
|
||||||
type Gender = 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
|
type Gender = 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
|
||||||
|
|
||||||
// ... (interfaces and constants are the same)
|
// ... (interfaces and constants are the same)
|
||||||
|
|
||||||
|
type IntensityLevel = 1 | 2 | 3 | 4 | 5;
|
||||||
|
|
||||||
interface Profile {
|
interface Profile {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
@@ -58,6 +61,7 @@ interface Profile {
|
|||||||
usage_reason: UsageReason | null;
|
usage_reason: UsageReason | null;
|
||||||
show_explicit_content: boolean | null;
|
show_explicit_content: boolean | null;
|
||||||
onboarding_completed: boolean | null;
|
onboarding_completed: boolean | null;
|
||||||
|
max_intensity: IntensityLevel | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usageReasonLabels: Record<UsageReason, { label: string; icon: string }> = {
|
const usageReasonLabels: Record<UsageReason, { label: string; icon: string }> = {
|
||||||
@@ -75,6 +79,14 @@ const genderLabels: Record<Gender, string> = {
|
|||||||
'prefer-not-to-say': 'Prefer not to say',
|
'prefer-not-to-say': 'Prefer not to say',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const intensityLabels: Record<IntensityLevel, { label: string; emoji: string }> = {
|
||||||
|
1: { label: 'Gentle', emoji: '💭' },
|
||||||
|
2: { label: 'Warm', emoji: '💕' },
|
||||||
|
3: { label: 'Playful', emoji: '😏' },
|
||||||
|
4: { label: 'Steamy', emoji: '🔥' },
|
||||||
|
5: { label: 'Intense', emoji: '🌶️' },
|
||||||
|
};
|
||||||
|
|
||||||
interface Partner {
|
interface Partner {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
@@ -88,6 +100,7 @@ interface Response {
|
|||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
pack: {
|
pack: {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -105,6 +118,30 @@ interface Match {
|
|||||||
message_count?: number;
|
message_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EnabledPack {
|
||||||
|
pack_id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
created_at: string | null;
|
||||||
|
pack: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
icon: string | null;
|
||||||
|
is_premium: boolean;
|
||||||
|
is_explicit: boolean;
|
||||||
|
category: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeatureInterest {
|
||||||
|
id: string;
|
||||||
|
feature_name: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
const answerIcons = {
|
const answerIcons = {
|
||||||
yes: <ThumbsUp className="h-4 w-4 text-green-500" />,
|
yes: <ThumbsUp className="h-4 w-4 text-green-500" />,
|
||||||
no: <ThumbsDown className="h-4 w-4 text-red-500" />,
|
no: <ThumbsDown className="h-4 w-4 text-red-500" />,
|
||||||
@@ -238,6 +275,8 @@ export function UserDetailPage() {
|
|||||||
const [responses, setResponses] = useState<Response[]>([]);
|
const [responses, setResponses] = useState<Response[]>([]);
|
||||||
const [matches, setMatches] = useState<Match[]>([]);
|
const [matches, setMatches] = useState<Match[]>([]);
|
||||||
const [mediaMessages, setMediaMessages] = useState<any[]>([]);
|
const [mediaMessages, setMediaMessages] = useState<any[]>([]);
|
||||||
|
const [enabledPacks, setEnabledPacks] = useState<EnabledPack[]>([]);
|
||||||
|
const [featureInterests, setFeatureInterests] = useState<FeatureInterest[]>([]);
|
||||||
const [responsesPage, setResponsesPage] = useState(1);
|
const [responsesPage, setResponsesPage] = useState(1);
|
||||||
const [responsesPageSize, setResponsesPageSize] = useState(10);
|
const [responsesPageSize, setResponsesPageSize] = useState(10);
|
||||||
const [responsesTotal, setResponsesTotal] = useState(0);
|
const [responsesTotal, setResponsesTotal] = useState(0);
|
||||||
@@ -282,17 +321,13 @@ export function UserDetailPage() {
|
|||||||
}
|
}
|
||||||
expiresAt = date.toISOString();
|
expiresAt = date.toISOString();
|
||||||
}
|
}
|
||||||
const { error } = await auditedSupabase.insert('subscriptions', {
|
// Use RPC function which handles permissions internally
|
||||||
user_id: userId,
|
const { error } = await supabase.rpc('admin_gift_premium', {
|
||||||
revenuecat_app_user_id: 'admin_grant',
|
target_user_id: userId,
|
||||||
product_id: 'admin_premium',
|
expires_at_param: expiresAt,
|
||||||
status: 'active',
|
|
||||||
store: 'manual',
|
|
||||||
purchased_at: new Date().toISOString(),
|
|
||||||
expires_at: expiresAt,
|
|
||||||
});
|
});
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
const { data: updatedProfile } = await supabase.from('profiles').select('*').eq('id', userId).single();
|
const { data: updatedProfile } = await supabase.rpc('get_profile_with_auth_info', { user_id: userId }).single();
|
||||||
if (updatedProfile) setProfile(updatedProfile);
|
if (updatedProfile) setProfile(updatedProfile);
|
||||||
setUpgradeOpen(false);
|
setUpgradeOpen(false);
|
||||||
alert('User upgraded to premium successfully!');
|
alert('User upgraded to premium successfully!');
|
||||||
@@ -308,9 +343,17 @@ export function UserDetailPage() {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data: profileData } = await supabase.from('profiles').select('*').eq('id', userId).single();
|
const { data: profileData } = await supabase.rpc('get_profile_with_auth_info', { user_id: userId }).single();
|
||||||
setProfile(profileData);
|
setProfile(profileData);
|
||||||
|
|
||||||
|
// Fetch feature interests for the user
|
||||||
|
const { data: featureInterestsData } = await supabase
|
||||||
|
.from('feature_interests')
|
||||||
|
.select('id, feature_name, created_at')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
setFeatureInterests((featureInterestsData || []) as FeatureInterest[]);
|
||||||
|
|
||||||
if (profileData?.couple_id) {
|
if (profileData?.couple_id) {
|
||||||
const { data: partnerData } = await supabase.from('profiles').select('id, name').eq('couple_id', profileData.couple_id).neq('id', userId).single();
|
const { data: partnerData } = await supabase.from('profiles').select('id, name').eq('couple_id', profileData.couple_id).neq('id', userId).single();
|
||||||
setPartner(partnerData);
|
setPartner(partnerData);
|
||||||
@@ -323,7 +366,7 @@ export function UserDetailPage() {
|
|||||||
|
|
||||||
const { data: responseData, count: responsesCount, error: responsesError } = await supabase
|
const { data: responseData, count: responsesCount, error: responsesError } = await supabase
|
||||||
.from('responses')
|
.from('responses')
|
||||||
.select('id, answer, created_at, question:questions(id, text, pack:question_packs(name))', { count: 'exact' })
|
.select('id, answer, created_at, question:questions(id, text, pack:question_packs(id, name))', { count: 'exact' })
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.range(responsesFrom, responsesTo);
|
.range(responsesFrom, responsesTo);
|
||||||
@@ -388,11 +431,39 @@ export function UserDetailPage() {
|
|||||||
setMediaMessages([]);
|
setMediaMessages([]);
|
||||||
setMediaTotal(0);
|
setMediaTotal(0);
|
||||||
}
|
}
|
||||||
|
// Fetch enabled packs for the couple
|
||||||
|
const { data: enabledPacksData, error: packsError } = await supabase
|
||||||
|
.from('couple_packs')
|
||||||
|
.select(`
|
||||||
|
pack_id,
|
||||||
|
enabled,
|
||||||
|
created_at,
|
||||||
|
pack:question_packs(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
is_premium,
|
||||||
|
is_explicit,
|
||||||
|
category:categories(id, name)
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.eq('couple_id', profileData.couple_id)
|
||||||
|
.eq('enabled', true)
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (packsError) {
|
||||||
|
console.error("Error fetching enabled packs:", packsError);
|
||||||
|
setEnabledPacks([]);
|
||||||
|
} else {
|
||||||
|
setEnabledPacks((enabledPacksData || []) as unknown as EnabledPack[]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setMatches([]);
|
setMatches([]);
|
||||||
setMatchesTotal(0);
|
setMatchesTotal(0);
|
||||||
setMediaMessages([]);
|
setMediaMessages([]);
|
||||||
setMediaTotal(0);
|
setMediaTotal(0);
|
||||||
|
setEnabledPacks([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load user data:', error);
|
console.error('Failed to load user data:', error);
|
||||||
@@ -446,7 +517,7 @@ export function UserDetailPage() {
|
|||||||
<h1 className="text-2xl font-bold">{profile.name || 'Unnamed User'}</h1>
|
<h1 className="text-2xl font-bold">{profile.name || 'Unnamed User'}</h1>
|
||||||
<RealtimeStatusIndicator status={profileStatus === 'SUBSCRIBED' && responsesStatus === 'SUBSCRIBED' ? 'SUBSCRIBED' : profileStatus} showLabel />
|
<RealtimeStatusIndicator status={profileStatus === 'SUBSCRIBED' && responsesStatus === 'SUBSCRIBED' ? 'SUBSCRIBED' : profileStatus} showLabel />
|
||||||
{profile.is_premium && <Badge className="bg-amber-500"><Crown className="h-3 w-3 mr-1" />Premium</Badge>}
|
{profile.is_premium && <Badge className="bg-amber-500"><Crown className="h-3 w-3 mr-1" />Premium</Badge>}
|
||||||
{!profile.is_premium && <Dialog open={upgradeOpen} onOpenChange={setUpgradeOpen}><DialogTrigger asChild><Button size="sm" variant="outline" className="ml-2 gap-1 h-7"><Gift className="h-3.5 w-3.5" />Gift Premium</Button></DialogTrigger><DialogContent><DialogHeader><DialogTitle>Gift Premium Access</DialogTitle><DialogDescription>Manually upgrade this user to premium status.</DialogDescription></DialogHeader><div className="grid gap-4 py-4"><div className="grid gap-2"><Label htmlFor="expiry-type">Duration</Label><Select value={expiryType} onValueChange={setExpiryType}><SelectTrigger id="expiry-type"><SelectValue placeholder="Select duration" /></SelectTrigger><SelectContent><SelectItem value="forever">Lifetime (No Expiry)</SelectItem><SelectItem value="1_month">1 Month</SelectItem><SelectItem value="1_year">1 Year</SelectItem><SelectItem value="custom">Custom Date</SelectItem></SelectContent></Select></div>{expiryType === 'custom' && (<div className="grid gap-2"><Label htmlFor="custom-date">Expiry Date</Label><Input id="custom-date" type="datetime-local" value={customDate} onChange={(e) => setCustomDate(e.target.value)} /></div>)}</div><DialogFooter><Button variant="outline" onClick={() => setUpgradeOpen(false)}>Cancel</Button><Button onClick={handleUpgrade} disabled={upgrading}>{upgrading ? 'Upgrading...' : 'Confirm Upgrade'}</Button></DialogFooter></DialogContent></Dialog>}
|
{!profile.is_premium && hasPermission(PERMISSION_KEYS.GIFT_PREMIUM) && <Dialog open={upgradeOpen} onOpenChange={setUpgradeOpen}><DialogTrigger asChild><Button size="sm" variant="outline" className="ml-2 gap-1 h-7"><Gift className="h-3.5 w-3.5" />Gift Premium</Button></DialogTrigger><DialogContent><DialogHeader><DialogTitle>Gift Premium Access</DialogTitle><DialogDescription>Manually upgrade this user to premium status.</DialogDescription></DialogHeader><div className="grid gap-4 py-4"><div className="grid gap-2"><Label htmlFor="expiry-type">Duration</Label><Select value={expiryType} onValueChange={setExpiryType}><SelectTrigger id="expiry-type"><SelectValue placeholder="Select duration" /></SelectTrigger><SelectContent><SelectItem value="forever">Lifetime (No Expiry)</SelectItem><SelectItem value="1_month">1 Month</SelectItem><SelectItem value="1_year">1 Year</SelectItem><SelectItem value="custom">Custom Date</SelectItem></SelectContent></Select></div>{expiryType === 'custom' && (<div className="grid gap-2"><Label htmlFor="custom-date">Expiry Date</Label><Input id="custom-date" type="datetime-local" value={customDate} onChange={(e) => setCustomDate(e.target.value)} /></div>)}</div><DialogFooter><Button variant="outline" onClick={() => setUpgradeOpen(false)}>Cancel</Button><Button onClick={handleUpgrade} disabled={upgrading}>{upgrading ? 'Upgrading...' : 'Confirm Upgrade'}</Button></DialogFooter></DialogContent></Dialog>}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground mb-4">{profile.email || 'No email'}</p>
|
<p className="text-muted-foreground mb-4">{profile.email || 'No email'}</p>
|
||||||
<div className="flex gap-6 text-sm">
|
<div className="flex gap-6 text-sm">
|
||||||
@@ -456,20 +527,46 @@ export function UserDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div></CardContent></Card>
|
</div></CardContent></Card>
|
||||||
{/* Onboarding Details */}
|
{/* Onboarding Details */}
|
||||||
<Card><CardHeader className="pb-3"><CardTitle className="text-lg flex items-center gap-2"><CheckCircle className="h-5 w-5 text-primary" />Onboarding Details</CardTitle><CardDescription>Information collected during user onboarding</CardDescription></CardHeader><CardContent><div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<Card><CardHeader className="pb-3"><CardTitle className="text-lg flex items-center gap-2"><CheckCircle className="h-5 w-5 text-primary" />Onboarding Details</CardTitle><CardDescription>Information collected during user onboarding</CardDescription></CardHeader><CardContent><div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
|
||||||
<div className="flex items-start gap-3"><div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900"><User className="h-4 w-4 text-blue-600 dark:text-blue-400" /></div><div><p className="text-sm font-medium text-muted-foreground">Gender</p><p className="font-medium">{profile.gender ? genderLabels[profile.gender] : 'Not set'}</p></div></div>
|
<div className="flex items-start gap-3"><div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900"><User className="h-4 w-4 text-blue-600 dark:text-blue-400" /></div><div><p className="text-sm font-medium text-muted-foreground">Gender</p><p className="font-medium">{profile.gender ? genderLabels[profile.gender] : 'Not set'}</p></div></div>
|
||||||
<div className="flex items-start gap-3"><div className="p-2 rounded-lg bg-purple-100 dark:bg-purple-900"><Target className="h-4 w-4 text-purple-600 dark:text-purple-400" /></div><div><p className="text-sm font-medium text-muted-foreground">Why using Sauci</p><p className="font-medium">{profile.usage_reason ? usageReasonLabels[profile.usage_reason]?.label : 'Not set'}</p></div></div>
|
<div className="flex items-start gap-3"><div className="p-2 rounded-lg bg-purple-100 dark:bg-purple-900"><Target className="h-4 w-4 text-purple-600 dark:text-purple-400" /></div><div><p className="text-sm font-medium text-muted-foreground">Why using Sauci</p><p className="font-medium">{profile.usage_reason ? usageReasonLabels[profile.usage_reason]?.label : 'Not set'}</p></div></div>
|
||||||
|
<div className="flex items-start gap-3"><div className="p-2 rounded-lg bg-pink-100 dark:bg-pink-900"><Heart className="h-4 w-4 text-pink-600 dark:text-pink-400" /></div><div><p className="text-sm font-medium text-muted-foreground">Comfort Zone</p><p className="font-medium">{profile.max_intensity ? `${intensityLabels[profile.max_intensity].emoji} ${intensityLabels[profile.max_intensity].label} (${profile.max_intensity})` : 'Not set'}</p></div></div>
|
||||||
<div className="flex items-start gap-3"><div className={`p-2 rounded-lg ${profile.show_explicit_content ? 'bg-amber-100 dark:bg-amber-900' : 'bg-gray-100 dark:bg-gray-800'}`}>{profile.show_explicit_content ? <Eye className="h-4 w-4 text-amber-600 dark:text-amber-400" /> : <EyeOff className="h-4 w-4 text-gray-500 dark:text-gray-400" />}</div><div><p className="text-sm font-medium text-muted-foreground">Explicit Content</p><p className="font-medium">{profile.show_explicit_content === null ? 'Not set' : profile.show_explicit_content ? 'Enabled' : 'Disabled'}</p></div></div>
|
<div className="flex items-start gap-3"><div className={`p-2 rounded-lg ${profile.show_explicit_content ? 'bg-amber-100 dark:bg-amber-900' : 'bg-gray-100 dark:bg-gray-800'}`}>{profile.show_explicit_content ? <Eye className="h-4 w-4 text-amber-600 dark:text-amber-400" /> : <EyeOff className="h-4 w-4 text-gray-500 dark:text-gray-400" />}</div><div><p className="text-sm font-medium text-muted-foreground">Explicit Content</p><p className="font-medium">{profile.show_explicit_content === null ? 'Not set' : profile.show_explicit_content ? 'Enabled' : 'Disabled'}</p></div></div>
|
||||||
<div className="flex items-start gap-3"><div className={`p-2 rounded-lg ${profile.onboarding_completed ? 'bg-green-100 dark:bg-green-900' : 'bg-red-100 dark:bg-red-900'}`}><CheckCircle className={`h-4 w-4 ${profile.onboarding_completed ? 'text-green-600 dark:text-green-400' : 'text-red-500 dark:text-red-400'}`} /></div><div><p className="text-sm font-medium text-muted-foreground">Onboarding</p><p className="font-medium">{profile.onboarding_completed ? 'Completed' : 'Not completed'}</p></div></div>
|
<div className="flex items-start gap-3"><div className={`p-2 rounded-lg ${profile.onboarding_completed ? 'bg-green-100 dark:bg-green-900' : 'bg-red-100 dark:bg-red-900'}`}><CheckCircle className={`h-4 w-4 ${profile.onboarding_completed ? 'text-green-600 dark:text-green-400' : 'text-red-500 dark:text-red-400'}`} /></div><div><p className="text-sm font-medium text-muted-foreground">Onboarding</p><p className="font-medium">{profile.onboarding_completed ? 'Completed' : 'Not completed'}</p></div></div>
|
||||||
</div></CardContent></Card>
|
</div></CardContent></Card>
|
||||||
|
{/* Feature Opt-Ins */}
|
||||||
|
{featureInterests.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Sparkles className="h-5 w-5 text-primary" />
|
||||||
|
Feature Opt-Ins
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Features the user has expressed interest in</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{featureInterests.map((interest) => (
|
||||||
|
<Badge key={interest.id} variant="secondary" className="capitalize">
|
||||||
|
{interest.feature_name}
|
||||||
|
<span className="ml-1 text-xs text-muted-foreground">
|
||||||
|
({format(new Date(interest.created_at), 'MMM d')})
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<Tabs defaultValue={canViewResponses ? "responses" : canViewMatches ? "matches" : canViewMedia ? "media" : "responses"}>
|
<Tabs defaultValue={canViewResponses ? "responses" : canViewMatches ? "matches" : canViewMedia ? "media" : "responses"}>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{canViewResponses && <TabsTrigger value="responses">Responses ({responsesTotal})</TabsTrigger>}
|
{canViewResponses && <TabsTrigger value="responses">Responses ({responsesTotal})</TabsTrigger>}
|
||||||
{canViewMatches && <TabsTrigger value="matches" disabled={!profile.couple_id}>Matches ({matchesTotal})</TabsTrigger>}
|
{canViewMatches && <TabsTrigger value="matches" disabled={!profile.couple_id}>Matches ({matchesTotal})</TabsTrigger>}
|
||||||
{canViewMedia && <TabsTrigger value="media">Media ({mediaTotal})</TabsTrigger>}
|
{canViewMedia && <TabsTrigger value="media">Media ({mediaTotal})</TabsTrigger>}
|
||||||
|
<TabsTrigger value="packs" disabled={!profile.couple_id}>Enabled Packs ({enabledPacks.length})</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{canViewResponses && <TabsContent value="responses" className="mt-4">{responses.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><p className="text-muted-foreground">No responses yet</p></Card> : <div className="space-y-4"><div className="rounded-md border"><Table><TableHeader><TableRow><TableHead>Question</TableHead><TableHead>Pack</TableHead><TableHead className="w-24">Answer</TableHead><TableHead className="w-32">Date</TableHead></TableRow></TableHeader><TableBody>{responses.map((r) => (<TableRow key={r.id}><TableCell className="max-w-md"><span className="line-clamp-2">{r.question.text}</span></TableCell><TableCell className="text-muted-foreground">{r.question.pack?.name || '—'}</TableCell><TableCell><div className="flex items-center gap-2">{answerIcons[r.answer]}<span className="capitalize">{r.answer}</span></div></TableCell><TableCell className="text-muted-foreground text-sm">{r.created_at ? format(new Date(r.created_at), 'MMM d, yyyy') : '—'}</TableCell></TableRow>))}</TableBody></Table></div><PaginationControls page={responsesPage} pageSize={responsesPageSize} totalCount={responsesTotal} onPageChange={setResponsesPage} onPageSizeChange={(size) => { setResponsesPage(1); setResponsesPageSize(size); }} /></div>}</TabsContent>}
|
{canViewResponses && <TabsContent value="responses" className="mt-4">{responses.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><p className="text-muted-foreground">No responses yet</p></Card> : <div className="space-y-4"><div className="rounded-md border"><Table><TableHeader><TableRow><TableHead>Question</TableHead><TableHead>Pack</TableHead><TableHead className="w-24">Answer</TableHead><TableHead className="w-32">Date</TableHead></TableRow></TableHeader><TableBody>{responses.map((r) => (<TableRow key={r.id}><TableCell className="max-w-md">{r.question.pack?.id ? (<Link to={`/packs/${r.question.pack.id}/questions`} className="line-clamp-2 text-primary hover:underline cursor-pointer">{r.question.text}</Link>) : (<span className="line-clamp-2">{r.question.text}</span>)}</TableCell><TableCell>{r.question.pack?.id ? (<Link to={`/packs/${r.question.pack.id}/questions`} className="text-primary hover:underline">{r.question.pack.name}</Link>) : (<span className="text-muted-foreground">{r.question.pack?.name || '—'}</span>)}</TableCell><TableCell><div className="flex items-center gap-2">{answerIcons[r.answer]}<span className="capitalize">{r.answer}</span></div></TableCell><TableCell className="text-muted-foreground text-sm">{r.created_at ? format(new Date(r.created_at), 'MMM d, yyyy') : '—'}</TableCell></TableRow>))}</TableBody></Table></div><PaginationControls page={responsesPage} pageSize={responsesPageSize} totalCount={responsesTotal} onPageChange={setResponsesPage} onPageSizeChange={(size) => { setResponsesPage(1); setResponsesPageSize(size); }} /></div>}</TabsContent>}
|
||||||
{canViewMatches && <TabsContent value="matches" className="mt-4">{matches.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><p className="text-muted-foreground">No matches yet</p></Card> : <div className="space-y-4"><div className="grid gap-4 md:grid-cols-2">{matches.map((match) => (<Card key={match.id} className="hover:shadow-md transition-shadow"><CardHeader className="pb-2"><div className="flex items-start justify-between"><Badge variant="secondary">{matchTypeLabels[match.match_type]}</Badge>{match.is_new && <Badge variant="default" className="text-xs">New</Badge>}</div><CardDescription className="line-clamp-2 mt-2">{match.question?.text || 'Unknown question'}</CardDescription></CardHeader><CardContent><div className="flex items-center justify-between"><div className="flex items-center gap-1 text-sm text-muted-foreground"><MessageCircle className="h-4 w-4" />{match.message_count} message{match.message_count !== 1 ? 's' : ''}</div>{canViewChats && <Link to={`/users/${userId}/matches/${match.id}`}><Button variant="ghost" size="sm">View Chat<ChevronRight className="ml-1 h-4 w-4" /></Button></Link>}</div></CardContent></Card>))}</div><PaginationControls page={matchesPage} pageSize={matchesPageSize} totalCount={matchesTotal} onPageChange={setMatchesPage} onPageSizeChange={(size) => { setMatchesPage(1); setMatchesPageSize(size); }} /></div>}</TabsContent>}
|
{canViewMatches && <TabsContent value="matches" className="mt-4">{matches.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><p className="text-muted-foreground">No matches yet</p></Card> : <div className="space-y-4"><div className="grid gap-4 md:grid-cols-2">{matches.map((match) => (<Card key={match.id} className="hover:shadow-md transition-shadow"><CardHeader className="pb-2"><div className="flex items-start justify-between"><Badge variant="secondary">{matchTypeLabels[match.match_type]}</Badge>{match.is_new && <Badge variant="default" className="text-xs">New</Badge>}</div><CardDescription className="line-clamp-2 mt-2">{match.question?.text || 'Unknown question'}</CardDescription></CardHeader><CardContent><div className="flex items-center justify-between"><div className="flex items-center gap-1 text-sm text-muted-foreground"><MessageCircle className="h-4 w-4" />{match.message_count} message{match.message_count !== 1 ? 's' : ''}</div>{canViewChats && <Link to={`/users/${userId}/matches/${match.id}`}><Button variant="ghost" size="sm">View Chat<ChevronRight className="ml-1 h-4 w-4" /></Button></Link>}</div></CardContent></Card>))}</div><PaginationControls page={matchesPage} pageSize={matchesPageSize} totalCount={matchesTotal} onPageChange={setMatchesPage} onPageSizeChange={(size) => { setMatchesPage(1); setMatchesPageSize(size); }} /></div>}</TabsContent>}
|
||||||
{canViewMedia && <TabsContent value="media" className="mt-4">{mediaMessages.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><Image className="h-12 w-12 text-muted-foreground mb-4" /><p className="text-muted-foreground">No media uploaded</p></Card> : <div className="space-y-4"><div className="grid gap-4 grid-cols-2 md:grid-cols-3 lg:grid-cols-4">{mediaMessages.map((message) => (<Card key={message.id} className="overflow-hidden"><div className="aspect-square bg-muted relative">
|
{canViewMedia && <TabsContent value="media" className="mt-4">{mediaMessages.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><Image className="h-12 w-12 text-muted-foreground mb-4" /><p className="text-muted-foreground">No media uploaded</p></Card> : <div className="space-y-4"><div className="grid gap-4 grid-cols-2 md:grid-cols-3 lg:grid-cols-4">{mediaMessages.map((message) => (<Card key={message.id} className="overflow-hidden"><div className="aspect-square bg-muted relative">
|
||||||
{message.media_type === 'video' ? (
|
{message.media_type === 'video' ? (
|
||||||
@@ -478,6 +575,59 @@ export function UserDetailPage() {
|
|||||||
<AdminDecryptedImage messageId={message.id} alt={`Media from ${message.created_at}`} />
|
<AdminDecryptedImage messageId={message.id} alt={`Media from ${message.created_at}`} />
|
||||||
)}
|
)}
|
||||||
</div><CardContent className="p-3"><div className="flex items-center justify-between text-sm"><div className="truncate text-muted-foreground">{message.media_type === 'video' ? 'Video' : 'Image'}</div>{canViewChats && message.match_id && <Link to={`/users/${userId}/matches/${message.match_id}`} className="text-primary hover:underline text-xs">View Chat</Link>}</div><div className="text-xs text-muted-foreground mt-1">{message.created_at ? format(new Date(message.created_at), 'MMM d, yyyy') : '—'}</div></CardContent></Card>))}</div><PaginationControls page={mediaPage} pageSize={mediaPageSize} totalCount={mediaTotal} onPageChange={setMediaPage} onPageSizeChange={(size) => { setMediaPage(1); setMediaPageSize(size); }} /></div>}</TabsContent>}
|
</div><CardContent className="p-3"><div className="flex items-center justify-between text-sm"><div className="truncate text-muted-foreground">{message.media_type === 'video' ? 'Video' : 'Image'}</div>{canViewChats && message.match_id && <Link to={`/users/${userId}/matches/${message.match_id}`} className="text-primary hover:underline text-xs">View Chat</Link>}</div><div className="text-xs text-muted-foreground mt-1">{message.created_at ? format(new Date(message.created_at), 'MMM d, yyyy') : '—'}</div></CardContent></Card>))}</div><PaginationControls page={mediaPage} pageSize={mediaPageSize} totalCount={mediaTotal} onPageChange={setMediaPage} onPageSizeChange={(size) => { setMediaPage(1); setMediaPageSize(size); }} /></div>}</TabsContent>}
|
||||||
|
<TabsContent value="packs" className="mt-4">
|
||||||
|
{enabledPacks.length === 0 ? (
|
||||||
|
<Card className="flex flex-col items-center justify-center py-12">
|
||||||
|
<Package className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
|
<p className="text-muted-foreground">No packs enabled</p>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{enabledPacks.map((ep) => (
|
||||||
|
<Link key={ep.pack_id} to={`/packs/${ep.pack.id}/questions`}>
|
||||||
|
<Card className="hover:shadow-md transition-shadow cursor-pointer">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconPreview value={ep.pack.icon} fallback="heart-outline" className="text-2xl" />
|
||||||
|
<CardTitle className="text-base">{ep.pack.name}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{ep.pack.is_premium && (
|
||||||
|
<Badge className="bg-amber-500 text-xs">
|
||||||
|
<Crown className="h-3 w-3 mr-1" />
|
||||||
|
Premium
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{ep.pack.is_explicit && (
|
||||||
|
<Badge variant="destructive" className="text-xs">
|
||||||
|
18+
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ep.pack.description && (
|
||||||
|
<CardDescription className="line-clamp-2 mt-2">
|
||||||
|
{ep.pack.description}
|
||||||
|
</CardDescription>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{ep.pack.category?.name || 'Uncategorized'}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
Enabled {ep.created_at ? format(new Date(ep.created_at), 'MMM d, yyyy') : '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,4 +5,7 @@
|
|||||||
expo-env.d.ts
|
expo-env.d.ts
|
||||||
# @end expo-cli
|
# @end expo-cli
|
||||||
*.aab
|
*.aab
|
||||||
*.ipa
|
*.ipa
|
||||||
|
*.apk
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<key>IS_ADS_ENABLED</key>
|
<key>IS_ADS_ENABLED</key>
|
||||||
<false></false>
|
<false></false>
|
||||||
<key>IS_ANALYTICS_ENABLED</key>
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
<false></false>
|
<true></true>
|
||||||
<key>IS_APPINVITE_ENABLED</key>
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
<true></true>
|
<true></true>
|
||||||
<key>IS_GCM_ENABLED</key>
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ android {
|
|||||||
applicationId 'com.sauci.app'
|
applicationId 'com.sauci.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 21
|
versionCode 30
|
||||||
versionName "1.0.0"
|
versionName "1.0.2"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
@@ -13,6 +13,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "764866133492-03k7q0mq6nqrv0phunncqjn85cgj6sep.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.sauci.app",
|
||||||
|
"certificate_hash": "f06026c754b046f280feac438a6910bd9a62b459"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"client_id": "764866133492-4g566mrb8tm5uti4ejub6oogcvnln7kc.apps.googleusercontent.com",
|
"client_id": "764866133492-4g566mrb8tm5uti4ejub6oogcvnln7kc.apps.googleusercontent.com",
|
||||||
"client_type": 1,
|
"client_type": 1,
|
||||||
|
|||||||
+4
@@ -12,3 +12,7 @@
|
|||||||
-keep class com.facebook.react.turbomodule.** { *; }
|
-keep class com.facebook.react.turbomodule.** { *; }
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# @generated begin expo-build-properties - expo prebuild (DO NOT MODIFY)
|
||||||
|
-keep class com.facebook.hermes.unicode.** { *; }
|
||||||
|
# @generated end expo-build-properties
|
||||||
@@ -18,7 +18,7 @@ buildscript {
|
|||||||
classpath 'com.google.gms:google-services:4.4.1'
|
classpath 'com.google.gms:google-services:4.4.1'
|
||||||
classpath('com.android.tools.build:gradle')
|
classpath('com.android.tools.build:gradle')
|
||||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,4 +56,5 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
|||||||
expo.useLegacyPackaging=false
|
expo.useLegacyPackaging=false
|
||||||
|
|
||||||
android.targetSdkVersion=35
|
android.targetSdkVersion=35
|
||||||
|
android.kotlinVersion=1.9.25
|
||||||
android.extraMavenRepos=[]
|
android.extraMavenRepos=[]
|
||||||
+17
-5
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Sauci",
|
"name": "Sauci",
|
||||||
"slug": "sauci",
|
"slug": "sauci",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"scheme": "app.sauci",
|
"scheme": "app.sauci",
|
||||||
@@ -16,8 +16,9 @@
|
|||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": false,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.sauci.app",
|
"bundleIdentifier": "com.sauci.app",
|
||||||
"buildNumber": "25",
|
"buildNumber": "30",
|
||||||
"usesAppleSignIn": true,
|
"usesAppleSignIn": true,
|
||||||
|
"googleServicesFile": "./GoogleService-Info.plist",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"UIBackgroundModes": [
|
"UIBackgroundModes": [
|
||||||
"remote-notification"
|
"remote-notification"
|
||||||
@@ -176,7 +177,7 @@
|
|||||||
"backgroundColor": "#1a1a2e"
|
"backgroundColor": "#1a1a2e"
|
||||||
},
|
},
|
||||||
"package": "com.sauci.app",
|
"package": "com.sauci.app",
|
||||||
"versionCode": 21,
|
"versionCode": 30,
|
||||||
"googleServicesFile": "./google-services.json",
|
"googleServicesFile": "./google-services.json",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"RECEIVE_BOOT_COMPLETED",
|
"RECEIVE_BOOT_COMPLETED",
|
||||||
@@ -192,11 +193,21 @@
|
|||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"@react-native-firebase/app",
|
||||||
[
|
[
|
||||||
"expo-build-properties",
|
"expo-build-properties",
|
||||||
{
|
{
|
||||||
"android": {
|
"android": {
|
||||||
"targetSdkVersion": 35
|
"targetSdkVersion": 35,
|
||||||
|
"kotlinVersion": "1.9.25",
|
||||||
|
"extraProguardRules": "-keep class com.facebook.hermes.unicode.** { *; }",
|
||||||
|
"useLegacyPackaging": false,
|
||||||
|
"manifestPlaceholders": {
|
||||||
|
"firebaseAnalyticsWithoutAdId": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"useFrameworks": "static"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -231,7 +242,8 @@
|
|||||||
{
|
{
|
||||||
"iosUrlScheme": "com.googleusercontent.apps.861972684340-0u2as8i5equfhmhmvjihottkf0ig7237"
|
"iosUrlScheme": "com.googleusercontent.apps.861972684340-0u2as8i5equfhmhmvjihottkf0ig7237"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"./plugins/withKotlinVersion.js"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
// Polyfills must be imported FIRST before any other React Native imports
|
// Polyfills must be imported FIRST before any other React Native imports
|
||||||
import "../src/polyfills/web";
|
import "../src/polyfills/web";
|
||||||
import "../src/polyfills/crypto";
|
|
||||||
|
|
||||||
// Initialize Sentry early to capture all errors
|
// Initialize Sentry early to capture all errors
|
||||||
import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry";
|
import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry";
|
||||||
initSentry();
|
initSentry();
|
||||||
|
|
||||||
// Initialize Analytics
|
// Analytics imports (initialization deferred until app mounts)
|
||||||
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
|
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
|
||||||
initAnalytics();
|
|
||||||
|
|
||||||
// Track cold start immediately
|
|
||||||
Events.appOpened("cold");
|
|
||||||
|
|
||||||
// Import push notification utilities
|
// Import push notification utilities
|
||||||
import {
|
import {
|
||||||
@@ -55,6 +50,16 @@ export default function RootLayout() {
|
|||||||
const { fetchUser, setUser } = useAuthStore();
|
const { fetchUser, setUser } = useAuthStore();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const appState = useRef(AppState.currentState);
|
const appState = useRef(AppState.currentState);
|
||||||
|
const analyticsInitialized = useRef(false);
|
||||||
|
|
||||||
|
// Initialize analytics after native modules are ready
|
||||||
|
useEffect(() => {
|
||||||
|
if (!analyticsInitialized.current) {
|
||||||
|
analyticsInitialized.current = true;
|
||||||
|
initAnalytics();
|
||||||
|
Events.appOpened("cold");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Track screen views
|
// Track screen views
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -5,13 +5,16 @@
|
|||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"development": {
|
"development": {
|
||||||
|
"node": "20.10.0",
|
||||||
"developmentClient": true,
|
"developmentClient": true,
|
||||||
"distribution": "internal"
|
"distribution": "internal"
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
|
"node": "20.10.0",
|
||||||
"distribution": "internal"
|
"distribution": "internal"
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
|
"node": "20.10.0",
|
||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"env": {
|
"env": {
|
||||||
"EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
|
"EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"react-native": {
|
||||||
|
"analytics_auto_collection_enabled": true,
|
||||||
|
"google_analytics_adid_collection_enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "764866133492-03k7q0mq6nqrv0phunncqjn85cgj6sep.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.sauci.app",
|
||||||
|
"certificate_hash": "f06026c754b046f280feac438a6910bd9a62b459"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"client_id": "764866133492-4g566mrb8tm5uti4ejub6oogcvnln7kc.apps.googleusercontent.com",
|
"client_id": "764866133492-4g566mrb8tm5uti4ejub6oogcvnln7kc.apps.googleusercontent.com",
|
||||||
"client_type": 1,
|
"client_type": 1,
|
||||||
|
|||||||
+187
-81
@@ -164,6 +164,7 @@ PODS:
|
|||||||
- React-jsinspector
|
- React-jsinspector
|
||||||
- React-NativeModulesApple
|
- React-NativeModulesApple
|
||||||
- React-RCTFabric
|
- React-RCTFabric
|
||||||
|
- React-rendererconsistency
|
||||||
- React-rendererdebug
|
- React-rendererdebug
|
||||||
- React-utils
|
- React-utils
|
||||||
- ReactCodegen
|
- ReactCodegen
|
||||||
@@ -303,19 +304,81 @@ PODS:
|
|||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- fast_float (6.1.4)
|
- fast_float (6.1.4)
|
||||||
- FBLazyVector (0.76.9)
|
- FBLazyVector (0.76.9)
|
||||||
|
- Firebase/CoreOnly (12.8.0):
|
||||||
|
- FirebaseCore (~> 12.8.0)
|
||||||
|
- FirebaseAnalytics/Core (12.8.0):
|
||||||
|
- FirebaseCore (~> 12.8.0)
|
||||||
|
- FirebaseInstallations (~> 12.8.0)
|
||||||
|
- GoogleAppMeasurement/Core (= 12.8.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- FirebaseAnalytics/IdentitySupport (12.8.0):
|
||||||
|
- FirebaseCore (~> 12.8.0)
|
||||||
|
- FirebaseInstallations (~> 12.8.0)
|
||||||
|
- GoogleAppMeasurement/IdentitySupport (= 12.8.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- FirebaseCore (12.8.0):
|
||||||
|
- FirebaseCoreInternal (~> 12.8.0)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
|
- FirebaseCoreInternal (12.8.0):
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
|
- FirebaseInstallations (12.8.0):
|
||||||
|
- FirebaseCore (~> 12.8.0)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
- fmt (11.0.2)
|
- fmt (11.0.2)
|
||||||
- glog (0.3.5)
|
- glog (0.3.5)
|
||||||
- GoogleSignIn (9.0.0):
|
- GoogleAppMeasurement/Core (12.8.0):
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- GoogleAppMeasurement/IdentitySupport (12.8.0):
|
||||||
|
- GoogleAppMeasurement/Core (= 12.8.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- GoogleSignIn (9.1.0):
|
||||||
- AppAuth (~> 2.0)
|
- AppAuth (~> 2.0)
|
||||||
- AppCheckCore (~> 11.0)
|
- AppCheckCore (~> 11.0)
|
||||||
- GTMAppAuth (~> 5.0)
|
- GTMAppAuth (~> 5.0)
|
||||||
- GTMSessionFetcher/Core (~> 3.3)
|
- GTMSessionFetcher/Core (~> 3.3)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Network
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Environment (8.1.0):
|
- GoogleUtilities/Environment (8.1.0):
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Logger (8.1.0):
|
- GoogleUtilities/Logger (8.1.0):
|
||||||
- GoogleUtilities/Environment
|
- GoogleUtilities/Environment
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/MethodSwizzler (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Network (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- "GoogleUtilities/NSData+zlib"
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Reachability
|
||||||
|
- "GoogleUtilities/NSData+zlib (8.1.0)":
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Privacy (8.1.0)
|
- GoogleUtilities/Privacy (8.1.0)
|
||||||
|
- GoogleUtilities/Reachability (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/UserDefaults (8.1.0):
|
- GoogleUtilities/UserDefaults (8.1.0):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
@@ -331,10 +394,14 @@ PODS:
|
|||||||
- libavif/core
|
- libavif/core
|
||||||
- libdav1d (>= 0.6.0)
|
- libdav1d (>= 0.6.0)
|
||||||
- libdav1d (1.2.0)
|
- libdav1d (1.2.0)
|
||||||
- OpenSSL-Universal (3.3.3001)
|
- nanopb (3.30910.0):
|
||||||
|
- nanopb/decode (= 3.30910.0)
|
||||||
|
- nanopb/encode (= 3.30910.0)
|
||||||
|
- nanopb/decode (3.30910.0)
|
||||||
|
- nanopb/encode (3.30910.0)
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- PurchasesHybridCommon (17.25.0):
|
- PurchasesHybridCommon (17.28.0):
|
||||||
- RevenueCat (= 5.51.1)
|
- RevenueCat (= 5.55.1)
|
||||||
- RCT-Folly (2024.10.14.00):
|
- RCT-Folly (2024.10.14.00):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -1613,7 +1680,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-compressor (1.13.0):
|
- react-native-compressor (1.16.0):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
@@ -1634,29 +1701,6 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-quick-crypto (0.7.17):
|
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- hermes-engine
|
|
||||||
- OpenSSL-Universal
|
|
||||||
- RCT-Folly (= 2024.10.14.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React
|
|
||||||
- React-Core
|
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- Yoga
|
|
||||||
- react-native-safe-area-context (4.12.0):
|
- react-native-safe-area-context (4.12.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-nativeconfig (0.76.9)
|
- React-nativeconfig (0.76.9)
|
||||||
@@ -1931,9 +1975,55 @@ PODS:
|
|||||||
- React-logger
|
- React-logger
|
||||||
- React-perflogger
|
- React-perflogger
|
||||||
- React-utils (= 0.76.9)
|
- React-utils (= 0.76.9)
|
||||||
- RevenueCat (5.51.1)
|
- RevenueCat (5.55.1)
|
||||||
- RNCAsyncStorage (1.23.1):
|
- RNCAsyncStorage (1.23.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- RNFBAnalytics (23.8.3):
|
||||||
|
- DoubleConversion
|
||||||
|
- FirebaseAnalytics/Core (= 12.8.0)
|
||||||
|
- FirebaseAnalytics/IdentitySupport (= 12.8.0)
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly (= 2024.10.14.00)
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- RNFBApp
|
||||||
|
- Yoga
|
||||||
|
- RNFBApp (23.8.3):
|
||||||
|
- DoubleConversion
|
||||||
|
- Firebase/CoreOnly (= 12.8.0)
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly (= 2024.10.14.00)
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- Yoga
|
||||||
- RNGestureHandler (2.20.2):
|
- RNGestureHandler (2.20.2):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
@@ -1958,8 +2048,8 @@ PODS:
|
|||||||
- RNGoogleSignin (16.1.1):
|
- RNGoogleSignin (16.1.1):
|
||||||
- GoogleSignIn (~> 9.0)
|
- GoogleSignIn (~> 9.0)
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNPurchases (9.6.12):
|
- RNPurchases (9.7.1):
|
||||||
- PurchasesHybridCommon (= 17.25.0)
|
- PurchasesHybridCommon (= 17.28.0)
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNReanimated (3.16.7):
|
- RNReanimated (3.16.7):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -2176,7 +2266,6 @@ DEPENDENCIES:
|
|||||||
- React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
|
- React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||||
- react-native-compressor (from `../../../node_modules/react-native-compressor`)
|
- react-native-compressor (from `../../../node_modules/react-native-compressor`)
|
||||||
- react-native-quick-crypto (from `../../../node_modules/react-native-quick-crypto`)
|
|
||||||
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
|
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
|
||||||
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
|
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||||
@@ -2206,6 +2295,8 @@ DEPENDENCIES:
|
|||||||
- ReactCodegen (from `build/generated/ios`)
|
- ReactCodegen (from `build/generated/ios`)
|
||||||
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
|
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
|
||||||
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
|
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
|
- "RNFBAnalytics (from `../../../node_modules/@react-native-firebase/analytics`)"
|
||||||
|
- "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)"
|
||||||
- RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`)
|
- RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`)
|
||||||
- "RNGoogleSignin (from `../../../node_modules/@react-native-google-signin/google-signin`)"
|
- "RNGoogleSignin (from `../../../node_modules/@react-native-google-signin/google-signin`)"
|
||||||
- RNPurchases (from `../../../node_modules/react-native-purchases`)
|
- RNPurchases (from `../../../node_modules/react-native-purchases`)
|
||||||
@@ -2218,13 +2309,19 @@ SPEC REPOS:
|
|||||||
trunk:
|
trunk:
|
||||||
- AppAuth
|
- AppAuth
|
||||||
- AppCheckCore
|
- AppCheckCore
|
||||||
|
- Firebase
|
||||||
|
- FirebaseAnalytics
|
||||||
|
- FirebaseCore
|
||||||
|
- FirebaseCoreInternal
|
||||||
|
- FirebaseInstallations
|
||||||
|
- GoogleAppMeasurement
|
||||||
- GoogleSignIn
|
- GoogleSignIn
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- GTMAppAuth
|
- GTMAppAuth
|
||||||
- GTMSessionFetcher
|
- GTMSessionFetcher
|
||||||
- libavif
|
- libavif
|
||||||
- libdav1d
|
- libdav1d
|
||||||
- OpenSSL-Universal
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PurchasesHybridCommon
|
- PurchasesHybridCommon
|
||||||
- RevenueCat
|
- RevenueCat
|
||||||
@@ -2372,8 +2469,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||||
react-native-compressor:
|
react-native-compressor:
|
||||||
:path: "../../../node_modules/react-native-compressor"
|
:path: "../../../node_modules/react-native-compressor"
|
||||||
react-native-quick-crypto:
|
|
||||||
:path: "../../../node_modules/react-native-quick-crypto"
|
|
||||||
react-native-safe-area-context:
|
react-native-safe-area-context:
|
||||||
:path: "../../../node_modules/react-native-safe-area-context"
|
:path: "../../../node_modules/react-native-safe-area-context"
|
||||||
React-nativeconfig:
|
React-nativeconfig:
|
||||||
@@ -2432,6 +2527,10 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../../node_modules/react-native/ReactCommon"
|
:path: "../../../node_modules/react-native/ReactCommon"
|
||||||
RNCAsyncStorage:
|
RNCAsyncStorage:
|
||||||
:path: "../../../node_modules/@react-native-async-storage/async-storage"
|
:path: "../../../node_modules/@react-native-async-storage/async-storage"
|
||||||
|
RNFBAnalytics:
|
||||||
|
:path: "../../../node_modules/@react-native-firebase/analytics"
|
||||||
|
RNFBApp:
|
||||||
|
:path: "../../../node_modules/@react-native-firebase/app"
|
||||||
RNGestureHandler:
|
RNGestureHandler:
|
||||||
:path: "../../../node_modules/react-native-gesture-handler"
|
:path: "../../../node_modules/react-native-gesture-handler"
|
||||||
RNGoogleSignin:
|
RNGoogleSignin:
|
||||||
@@ -2461,8 +2560,8 @@ SPEC CHECKSUMS:
|
|||||||
EXNotifications: 9d3d17e52c95f377750d4a2ae553a716313dd4aa
|
EXNotifications: 9d3d17e52c95f377750d4a2ae553a716313dd4aa
|
||||||
Expo: 3889933926fade17d8607f449c57b87c4022433f
|
Expo: 3889933926fade17d8607f449c57b87c4022433f
|
||||||
expo-dev-client: db44302cdbe0ec55b0ef1849c9a23a76dec6dbac
|
expo-dev-client: db44302cdbe0ec55b0ef1849c9a23a76dec6dbac
|
||||||
expo-dev-launcher: 82de1bf29e91f4794bcc65a697e7066d9a826bee
|
expo-dev-launcher: c67c6a87f8a8ff54abda104abdc057ee4565a3f4
|
||||||
expo-dev-menu: 3fda3eb2b3394dc92955f6ff60cb278f843641be
|
expo-dev-menu: 1426660b67d2d594f70ec1d8a85bc26cafcf1bce
|
||||||
expo-dev-menu-interface: 00dc42302a72722fdecec3fa048de84a9133bcc4
|
expo-dev-menu-interface: 00dc42302a72722fdecec3fa048de84a9133bcc4
|
||||||
ExpoAdapterGoogleSignIn: 21ff1daeef3d21cb0b45772d8029102cc728d6b1
|
ExpoAdapterGoogleSignIn: 21ff1daeef3d21cb0b45772d8029102cc728d6b1
|
||||||
ExpoAppleAuthentication: 52631ed9dcb71c65712a447bbb9a5667bb8fcf0c
|
ExpoAppleAuthentication: 52631ed9dcb71c65712a447bbb9a5667bb8fcf0c
|
||||||
@@ -2480,23 +2579,29 @@ SPEC CHECKSUMS:
|
|||||||
ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402
|
ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402
|
||||||
ExpoLocalAuthentication: eb2be9c7bcdc68e9434d4be4bb5aa5cf7943e816
|
ExpoLocalAuthentication: eb2be9c7bcdc68e9434d4be4bb5aa5cf7943e816
|
||||||
ExpoMediaLibrary: 2709fc2e4fcf87d28767714abcc603c4bec228c6
|
ExpoMediaLibrary: 2709fc2e4fcf87d28767714abcc603c4bec228c6
|
||||||
ExpoModulesCore: 725faec070d590810d2ea5983d9f78f7cf6a38ec
|
ExpoModulesCore: 2d8d1c8e238bd5e1448b06c284ac896b162fb856
|
||||||
ExpoSecureStore: 9a3665d7161dd1031be386e278bffd623658f316
|
ExpoSecureStore: 9a3665d7161dd1031be386e278bffd623658f316
|
||||||
EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db
|
EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db
|
||||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||||
FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45
|
FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45
|
||||||
|
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
|
||||||
|
FirebaseAnalytics: f20bbad8cb7f65d8a5eaefeb424ae8800a31bdfc
|
||||||
|
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
|
||||||
|
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
|
||||||
|
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
|
||||||
fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6
|
fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6
|
||||||
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
|
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
|
||||||
GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77
|
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
|
||||||
|
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
||||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||||
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
|
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
|
||||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||||
OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PurchasesHybridCommon: 6a79a873ab52f777bfa36e9516f3fcd84d3b3428
|
PurchasesHybridCommon: c2f8f5fcd2e472c585fccd1ab2fe317a3dc1171e
|
||||||
RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17
|
RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17
|
||||||
RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83
|
RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83
|
||||||
RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716
|
RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716
|
||||||
@@ -2506,70 +2611,71 @@ SPEC CHECKSUMS:
|
|||||||
React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245
|
React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245
|
||||||
React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c
|
React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c
|
||||||
React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab
|
React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab
|
||||||
React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964
|
React-debug: f1956672cf545729afb71ef838a9789c2a9088f5
|
||||||
React-defaultsnativemodule: 21f216e8db975897eb32b5f13247f5bbfaa97f41
|
React-defaultsnativemodule: 30db2698d8b37831cf97098978ddc8ea97251898
|
||||||
React-domnativemodule: 19270ad4b8d33312838d257f24731a0026809d49
|
React-domnativemodule: 1c018f599b60aa292d764bcee1c52716919940c6
|
||||||
React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b
|
React-Fabric: f637bbb636e3348a66d269f179cd39be1d31ee3d
|
||||||
React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520
|
React-FabricComponents: 9542268e7cc59a49f0c2c1906d8ab9729a979b5a
|
||||||
React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576
|
React-FabricImage: e2f72efe22e23ff3740502553614f1c2d86132c9
|
||||||
React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c
|
React-featureflags: dc9c91fcd6a75cc95bd7fec184d0be8ad0f26db5
|
||||||
React-featureflagsnativemodule: 3a8731d8fd9f755be57e00d9fa8a7f92aa77e87d
|
React-featureflagsnativemodule: 1c6ca282bd47de71a16420e29e7de28d46e59110
|
||||||
React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121
|
React-graphics: 85866d95589b775bcb5f02946a96e948f53088f1
|
||||||
React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf
|
React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf
|
||||||
React-idlecallbacksnativemodule: 9a2c5b5c174c0c476f039bedc1b9497a8272133e
|
React-idlecallbacksnativemodule: d8e52f9e2bbc15f57714a6def9ba7ecea0f59f9f
|
||||||
React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020
|
React-ImageManager: 5b3843bea322e4884400f1d78cfde7c435c374bc
|
||||||
React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6
|
React-jserrorhandler: 23161cf9fef2da6d56bf5cfda14f4abc8d62b286
|
||||||
React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d
|
React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d
|
||||||
React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893
|
React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893
|
||||||
React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086
|
React-jsinspector: 3f8d148785a57f082e71382657c5fbe1be75bcdf
|
||||||
React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f
|
React-jsitracing: 69456514c86311eb042d824e4cf69f9b524b5b97
|
||||||
React-logger: c4052eb941cca9a097ef01b59543a656dc088559
|
React-logger: c4052eb941cca9a097ef01b59543a656dc088559
|
||||||
React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de
|
React-Mapbuffer: 9343a5c14536d4463c80f09a960653d754daae21
|
||||||
React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3
|
React-microtasksnativemodule: 338bfa0a7d8cd1ed212b055f2143a6789252137a
|
||||||
react-native-compressor: 2c3a41768bdd367f6e121f555dff9d3eda1810e5
|
react-native-compressor: 40b3e2b942aebac3daaae9549b23a13510c67a6f
|
||||||
react-native-quick-crypto: 7bb26af9b3705dbb9277c09305104d37ede74916
|
|
||||||
react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448
|
react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448
|
||||||
React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678
|
React-nativeconfig: 415626a63057638759bcc75e0a96e2e07771a479
|
||||||
React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e
|
React-NativeModulesApple: d33b55553c6957ff94835574636838d78121a1c6
|
||||||
React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358
|
React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358
|
||||||
React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc
|
React-performancetimeline: f831af5af289407b87cdfbceafb7623e5960f9f3
|
||||||
React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342
|
React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342
|
||||||
React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585
|
React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585
|
||||||
React-RCTAppDelegate: 1e5b43833e3e36e9fa34eec20be98174bc0e14a2
|
React-RCTAppDelegate: 1e5b43833e3e36e9fa34eec20be98174bc0e14a2
|
||||||
React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6
|
React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6
|
||||||
React-RCTFabric: bd906861a4e971e21d8df496c2d8f3ca6956f840
|
React-RCTFabric: 07e1434e6bd8124460c4b4eb3bdb82b58bfafaee
|
||||||
React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8
|
React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8
|
||||||
React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa
|
React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa
|
||||||
React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7
|
React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7
|
||||||
React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223
|
React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223
|
||||||
React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf
|
React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf
|
||||||
React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78
|
React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78
|
||||||
React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6
|
React-rendererconsistency: b89c074e22ca2b9eaa8013e1310c69db85364893
|
||||||
React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec
|
React-rendererdebug: 25290691d85200d9d229f2c41c24b285a9f04e4b
|
||||||
React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74
|
React-rncore: 2a6efd857e10a3b299f8a7fe912fd332aa7e515a
|
||||||
React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd
|
React-RuntimeApple: 383f3b59440644f555d0a475379c5ee97c3173f6
|
||||||
React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb
|
React-RuntimeCore: 753cb0bcae285509787d465d176c074ef7a6c537
|
||||||
React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899
|
React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899
|
||||||
React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385
|
React-RuntimeHermes: bad4e404e8a6c25bc6ebcfd5eccb671bed76a67d
|
||||||
React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d
|
React-runtimescheduler: a352af9ab3939273ee0e02650cfc1c8ee6e4d0c9
|
||||||
React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9
|
React-timing: a693c531e5627dcc200fc7286cbbebf73d73469d
|
||||||
React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f
|
React-utils: 59c5bbbc0e72be22c9d6eceb40afadf9be872819
|
||||||
ReactCodegen: 2a46abb2e345dc8efaff0a724f5f8639230eb974
|
ReactCodegen: 52b654894b5771d0085b24d4b1b897ebeaf939dc
|
||||||
ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9
|
ReactCommon: b2eb96a61b826ff327a773a74357b302cf6da678
|
||||||
RevenueCat: eab035bbab271faccfef5c36eaff2a1ffef14dc0
|
RevenueCat: 347221fad5ab0ed5636546867deae2c32c64749f
|
||||||
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
|
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
|
||||||
RNGestureHandler: 32336458ef842088a459ccfcc10b583186de2633
|
RNFBAnalytics: d1c906bbb01b3a6f6b99d6eaa977f04b1ae5d279
|
||||||
|
RNFBApp: fecce94543a693bcc73807a83ce5e9d70ff68725
|
||||||
|
RNGestureHandler: 2dd124adb171aab16629e9387cf1830957d242c2
|
||||||
RNGoogleSignin: 5782dd7dad559608a06f3c6b1d4142c395fb5308
|
RNGoogleSignin: 5782dd7dad559608a06f3c6b1d4142c395fb5308
|
||||||
RNPurchases: cc3333cede6c0f040545a95c38ae627d0174c058
|
RNPurchases: e265e2ed27349b567b91b89d8712aa8c9c57f5b0
|
||||||
RNReanimated: 82bf292e3c590d2d9886039f83e1baf0c1b2cdc4
|
RNReanimated: 38e033fd1790c817678bc8a075277cdc49d6817c
|
||||||
RNScreens: 295d9c0aaeb7f680d03d7e9b476569a4959aae89
|
RNScreens: ad2d0cfb3f15c9ed56ffb43651366ec0dcd70c36
|
||||||
RNSentry: 41eab31e46a191a4f9296d64a318b2a6bf0f98c5
|
RNSentry: a8f0d86b88675719ce96cb31bd9258dbd793e535
|
||||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||||
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
|
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
|
||||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||||
Sentry: 1ca8405451040482877dcd344dfa3ef80b646631
|
Sentry: 1ca8405451040482877dcd344dfa3ef80b646631
|
||||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||||
Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a
|
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
|
||||||
|
|
||||||
PODFILE CHECKSUM: 085b90662c05f70d43685ff8bec1ea714a98abb3
|
PODFILE CHECKSUM: 085b90662c05f70d43685ff8bec1ea714a98abb3
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"expo.jsEngine": "hermes",
|
"expo.jsEngine": "hermes",
|
||||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
"newArchEnabled": "false",
|
"newArchEnabled": "false",
|
||||||
|
"ios.useFrameworks": "static",
|
||||||
"apple.extraPods": "[]",
|
"apple.extraPods": "[]",
|
||||||
"apple.ccacheEnabled": "false",
|
"apple.ccacheEnabled": "false",
|
||||||
"apple.privacyManifestAggregationEnabled": "true"
|
"apple.privacyManifestAggregationEnabled": "true"
|
||||||
|
|||||||
@@ -3,36 +3,38 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 46;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
|
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||||
|
1AFB24071BC9A2F5B90C1951 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 787323E49260E99741AD6413 /* PrivacyInfo.xcprivacy */; };
|
||||||
|
1F3D2CE02DD8CC2E2BA9F93A /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27B40580D18D29CBD94DD2C3 /* Pods_Sauci.framework */; };
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||||
656E142A44D7042462875BC2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0FE9A3D4025061824A4C95F7 /* PrivacyInfo.xcprivacy */; };
|
47690AB7B3224CD3B1655332 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2EDE49AC9AAC445892628B2B /* GoogleService-Info.plist */; };
|
||||||
96905EF65AED1B983A6B3ABC /* libPods-Sauci.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */; };
|
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||||
BBC1EC435C914364BBC9058D /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 157EFF60C4BA4BA694688B2F /* noop-file.swift */; };
|
E52FFD661AC14FC08C4F239D /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F85F9C76D34B859549F3A7 /* noop-file.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
0FE9A3D4025061824A4C95F7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
|
||||||
13B07F961A680F5B00A75B9A /* Sauci.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sauci.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
13B07F961A680F5B00A75B9A /* Sauci.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sauci.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Sauci/AppDelegate.h; sourceTree = "<group>"; };
|
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Sauci/AppDelegate.h; sourceTree = "<group>"; };
|
||||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Sauci/AppDelegate.mm; sourceTree = "<group>"; };
|
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Sauci/AppDelegate.mm; sourceTree = "<group>"; };
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sauci/Images.xcassets; sourceTree = "<group>"; };
|
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sauci/Images.xcassets; sourceTree = "<group>"; };
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sauci/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sauci/Info.plist; sourceTree = "<group>"; };
|
||||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Sauci/main.m; sourceTree = "<group>"; };
|
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Sauci/main.m; sourceTree = "<group>"; };
|
||||||
157EFF60C4BA4BA694688B2F /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Sauci/noop-file.swift"; sourceTree = "<group>"; };
|
27B40580D18D29CBD94DD2C3 /* Pods_Sauci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sauci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sauci.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
2EDE49AC9AAC445892628B2B /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Sauci/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||||
|
31F85F9C76D34B859549F3A7 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Sauci/noop-file.swift"; sourceTree = "<group>"; };
|
||||||
6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.debug.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.debug.xcconfig"; sourceTree = "<group>"; };
|
6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.debug.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
787323E49260E99741AD6413 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.release.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.release.xcconfig"; sourceTree = "<group>"; };
|
7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.release.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
A9934E72B0154500A352F0F1 /* Sauci-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Sauci-Bridging-Header.h"; path = "Sauci/Sauci-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Sauci/SplashScreen.storyboard; sourceTree = "<group>"; };
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Sauci/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||||
DFD02E18A6174627BE6CBF41 /* Sauci-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Sauci-Bridging-Header.h"; path = "Sauci/Sauci-Bridging-Header.h"; sourceTree = "<group>"; };
|
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@@ -42,7 +44,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
96905EF65AED1B983A6B3ABC /* libPods-Sauci.a in Frameworks */,
|
1F3D2CE02DD8CC2E2BA9F93A /* Pods_Sauci.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -59,9 +61,10 @@
|
|||||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||||
13B07FB71A68108700A75B9A /* main.m */,
|
13B07FB71A68108700A75B9A /* main.m */,
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||||
157EFF60C4BA4BA694688B2F /* noop-file.swift */,
|
2EDE49AC9AAC445892628B2B /* GoogleService-Info.plist */,
|
||||||
DFD02E18A6174627BE6CBF41 /* Sauci-Bridging-Header.h */,
|
31F85F9C76D34B859549F3A7 /* noop-file.swift */,
|
||||||
0FE9A3D4025061824A4C95F7 /* PrivacyInfo.xcprivacy */,
|
A9934E72B0154500A352F0F1 /* Sauci-Bridging-Header.h */,
|
||||||
|
787323E49260E99741AD6413 /* PrivacyInfo.xcprivacy */,
|
||||||
);
|
);
|
||||||
name = Sauci;
|
name = Sauci;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -70,7 +73,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Sauci.a */,
|
27B40580D18D29CBD94DD2C3 /* Pods_Sauci.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -147,14 +150,15 @@
|
|||||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Sauci" */;
|
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Sauci" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||||
B4F8858AB365CBF283E38EB6 /* [Expo] Configure project */,
|
DB0F43D5EFF9CB0780FDD6A5 /* [Expo] Configure project */,
|
||||||
13B07F871A680F5B00A75B9A /* Sources */,
|
13B07F871A680F5B00A75B9A /* Sources */,
|
||||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||||
92A7F2AE90734CFDBD5EA084 /* Upload Debug Symbols to Sentry */,
|
F051EF99F56144029D3DFD83 /* Upload Debug Symbols to Sentry */,
|
||||||
EE1F9249669B846739AA38F5 /* [CP] Embed Pods Frameworks */,
|
D1E774350CB6864AD97A24B4 /* [CP] Embed Pods Frameworks */,
|
||||||
|
658D10E2FBC1B3A1C410A9EB /* [CP-User] [RNFB] Core Configuration */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -175,6 +179,8 @@
|
|||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
13B07F861A680F5B00A75B9A = {
|
13B07F861A680F5B00A75B9A = {
|
||||||
LastSwiftMigration = 1250;
|
LastSwiftMigration = 1250;
|
||||||
|
DevelopmentTeam = "BXVPBATDZY";
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -204,7 +210,8 @@
|
|||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||||
656E142A44D7042462875BC2 /* PrivacyInfo.xcprivacy in Resources */,
|
47690AB7B3224CD3B1655332 /* GoogleService-Info.plist in Resources */,
|
||||||
|
1AFB24071BC9A2F5B90C1951 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -248,6 +255,19 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
|
658D10E2FBC1B3A1C410A9EB /* [CP-User] [RNFB] Core Configuration */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)",
|
||||||
|
);
|
||||||
|
name = "[CP-User] [RNFB] Core Configuration";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACLine truncated
|
||||||
|
};
|
||||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -262,6 +282,9 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/GTMAppAuth/GTMAppAuth_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/GTMAppAuth/GTMAppAuth_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle",
|
||||||
@@ -279,6 +302,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle",
|
||||||
);
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
@@ -289,6 +313,9 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoMediaLibrary_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoMediaLibrary_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseInstallations_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMAppAuth_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMAppAuth_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
|
||||||
@@ -306,27 +333,32 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
92A7F2AE90734CFDBD5EA084 /* Upload Debug Symbols to Sentry */ = {
|
D1E774350CB6864AD97A24B4 /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||||
);
|
);
|
||||||
name = "Upload Debug Symbols to Sentry";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
B4F8858AB365CBF283E38EB6 /* [Expo] Configure project */ = {
|
DB0F43D5EFF9CB0780FDD6A5 /* [Expo] Configure project */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -345,25 +377,19 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Sauci/expo-configure-project.sh\"\n";
|
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Sauci/expo-configure-project.sh\"\n";
|
||||||
};
|
};
|
||||||
EE1F9249669B846739AA38F5 /* [CP] Embed Pods Frameworks */ = {
|
F051EF99F56144029D3DFD83 /* Upload Debug Symbols to Sentry */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh",
|
|
||||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
|
|
||||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "Upload Debug Symbols to Sentry";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
|
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh\"\n";
|
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
@@ -375,7 +401,7 @@
|
|||||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||||
BBC1EC435C914364BBC9058D /* noop-file.swift in Sources */,
|
E52FFD661AC14FC08C4F239D /* noop-file.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -389,8 +415,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -398,10 +423,7 @@
|
|||||||
);
|
);
|
||||||
INFOPLIST_FILE = Sauci/Info.plist;
|
INFOPLIST_FILE = Sauci/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -416,6 +438,9 @@
|
|||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1";
|
TARGETED_DEVICE_FAMILY = "1";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
DEVELOPMENT_TEAM = "BXVPBATDZY";
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -426,14 +451,10 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
|
||||||
INFOPLIST_FILE = Sauci/Info.plist;
|
INFOPLIST_FILE = Sauci/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -447,6 +468,9 @@
|
|||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1";
|
TARGETED_DEVICE_FAMILY = "1";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
DEVELOPMENT_TEAM = "BXVPBATDZY";
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -497,11 +521,19 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
HEADER_SEARCH_PATHS = (
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
/usr/lib/swift,
|
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||||
);
|
);
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@@ -556,11 +588,19 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
HEADER_SEARCH_PATHS = (
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
/usr/lib/swift,
|
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||||
);
|
);
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
|
#import <Firebase/Firebase.h>
|
||||||
|
|
||||||
#import <React/RCTBundleURLProvider.h>
|
#import <React/RCTBundleURLProvider.h>
|
||||||
#import <React/RCTLinkingManager.h>
|
#import <React/RCTLinkingManager.h>
|
||||||
@@ -7,6 +8,9 @@
|
|||||||
|
|
||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||||
{
|
{
|
||||||
|
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
|
||||||
|
[FIRApp configure];
|
||||||
|
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
|
||||||
self.moduleName = @"main";
|
self.moduleName = @"main";
|
||||||
|
|
||||||
// You can add your custom initial props in the dictionary below.
|
// You can add your custom initial props in the dictionary below.
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyDlJoqvK9ONUVAKxDBFTGZg1I7slm2DuJ4</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>764866133492</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.sauci.app</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>sauci-6840e</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>sauci-6840e.firebasestorage.app</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:764866133492:ios:0290bfbe613a4f48fe4da1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.0</string>
|
<string>1.0.2</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>25</string>
|
<string>30</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>CA92.1</string>
|
<string>CA92.1</string>
|
||||||
|
<string>1C8F.1</string>
|
||||||
<string>C56D.1</string>
|
<string>C56D.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -25,11 +25,9 @@ config.resolver.nodeModulesPaths = [
|
|||||||
// of the workspace root, unless they are installed in the project root.
|
// of the workspace root, unless they are installed in the project root.
|
||||||
config.resolver.disableHierarchicalLookup = true;
|
config.resolver.disableHierarchicalLookup = true;
|
||||||
|
|
||||||
// 4. E2EE crypto polyfill: resolve 'crypto' to react-native-quick-crypto
|
// 4. Keep extraNodeModules in case we need to add monorepo overrides later
|
||||||
// This enables Web Crypto API for encryption operations
|
|
||||||
config.resolver.extraNodeModules = {
|
config.resolver.extraNodeModules = {
|
||||||
...config.resolver.extraNodeModules,
|
...config.resolver.extraNodeModules,
|
||||||
crypto: require.resolve('react-native-quick-crypto'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
@@ -14,12 +14,16 @@
|
|||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:ci": "jest --runInBand --ci",
|
"test:ci": "jest --runInBand --ci",
|
||||||
"release:ios": "dotenv -e .env -- npx expo prebuild --platform ios && dotenv -e .env -- eas build --platform ios --profile production --local",
|
"release:ios": "dotenv -e .env -- npx expo prebuild --platform ios && dotenv -e .env -- eas build --platform ios --profile production --local",
|
||||||
"release:android": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile production --local"
|
"release:ios:preview": "dotenv -e .env -- npx expo prebuild --platform ios && dotenv -e .env -- eas build --platform ios --profile preview --local",
|
||||||
|
"release:android": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile production --local",
|
||||||
|
"release:android:preview": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile preview --local"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/ngrok": "^4.1.0",
|
"@expo/ngrok": "^4.1.0",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
|
"@react-native-firebase/analytics": "^23.8.2",
|
||||||
|
"@react-native-firebase/app": "^23.8.2",
|
||||||
"@react-native-google-signin/google-signin": "^16.1.1",
|
"@react-native-google-signin/google-signin": "^16.1.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.0.0",
|
"@react-navigation/bottom-tabs": "^7.0.0",
|
||||||
"@react-navigation/native": "^7.0.0",
|
"@react-navigation/native": "^7.0.0",
|
||||||
@@ -51,14 +55,12 @@
|
|||||||
"expo-router": "~4.0.0",
|
"expo-router": "~4.0.0",
|
||||||
"expo-secure-store": "~14.0.0",
|
"expo-secure-store": "~14.0.0",
|
||||||
"expo-status-bar": "~2.0.0",
|
"expo-status-bar": "~2.0.0",
|
||||||
"posthog-react-native": "^4.17.1",
|
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
"react-native-compressor": "^1.13.0",
|
"react-native-compressor": "^1.13.0",
|
||||||
"react-native-gesture-handler": "~2.20.0",
|
"react-native-gesture-handler": "~2.20.0",
|
||||||
"react-native-purchases": "^9.6.12",
|
"react-native-purchases": "^9.6.12",
|
||||||
"react-native-quick-crypto": "^0.7.11",
|
|
||||||
"react-native-reanimated": "~3.16.0",
|
"react-native-reanimated": "~3.16.0",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const { withProjectBuildGradle } = require("expo/config-plugins");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config plugin to ensure Kotlin 1.9.x is used in the Android build.
|
||||||
|
* This provides a safety net in case the default version causes issues.
|
||||||
|
*/
|
||||||
|
function withKotlinVersion(config, kotlinVersion = "1.9.25") {
|
||||||
|
return withProjectBuildGradle(config, (config) => {
|
||||||
|
if (config.modResults.language === "groovy") {
|
||||||
|
// Ensure Kotlin version is set correctly
|
||||||
|
config.modResults.contents = config.modResults.contents.replace(
|
||||||
|
/kotlinVersion = findProperty\('android\.kotlinVersion'\) \?: ['"][\d.]+['"]/,
|
||||||
|
`kotlinVersion = findProperty('android.kotlinVersion') ?: '${kotlinVersion}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = withKotlinVersion;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Sauci Mobile Release Notes
|
||||||
|
|
||||||
|
## v1.0.1 (Build 30) - January 2025
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- **Match Archiving** - Archive old matches to keep your active list focused
|
||||||
|
- **Daily Swipe Limits** - Pace your discovery with daily question limits
|
||||||
|
- **Intensity Preferences** - Set your comfort level for question intensity (1-5 scale)
|
||||||
|
- **Infinite Scroll** - Smoother browsing through Matches and Answers with infinite scroll
|
||||||
|
- **Chat Date Separators** - Easily see when conversations happened
|
||||||
|
- **Lifecycle Notifications** - Get reminded to check in with your partner
|
||||||
|
- **Message Reporting** - Report inappropriate messages for review
|
||||||
|
- **Account Deletion** - Full account deletion option in settings
|
||||||
|
- **Reset Progress** - Start fresh by resetting your question progress
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- **Enhanced Swipe UI** - Smoother swiping experience with visual refinements
|
||||||
|
- **Improved Chat Layout** - Match card moved into message list for cleaner design
|
||||||
|
- **Keyboard Behavior** - Chat header collapses when keyboard opens for more screen space
|
||||||
|
- **Profile Settings** - New appearance and notification preferences
|
||||||
|
- **Premium Design** - Updated subscription card with gold styling
|
||||||
|
- **Version Display** - App version now visible in settings
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fixed subscription status incorrectly showing "Via partner's" after direct purchase
|
||||||
|
- Fixed Android navigation issues
|
||||||
|
- Various performance improvements and stability fixes
|
||||||
|
|
||||||
|
### Privacy
|
||||||
|
- Removed partner answered indicator for enhanced privacy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.0 (Build 25) - January 8, 2025
|
||||||
|
|
||||||
|
Initial release.
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Pressable,
|
Pressable,
|
||||||
Animated,
|
Animated,
|
||||||
|
Keyboard,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
@@ -106,6 +107,9 @@ export const MatchNotificationModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
// Dismiss keyboard so it doesn't block the modal buttons
|
||||||
|
Keyboard.dismiss();
|
||||||
|
|
||||||
// Reset animations
|
// Reset animations
|
||||||
modalScale.setValue(0.8);
|
modalScale.setValue(0.8);
|
||||||
modalOpacity.setValue(0);
|
modalOpacity.setValue(0);
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const ChatMessagesComponent: React.FC<ChatMessagesProps> = ({
|
|||||||
style={styles.messageList}
|
style={styles.messageList}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
|
keyboardDismissMode="on-drag"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
|
|||||||
@@ -1,7 +1,77 @@
|
|||||||
import PostHog from "posthog-react-native";
|
import { Platform } from "react-native";
|
||||||
|
import Constants from "expo-constants";
|
||||||
let posthog: PostHog | null = null;
|
// Import default export to ensure namespace registration runs
|
||||||
let isInitialized = false;
|
import analytics, {
|
||||||
|
getAnalytics,
|
||||||
|
logEvent as firebaseLogEvent,
|
||||||
|
setUserId as firebaseSetUserId,
|
||||||
|
setUserProperty,
|
||||||
|
setAnalyticsCollectionEnabled,
|
||||||
|
resetAnalyticsData,
|
||||||
|
FirebaseAnalyticsTypes,
|
||||||
|
} from "@react-native-firebase/analytics";
|
||||||
|
|
||||||
|
// Force the analytics module to be retained (prevents tree-shaking)
|
||||||
|
const _analyticsModule = analytics;
|
||||||
|
|
||||||
|
let isInitialized = false;
|
||||||
|
let analyticsInstance: FirebaseAnalyticsTypes.Module | null = null;
|
||||||
|
let analyticsUnavailable = false;
|
||||||
|
let analyticsUnavailableLogged = false;
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
if (!error) return "";
|
||||||
|
if (typeof error === "string") return error;
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
|
if (typeof (error as { message?: unknown }).message === "string") {
|
||||||
|
return (error as { message?: string }).message || "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAnalyticsSupported(): boolean {
|
||||||
|
if (Platform.OS === "web") return false;
|
||||||
|
if (Constants.appOwnership === "expo") return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logAnalyticsUnavailableOnce(reason: string) {
|
||||||
|
if (analyticsUnavailableLogged) return;
|
||||||
|
analyticsUnavailableLogged = true;
|
||||||
|
if (__DEV__) console.log("[Analytics] " + reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnalyticsInstance(): FirebaseAnalyticsTypes.Module | null {
|
||||||
|
if (analyticsUnavailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAnalyticsSupported()) {
|
||||||
|
analyticsUnavailable = true;
|
||||||
|
logAnalyticsUnavailableOnce("Skipping Firebase Analytics on Expo Go or web.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (analyticsInstance) {
|
||||||
|
return analyticsInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
analyticsInstance = getAnalytics();
|
||||||
|
return analyticsInstance;
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
if (message.includes("module could not be found")) {
|
||||||
|
analyticsUnavailable = true;
|
||||||
|
logAnalyticsUnavailableOnce(
|
||||||
|
"Firebase Analytics native module missing. Rebuild with a dev client or native app."
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
console.warn("[Analytics] Failed to get analytics instance:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function initAnalytics() {
|
export async function initAnalytics() {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
@@ -9,30 +79,19 @@ export async function initAnalytics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip initialization in non-RN environments (e.g., during tsc/Node.js)
|
// Skip initialization in non-RN environments (e.g., during tsc/Node.js)
|
||||||
// AsyncStorage requires window to be defined
|
|
||||||
if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
|
if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = process.env.EXPO_PUBLIC_POSTHOG_API_KEY;
|
|
||||||
const host = process.env.EXPO_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com";
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
console.warn("[Analytics] PostHog API key not configured");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
posthog = new PostHog(apiKey, {
|
const analytics = getAnalyticsInstance();
|
||||||
host,
|
if (analytics) {
|
||||||
// Disable automatic capture in production for privacy
|
await setAnalyticsCollectionEnabled(analytics, true);
|
||||||
captureAppLifecycleEvents: true,
|
isInitialized = true;
|
||||||
});
|
console.log("[Analytics] Firebase Analytics initialized");
|
||||||
|
}
|
||||||
isInitialized = true;
|
|
||||||
console.log("[Analytics] PostHog initialized");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Analytics] Failed to initialize PostHog:", error);
|
console.warn("[Analytics] Failed to initialize Firebase Analytics:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,31 +101,41 @@ export function logEvent(
|
|||||||
params?: Record<string, string | number | boolean>
|
params?: Record<string, string | number | boolean>
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (__DEV__) console.log("[Analytics] Event:", name, params);
|
if (__DEV__) console.log("[Analytics] Event:", name, params);
|
||||||
posthog?.capture(name, params);
|
const analytics = getAnalyticsInstance();
|
||||||
} catch (error) {
|
if (analytics) {
|
||||||
console.warn("[Analytics] Failed to log event:", error);
|
firebaseLogEvent(analytics, name, params);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Analytics] Failed to log event:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set user ID for tracking
|
// Set user ID for tracking
|
||||||
export function setUserId(userId: string) {
|
export function setUserId(userId: string) {
|
||||||
try {
|
try {
|
||||||
if (__DEV__) console.log("[Analytics] Set user ID:", userId);
|
if (__DEV__) console.log("[Analytics] Set user ID:", userId);
|
||||||
posthog?.identify(userId);
|
const analytics = getAnalyticsInstance();
|
||||||
} catch (error) {
|
if (analytics) {
|
||||||
console.warn("[Analytics] Failed to set user ID:", error);
|
firebaseSetUserId(analytics, userId);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Analytics] Failed to set user ID:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear user ID on logout
|
// Clear user ID on logout
|
||||||
export function clearUserId() {
|
export function clearUserId() {
|
||||||
try {
|
try {
|
||||||
if (__DEV__) console.log("[Analytics] Cleared user ID");
|
if (__DEV__) console.log("[Analytics] Cleared user ID");
|
||||||
posthog?.reset();
|
const analytics = getAnalyticsInstance();
|
||||||
} catch (error) {
|
if (analytics) {
|
||||||
console.warn("[Analytics] Failed to clear user ID:", error);
|
firebaseSetUserId(analytics, null);
|
||||||
}
|
resetAnalyticsData(analytics);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Analytics] Failed to clear user ID:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set user properties
|
// Set user properties
|
||||||
@@ -74,25 +143,30 @@ export function setUserProperties(
|
|||||||
properties: Record<string, string | null>
|
properties: Record<string, string | null>
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (__DEV__) console.log("[Analytics] Set user properties:", properties);
|
if (__DEV__) console.log("[Analytics] Set user properties:", properties);
|
||||||
// Filter out null values for PostHog
|
const analytics = getAnalyticsInstance();
|
||||||
const filteredProps: Record<string, string> = {};
|
if (analytics) {
|
||||||
for (const [key, value] of Object.entries(properties)) {
|
// Firebase Analytics requires setting user properties one at a time
|
||||||
if (value !== null) {
|
for (const [key, value] of Object.entries(properties)) {
|
||||||
filteredProps[key] = value;
|
setUserProperty(analytics, key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
posthog?.capture("$set", { $set: filteredProps });
|
} catch (error) {
|
||||||
} catch (error) {
|
console.warn("[Analytics] Failed to set user properties:", error);
|
||||||
console.warn("[Analytics] Failed to set user properties:", error);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log screen view
|
// Log screen view
|
||||||
export function logScreenView(screenName: string, screenClass?: string) {
|
export function logScreenView(screenName: string, screenClass?: string) {
|
||||||
try {
|
try {
|
||||||
if (__DEV__) console.log("[Analytics] Screen view:", screenName);
|
if (__DEV__) console.log("[Analytics] Screen view:", screenName);
|
||||||
posthog?.screen(screenName, { screen_class: screenClass ?? screenName });
|
const analytics = getAnalyticsInstance();
|
||||||
|
if (analytics) {
|
||||||
|
firebaseLogEvent(analytics, "screen_view", {
|
||||||
|
screen_name: screenName,
|
||||||
|
screen_class: screenClass ?? screenName,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Analytics] Failed to log screen view:", error);
|
console.warn("[Analytics] Failed to log screen view:", error);
|
||||||
}
|
}
|
||||||
@@ -100,11 +174,9 @@ export function logScreenView(screenName: string, screenClass?: string) {
|
|||||||
|
|
||||||
// Flush events (useful before app backgrounding)
|
// Flush events (useful before app backgrounding)
|
||||||
export function flush() {
|
export function flush() {
|
||||||
try {
|
// Firebase Analytics batches and sends events automatically
|
||||||
posthog?.flush();
|
// No manual flush needed, but we keep the function for API compatibility
|
||||||
} catch (error) {
|
if (__DEV__) console.log("[Analytics] Flush called (no-op for Firebase)");
|
||||||
console.warn("[Analytics] Failed to flush:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-defined event helpers for common actions
|
// Pre-defined event helpers for common actions
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Crypto polyfill for React Native
|
|
||||||
*
|
|
||||||
* This file must be imported BEFORE any code that uses crypto.
|
|
||||||
* It installs react-native-quick-crypto as the global crypto implementation.
|
|
||||||
*
|
|
||||||
* On web, we skip this since the native Web Crypto API is available.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Platform } from 'react-native';
|
|
||||||
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
try {
|
|
||||||
// Import react-native-quick-crypto to install its polyfill
|
|
||||||
const QuickCrypto = require('react-native-quick-crypto');
|
|
||||||
|
|
||||||
// Install the polyfill globally if not already present
|
|
||||||
if (typeof global.crypto === 'undefined') {
|
|
||||||
(global as any).crypto = QuickCrypto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also ensure subtle is available
|
|
||||||
if (global.crypto && !global.crypto.subtle) {
|
|
||||||
(global.crypto as any).subtle = QuickCrypto.subtle;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[Crypto] react-native-quick-crypto polyfill installed');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Crypto] Failed to install crypto polyfill:', error);
|
|
||||||
console.error('[Crypto] Make sure you have run: npx expo prebuild --clean');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -120,16 +120,15 @@ jest.mock('base64-arraybuffer', () => ({
|
|||||||
decode: jest.fn(() => new ArrayBuffer(0)),
|
decode: jest.fn(() => new ArrayBuffer(0)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('posthog-react-native', () => {
|
jest.mock('@react-native-firebase/app', () => ({
|
||||||
return class PostHog {
|
getApp: jest.fn(() => ({})),
|
||||||
capture = jest.fn();
|
}));
|
||||||
identify = jest.fn();
|
|
||||||
reset = jest.fn();
|
|
||||||
screen = jest.fn();
|
|
||||||
flush = jest.fn();
|
|
||||||
|
|
||||||
constructor() {
|
jest.mock('@react-native-firebase/analytics', () => ({
|
||||||
// no-op
|
getAnalytics: jest.fn(() => ({})),
|
||||||
}
|
logEvent: jest.fn(),
|
||||||
};
|
setUserId: jest.fn(),
|
||||||
});
|
setUserProperty: jest.fn(),
|
||||||
|
setAnalyticsCollectionEnabled: jest.fn(() => Promise.resolve()),
|
||||||
|
resetAnalyticsData: jest.fn(),
|
||||||
|
}));
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||||
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send delayed pack change notifications.
|
||||||
|
* Called by pg_cron every 5 minutes to check for pending notifications
|
||||||
|
* where notify_at has passed.
|
||||||
|
*
|
||||||
|
* Notifications are sent when a user enables new question packs. The notification
|
||||||
|
* is delayed by 30 minutes, with the timer resetting on each new pack enable.
|
||||||
|
* This prevents spam when a user enables multiple packs in quick succession.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ExpoPushMessage {
|
||||||
|
to: string;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
sound?: string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendExpoPushNotifications(messages: ExpoPushMessage[]) {
|
||||||
|
if (messages.length === 0) return;
|
||||||
|
|
||||||
|
const response = await fetch("https://exp.host/--/api/v2/push/send", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(messages),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
console.error("Expo push error:", error);
|
||||||
|
throw new Error(`Failed to send push notification: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.serve(async (req) => {
|
||||||
|
// Only allow POST (from cron) or GET (for manual testing)
|
||||||
|
if (req.method !== "POST" && req.method !== "GET") {
|
||||||
|
return new Response("Method not allowed", { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get("SUPABASE_URL")!,
|
||||||
|
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all pending notifications where notify_at has passed
|
||||||
|
const { data: pendingNotifications, error: fetchError } = await supabase
|
||||||
|
.from("pending_pack_notifications")
|
||||||
|
.select("id, couple_id, changed_by_user_id, notify_at")
|
||||||
|
.lte("notify_at", new Date().toISOString());
|
||||||
|
|
||||||
|
if (fetchError) {
|
||||||
|
console.error("Error fetching pending notifications:", fetchError);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: fetchError.message }),
|
||||||
|
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pendingNotifications || pendingNotifications.length === 0) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ sent: 0, message: "No pending notifications" }),
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: ExpoPushMessage[] = [];
|
||||||
|
const processedIds: string[] = [];
|
||||||
|
|
||||||
|
for (const notification of pendingNotifications) {
|
||||||
|
// Get the partner's push token (not the user who made changes)
|
||||||
|
const { data: partner, error: partnerError } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.select("push_token")
|
||||||
|
.eq("couple_id", notification.couple_id)
|
||||||
|
.neq("id", notification.changed_by_user_id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (partnerError) {
|
||||||
|
console.error(
|
||||||
|
`Error fetching partner for couple ${notification.couple_id}:`,
|
||||||
|
partnerError
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partner?.push_token) {
|
||||||
|
messages.push({
|
||||||
|
to: partner.push_token,
|
||||||
|
title: "Question packs updated",
|
||||||
|
body: "Your partner updated the question packs",
|
||||||
|
sound: "default",
|
||||||
|
data: { type: "pack_change" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processedIds.push(notification.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all notifications
|
||||||
|
if (messages.length > 0) {
|
||||||
|
await sendExpoPushNotifications(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete processed notifications
|
||||||
|
if (processedIds.length > 0) {
|
||||||
|
const { error: deleteError } = await supabase
|
||||||
|
.from("pending_pack_notifications")
|
||||||
|
.delete()
|
||||||
|
.in("id", processedIds);
|
||||||
|
|
||||||
|
if (deleteError) {
|
||||||
|
console.error("Error deleting processed notifications:", deleteError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Sent ${messages.length} pack change notifications`);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
sent: messages.length,
|
||||||
|
processed: processedIds.length,
|
||||||
|
}),
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Unexpected error:", err);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "Internal server error" }),
|
||||||
|
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,228 @@
|
|||||||
|
-- Randomize question order for variety
|
||||||
|
-- Changes ORDER BY from intensity ASC to RANDOM()
|
||||||
|
-- Keeps partner-answered priority for gap resolution
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_recommended_questions(target_pack_id UUID DEFAULT NULL)
|
||||||
|
RETURNS TABLE(
|
||||||
|
id UUID,
|
||||||
|
text TEXT,
|
||||||
|
partner_text TEXT,
|
||||||
|
is_two_part BOOLEAN,
|
||||||
|
pack_id UUID,
|
||||||
|
intensity INTEGER,
|
||||||
|
partner_answered BOOLEAN,
|
||||||
|
allowed_couple_genders TEXT[],
|
||||||
|
target_user_genders TEXT[]
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
v_couple_id UUID;
|
||||||
|
v_partner_id UUID;
|
||||||
|
v_user_gender TEXT;
|
||||||
|
v_partner_gender TEXT;
|
||||||
|
v_couple_type TEXT;
|
||||||
|
v_has_premium BOOLEAN;
|
||||||
|
v_gap_threshold INTEGER;
|
||||||
|
v_my_unanswered INTEGER;
|
||||||
|
v_partner_unanswered INTEGER;
|
||||||
|
v_net_gap INTEGER;
|
||||||
|
v_user_max_intensity INTEGER;
|
||||||
|
v_daily_limit INTEGER;
|
||||||
|
v_daily_count INTEGER;
|
||||||
|
v_today_start TIMESTAMPTZ;
|
||||||
|
v_tomorrow_start TIMESTAMPTZ;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := auth.uid();
|
||||||
|
|
||||||
|
SELECT p.couple_id, p.gender, COALESCE(p.max_intensity, 5)
|
||||||
|
INTO v_couple_id, v_user_gender, v_user_max_intensity
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.id = v_user_id;
|
||||||
|
|
||||||
|
IF v_couple_id IS NULL THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT p.id, p.gender INTO v_partner_id, v_partner_gender
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.couple_id = v_couple_id AND p.id != v_user_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- Check premium access (used for both answer gap and daily limit)
|
||||||
|
v_has_premium := public.has_premium_access(v_user_id);
|
||||||
|
|
||||||
|
-- Check daily response limit (only when not browsing a specific pack and user is non-premium)
|
||||||
|
IF v_partner_id IS NOT NULL AND target_pack_id IS NULL AND NOT v_has_premium THEN
|
||||||
|
-- Get daily limit from app_config
|
||||||
|
SELECT COALESCE(ac.daily_response_limit, 0) INTO v_daily_limit
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If daily limit > 0, check if user has reached it
|
||||||
|
IF v_daily_limit > 0 THEN
|
||||||
|
-- Calculate UTC day boundaries
|
||||||
|
v_today_start := date_trunc('day', NOW() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC';
|
||||||
|
v_tomorrow_start := v_today_start + INTERVAL '1 day';
|
||||||
|
|
||||||
|
-- Count responses created today (UTC)
|
||||||
|
SELECT COUNT(*)::INTEGER INTO v_daily_count
|
||||||
|
FROM responses r
|
||||||
|
WHERE r.user_id = v_user_id
|
||||||
|
AND r.created_at >= v_today_start
|
||||||
|
AND r.created_at < v_tomorrow_start;
|
||||||
|
|
||||||
|
-- If over daily limit, return empty (old apps see "All caught up!")
|
||||||
|
IF v_daily_count >= v_daily_limit THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Check answer gap threshold using NET gap (only when not browsing a specific pack)
|
||||||
|
IF v_partner_id IS NOT NULL AND target_pack_id IS NULL THEN
|
||||||
|
-- Get threshold from app_config
|
||||||
|
SELECT COALESCE(ac.answer_gap_threshold, 10) INTO v_gap_threshold
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If threshold > 0, check the NET gap
|
||||||
|
IF v_gap_threshold > 0 THEN
|
||||||
|
-- Calculate NET gap to prevent deadlocks
|
||||||
|
WITH enabled_packs AS (
|
||||||
|
SELECT cp.pack_id
|
||||||
|
FROM couple_packs cp
|
||||||
|
WHERE cp.couple_id = v_couple_id AND cp.enabled = true
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
-- Questions I answered that partner hasn't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_user_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_partner_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer,
|
||||||
|
-- Questions partner answered that I haven't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_partner_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_user_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer
|
||||||
|
INTO v_my_unanswered, v_partner_unanswered
|
||||||
|
FROM responses r
|
||||||
|
INNER JOIN questions q ON q.id = r.question_id
|
||||||
|
WHERE r.couple_id = v_couple_id
|
||||||
|
AND r.user_id IN (v_user_id, v_partner_id)
|
||||||
|
AND EXISTS (SELECT 1 FROM enabled_packs)
|
||||||
|
AND q.pack_id IN (SELECT ep.pack_id FROM enabled_packs ep);
|
||||||
|
|
||||||
|
-- Net gap: only positive values mean user is truly ahead
|
||||||
|
v_net_gap := GREATEST(0, v_my_unanswered - v_partner_unanswered);
|
||||||
|
|
||||||
|
-- If over threshold, return empty (old apps see "All caught up!")
|
||||||
|
IF v_net_gap >= v_gap_threshold THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_user_gender IS NOT NULL AND v_partner_gender IS NOT NULL THEN
|
||||||
|
IF v_user_gender < v_partner_gender THEN
|
||||||
|
v_couple_type := v_user_gender || '+' || v_partner_gender;
|
||||||
|
ELSE
|
||||||
|
v_couple_type := v_partner_gender || '+' || v_user_gender;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
WITH user_config_exists AS (
|
||||||
|
SELECT 1 FROM public.couple_packs WHERE couple_id = v_couple_id LIMIT 1
|
||||||
|
),
|
||||||
|
active_packs AS (
|
||||||
|
SELECT target_pack_id AS pack_id
|
||||||
|
WHERE target_pack_id IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.question_packs qp
|
||||||
|
WHERE qp.id = target_pack_id
|
||||||
|
AND (qp.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT cp.pack_id FROM public.couple_packs cp
|
||||||
|
INNER JOIN public.question_packs qp ON qp.id = cp.pack_id
|
||||||
|
WHERE target_pack_id IS NULL
|
||||||
|
AND cp.couple_id = v_couple_id
|
||||||
|
AND cp.enabled = TRUE
|
||||||
|
AND (qp.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
AND EXISTS (SELECT 1 FROM user_config_exists)
|
||||||
|
UNION ALL
|
||||||
|
SELECT p.id AS pack_id FROM public.question_packs p
|
||||||
|
WHERE target_pack_id IS NULL
|
||||||
|
AND p.is_public = TRUE
|
||||||
|
AND (p.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM user_config_exists)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
CASE
|
||||||
|
WHEN q.partner_text IS NOT NULL AND EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) THEN q.partner_text
|
||||||
|
ELSE q.text
|
||||||
|
END as text,
|
||||||
|
q.partner_text,
|
||||||
|
q.partner_text IS NOT NULL as is_two_part,
|
||||||
|
q.pack_id,
|
||||||
|
q.intensity,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) AS partner_answered,
|
||||||
|
q.allowed_couple_genders,
|
||||||
|
q.target_user_genders
|
||||||
|
FROM public.questions q
|
||||||
|
WHERE q.pack_id IN (SELECT ap.pack_id FROM active_packs ap)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.user_id = v_user_id
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
q.allowed_couple_genders IS NULL
|
||||||
|
OR v_couple_type IS NULL
|
||||||
|
OR v_couple_type = ANY(q.allowed_couple_genders)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
q.target_user_genders IS NULL
|
||||||
|
OR v_user_gender = ANY(q.target_user_genders)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Filter by user's max_intensity preference
|
||||||
|
AND (
|
||||||
|
q.intensity IS NULL
|
||||||
|
OR q.intensity <= v_user_max_intensity
|
||||||
|
)
|
||||||
|
-- ORDER BY: Partner-answered questions first, then RANDOM for variety
|
||||||
|
ORDER BY
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) DESC,
|
||||||
|
RANDOM();
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_recommended_questions(UUID) IS 'Returns recommended questions with partner-answered first, then randomized for variety. Uses NET gap calculation for blocking.';
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
-- Fix answer gap deadlock by calculating NET gap instead of independent gaps
|
||||||
|
-- Also add ORDER BY to get_recommended_questions to prioritize partner-answered questions
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Part 1: Fix get_answer_gap_status() to calculate NET gap
|
||||||
|
-- ============================================================================
|
||||||
|
--
|
||||||
|
-- Problem: Both partners can be "ahead" simultaneously when they answer different questions
|
||||||
|
-- Example: User A answers Q1-Q10, User B answers Q11-Q20
|
||||||
|
-- - Old: Both have gap=10, both blocked (deadlock!)
|
||||||
|
-- - New: Both have net_gap=0, neither blocked
|
||||||
|
--
|
||||||
|
-- NET gap = (questions I answered that partner hasn't) - (questions partner answered that I haven't)
|
||||||
|
-- Only positive values mean user is truly ahead
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_answer_gap_status()
|
||||||
|
RETURNS TABLE(
|
||||||
|
unanswered_by_partner integer,
|
||||||
|
threshold integer,
|
||||||
|
is_blocked boolean
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
v_couple_id UUID;
|
||||||
|
v_partner_id UUID;
|
||||||
|
v_my_unanswered integer;
|
||||||
|
v_partner_unanswered integer;
|
||||||
|
v_net_gap integer;
|
||||||
|
v_threshold integer;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := auth.uid();
|
||||||
|
|
||||||
|
-- Get user's couple_id
|
||||||
|
SELECT p.couple_id INTO v_couple_id
|
||||||
|
FROM profiles p
|
||||||
|
WHERE p.id = v_user_id;
|
||||||
|
|
||||||
|
-- No couple = no blocking needed
|
||||||
|
IF v_couple_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT 0, 0, false;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get partner's ID
|
||||||
|
SELECT p.id INTO v_partner_id
|
||||||
|
FROM profiles p
|
||||||
|
WHERE p.couple_id = v_couple_id AND p.id != v_user_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- No partner = no blocking needed
|
||||||
|
IF v_partner_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT 0, 0, false;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get threshold from app_config
|
||||||
|
SELECT COALESCE(ac.answer_gap_threshold, 10) INTO v_threshold
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If threshold is 0, feature is disabled
|
||||||
|
IF v_threshold = 0 THEN
|
||||||
|
RETURN QUERY SELECT 0, 0, false;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Calculate NET gap to prevent deadlocks
|
||||||
|
WITH enabled_packs AS (
|
||||||
|
-- Get packs that the couple has explicitly enabled
|
||||||
|
SELECT cp.pack_id
|
||||||
|
FROM couple_packs cp
|
||||||
|
WHERE cp.couple_id = v_couple_id AND cp.enabled = true
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
-- Questions I answered that partner hasn't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_user_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_partner_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer,
|
||||||
|
-- Questions partner answered that I haven't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_partner_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_user_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer
|
||||||
|
INTO v_my_unanswered, v_partner_unanswered
|
||||||
|
FROM responses r
|
||||||
|
INNER JOIN questions q ON q.id = r.question_id
|
||||||
|
WHERE r.couple_id = v_couple_id
|
||||||
|
AND r.user_id IN (v_user_id, v_partner_id)
|
||||||
|
AND EXISTS (SELECT 1 FROM enabled_packs)
|
||||||
|
AND q.pack_id IN (SELECT pack_id FROM enabled_packs);
|
||||||
|
|
||||||
|
-- Net gap: only positive values mean user is truly ahead
|
||||||
|
-- If partner has answered more, user's net gap is 0 (they're behind or even)
|
||||||
|
v_net_gap := GREATEST(0, v_my_unanswered - v_partner_unanswered);
|
||||||
|
|
||||||
|
-- Return net gap (displayed as "unanswered_by_partner" for backwards compatibility)
|
||||||
|
RETURN QUERY SELECT v_net_gap, v_threshold, (v_net_gap >= v_threshold);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_answer_gap_status() IS 'Returns answer gap status using NET gap calculation. Prevents deadlocks when both partners answer different questions.';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Part 2: Update get_recommended_questions() to prioritize partner-answered questions
|
||||||
|
-- ============================================================================
|
||||||
|
--
|
||||||
|
-- Add ORDER BY to ensure:
|
||||||
|
-- 1. Questions partner has already answered come first (helps resolve gaps)
|
||||||
|
-- 2. Lower intensity questions shown first within each group
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_recommended_questions(target_pack_id UUID DEFAULT NULL)
|
||||||
|
RETURNS TABLE(
|
||||||
|
id UUID,
|
||||||
|
text TEXT,
|
||||||
|
partner_text TEXT,
|
||||||
|
is_two_part BOOLEAN,
|
||||||
|
pack_id UUID,
|
||||||
|
intensity INTEGER,
|
||||||
|
partner_answered BOOLEAN,
|
||||||
|
allowed_couple_genders TEXT[],
|
||||||
|
target_user_genders TEXT[]
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
v_couple_id UUID;
|
||||||
|
v_partner_id UUID;
|
||||||
|
v_user_gender TEXT;
|
||||||
|
v_partner_gender TEXT;
|
||||||
|
v_couple_type TEXT;
|
||||||
|
v_has_premium BOOLEAN;
|
||||||
|
v_gap_threshold INTEGER;
|
||||||
|
v_my_unanswered INTEGER;
|
||||||
|
v_partner_unanswered INTEGER;
|
||||||
|
v_net_gap INTEGER;
|
||||||
|
v_user_max_intensity INTEGER;
|
||||||
|
v_daily_limit INTEGER;
|
||||||
|
v_daily_count INTEGER;
|
||||||
|
v_today_start TIMESTAMPTZ;
|
||||||
|
v_tomorrow_start TIMESTAMPTZ;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := auth.uid();
|
||||||
|
|
||||||
|
SELECT p.couple_id, p.gender, COALESCE(p.max_intensity, 5)
|
||||||
|
INTO v_couple_id, v_user_gender, v_user_max_intensity
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.id = v_user_id;
|
||||||
|
|
||||||
|
IF v_couple_id IS NULL THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT p.id, p.gender INTO v_partner_id, v_partner_gender
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.couple_id = v_couple_id AND p.id != v_user_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- Check premium access (used for both answer gap and daily limit)
|
||||||
|
v_has_premium := public.has_premium_access(v_user_id);
|
||||||
|
|
||||||
|
-- Check daily response limit (only when not browsing a specific pack and user is non-premium)
|
||||||
|
IF v_partner_id IS NOT NULL AND target_pack_id IS NULL AND NOT v_has_premium THEN
|
||||||
|
-- Get daily limit from app_config
|
||||||
|
SELECT COALESCE(ac.daily_response_limit, 0) INTO v_daily_limit
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If daily limit > 0, check if user has reached it
|
||||||
|
IF v_daily_limit > 0 THEN
|
||||||
|
-- Calculate UTC day boundaries
|
||||||
|
v_today_start := date_trunc('day', NOW() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC';
|
||||||
|
v_tomorrow_start := v_today_start + INTERVAL '1 day';
|
||||||
|
|
||||||
|
-- Count responses created today (UTC)
|
||||||
|
SELECT COUNT(*)::INTEGER INTO v_daily_count
|
||||||
|
FROM responses r
|
||||||
|
WHERE r.user_id = v_user_id
|
||||||
|
AND r.created_at >= v_today_start
|
||||||
|
AND r.created_at < v_tomorrow_start;
|
||||||
|
|
||||||
|
-- If over daily limit, return empty (old apps see "All caught up!")
|
||||||
|
IF v_daily_count >= v_daily_limit THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Check answer gap threshold using NET gap (only when not browsing a specific pack)
|
||||||
|
IF v_partner_id IS NOT NULL AND target_pack_id IS NULL THEN
|
||||||
|
-- Get threshold from app_config
|
||||||
|
SELECT COALESCE(ac.answer_gap_threshold, 10) INTO v_gap_threshold
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If threshold > 0, check the NET gap
|
||||||
|
IF v_gap_threshold > 0 THEN
|
||||||
|
-- Calculate NET gap to prevent deadlocks
|
||||||
|
WITH enabled_packs AS (
|
||||||
|
SELECT cp.pack_id
|
||||||
|
FROM couple_packs cp
|
||||||
|
WHERE cp.couple_id = v_couple_id AND cp.enabled = true
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
-- Questions I answered that partner hasn't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_user_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_partner_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer,
|
||||||
|
-- Questions partner answered that I haven't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_partner_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_user_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer
|
||||||
|
INTO v_my_unanswered, v_partner_unanswered
|
||||||
|
FROM responses r
|
||||||
|
INNER JOIN questions q ON q.id = r.question_id
|
||||||
|
WHERE r.couple_id = v_couple_id
|
||||||
|
AND r.user_id IN (v_user_id, v_partner_id)
|
||||||
|
AND EXISTS (SELECT 1 FROM enabled_packs)
|
||||||
|
AND q.pack_id IN (SELECT ep.pack_id FROM enabled_packs ep);
|
||||||
|
|
||||||
|
-- Net gap: only positive values mean user is truly ahead
|
||||||
|
v_net_gap := GREATEST(0, v_my_unanswered - v_partner_unanswered);
|
||||||
|
|
||||||
|
-- If over threshold, return empty (old apps see "All caught up!")
|
||||||
|
IF v_net_gap >= v_gap_threshold THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_user_gender IS NOT NULL AND v_partner_gender IS NOT NULL THEN
|
||||||
|
IF v_user_gender < v_partner_gender THEN
|
||||||
|
v_couple_type := v_user_gender || '+' || v_partner_gender;
|
||||||
|
ELSE
|
||||||
|
v_couple_type := v_partner_gender || '+' || v_user_gender;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
WITH user_config_exists AS (
|
||||||
|
SELECT 1 FROM public.couple_packs WHERE couple_id = v_couple_id LIMIT 1
|
||||||
|
),
|
||||||
|
active_packs AS (
|
||||||
|
SELECT target_pack_id AS pack_id
|
||||||
|
WHERE target_pack_id IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.question_packs qp
|
||||||
|
WHERE qp.id = target_pack_id
|
||||||
|
AND (qp.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT cp.pack_id FROM public.couple_packs cp
|
||||||
|
INNER JOIN public.question_packs qp ON qp.id = cp.pack_id
|
||||||
|
WHERE target_pack_id IS NULL
|
||||||
|
AND cp.couple_id = v_couple_id
|
||||||
|
AND cp.enabled = TRUE
|
||||||
|
AND (qp.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
AND EXISTS (SELECT 1 FROM user_config_exists)
|
||||||
|
UNION ALL
|
||||||
|
SELECT p.id AS pack_id FROM public.question_packs p
|
||||||
|
WHERE target_pack_id IS NULL
|
||||||
|
AND p.is_public = TRUE
|
||||||
|
AND (p.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM user_config_exists)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
CASE
|
||||||
|
WHEN q.partner_text IS NOT NULL AND EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) THEN q.partner_text
|
||||||
|
ELSE q.text
|
||||||
|
END as text,
|
||||||
|
q.partner_text,
|
||||||
|
q.partner_text IS NOT NULL as is_two_part,
|
||||||
|
q.pack_id,
|
||||||
|
q.intensity,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) AS partner_answered,
|
||||||
|
q.allowed_couple_genders,
|
||||||
|
q.target_user_genders
|
||||||
|
FROM public.questions q
|
||||||
|
WHERE q.pack_id IN (SELECT ap.pack_id FROM active_packs ap)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.user_id = v_user_id
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
q.allowed_couple_genders IS NULL
|
||||||
|
OR v_couple_type IS NULL
|
||||||
|
OR v_couple_type = ANY(q.allowed_couple_genders)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
q.target_user_genders IS NULL
|
||||||
|
OR v_user_gender = ANY(q.target_user_genders)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Filter by user's max_intensity preference
|
||||||
|
AND (
|
||||||
|
q.intensity IS NULL
|
||||||
|
OR q.intensity <= v_user_max_intensity
|
||||||
|
)
|
||||||
|
-- ORDER BY: Partner-answered questions first to help resolve gaps, then by intensity
|
||||||
|
ORDER BY
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) DESC,
|
||||||
|
q.intensity ASC;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_recommended_questions(UUID) IS 'Returns recommended questions prioritizing partner-answered questions first to prevent deadlocks. Uses NET gap calculation for blocking.';
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
-- Add delayed pack change notification system
|
||||||
|
-- Notifies partner when a user enables new question packs (with 30-min debounce)
|
||||||
|
|
||||||
|
-- Create pending notifications table
|
||||||
|
CREATE TABLE pending_pack_notifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
couple_id UUID NOT NULL REFERENCES couples(id) ON DELETE CASCADE,
|
||||||
|
changed_by_user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||||
|
notify_at TIMESTAMPTZ NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
UNIQUE(couple_id) -- Only one pending notification per couple (enables UPSERT for timer reset)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for efficient cron queries
|
||||||
|
CREATE INDEX idx_pending_pack_notifications_notify_at
|
||||||
|
ON pending_pack_notifications(notify_at);
|
||||||
|
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE pending_pack_notifications ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- RLS policies
|
||||||
|
CREATE POLICY "Users can view their couple's pending notifications"
|
||||||
|
ON pending_pack_notifications FOR SELECT
|
||||||
|
USING (couple_id = get_auth_user_couple_id());
|
||||||
|
|
||||||
|
CREATE POLICY "Super admins can view all pending notifications"
|
||||||
|
ON pending_pack_notifications FOR SELECT
|
||||||
|
USING (is_super_admin());
|
||||||
|
|
||||||
|
-- Updated_at trigger
|
||||||
|
CREATE TRIGGER update_pending_pack_notifications_updated_at
|
||||||
|
BEFORE UPDATE ON pending_pack_notifications
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||||
|
|
||||||
|
-- Function to queue pack change notifications
|
||||||
|
CREATE OR REPLACE FUNCTION queue_pack_change_notification()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
BEGIN
|
||||||
|
-- Only trigger when enabling a pack (not disabling)
|
||||||
|
IF NEW.enabled = false THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- For UPDATE, only proceed if changing from disabled to enabled
|
||||||
|
IF TG_OP = 'UPDATE' AND OLD.enabled = true THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get the user making the change
|
||||||
|
v_user_id := auth.uid();
|
||||||
|
|
||||||
|
-- Skip if no auth context (service role operations)
|
||||||
|
IF v_user_id IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- UPSERT: Insert new notification or reset timer on existing one
|
||||||
|
INSERT INTO pending_pack_notifications (
|
||||||
|
couple_id,
|
||||||
|
changed_by_user_id,
|
||||||
|
notify_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
NEW.couple_id,
|
||||||
|
v_user_id,
|
||||||
|
now() + interval '30 minutes'
|
||||||
|
)
|
||||||
|
ON CONFLICT (couple_id) DO UPDATE SET
|
||||||
|
changed_by_user_id = EXCLUDED.changed_by_user_id,
|
||||||
|
notify_at = now() + interval '30 minutes',
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Create trigger on couple_packs
|
||||||
|
CREATE TRIGGER on_couple_pack_enabled
|
||||||
|
AFTER INSERT OR UPDATE OF enabled ON couple_packs
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION queue_pack_change_notification();
|
||||||
|
|
||||||
|
-- Schedule cron job (runs every 5 minutes)
|
||||||
|
SELECT cron.schedule(
|
||||||
|
'send-pack-change-notifications',
|
||||||
|
'*/5 * * * *',
|
||||||
|
$$
|
||||||
|
SELECT net.http_post(
|
||||||
|
url := 'https://ckjcrkjpmhqhiucifukx.supabase.co/functions/v1/send-pack-change-notification',
|
||||||
|
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||||
|
body := '{}'::jsonb
|
||||||
|
)
|
||||||
|
$$
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 16.28.0",
|
||||||
|
"appVersionSource": "remote"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"development": {
|
||||||
|
"developmentClient": true,
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"autoIncrement": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+4196
-1543
File diff suppressed because it is too large.
Load diff
+12
-11
@@ -19,14 +19,15 @@
|
|||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"packageManager": "npm@10.2.0",
|
"packageManager": "npm@10.2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"rollup": "4.54.0"
|
"rollup": "4.54.0",
|
||||||
},
|
"react-native": "0.76.9"
|
||||||
"optionalDependencies": {
|
},
|
||||||
"@rollup/rollup-linux-x64-musl": "4.54.0",
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.54.0"
|
"@rollup/rollup-linux-x64-musl": "4.54.0",
|
||||||
}
|
"@rollup/rollup-linux-arm64-musl": "4.54.0"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user