mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat: add live draw feature and clean up docs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
520d0a8444
commit
4c18afaeae
63 files changed
+6728
-12854
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { supabase, supabaseConfig } from '@/config';
|
||||
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Crown, Users, MessageCircle, ChevronRight, ThumbsUp, ThumbsDown, Minus, Gift, Image, Video as VideoIcon, Target, User, Eye, EyeOff, CheckCircle, Package, Sparkles, Flame, Trophy, Calendar, AlertCircle, Clock } from 'lucide-react';
|
||||
import { Crown, Users, MessageCircle, ChevronRight, ThumbsUp, ThumbsDown, Minus, Gift, Image, Video as VideoIcon, Target, User, Eye, EyeOff, CheckCircle, Package, Sparkles, Flame, Trophy, Calendar, AlertCircle, Clock, Pencil } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { IconPreview } from '@/components/ui/icon-picker';
|
||||
|
||||
@@ -280,6 +280,146 @@ function AdminDecryptedVideo({ messageId }: { messageId: string }) {
|
||||
return <video src={url} controls className="w-full h-full object-cover" preload="metadata" />;
|
||||
}
|
||||
|
||||
interface StrokePoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface StrokeSegment {
|
||||
id: string;
|
||||
userId: string;
|
||||
points: StrokePoint[];
|
||||
color: string;
|
||||
width: number;
|
||||
timestamp: number;
|
||||
isEraser: boolean;
|
||||
}
|
||||
|
||||
const CANVAS_BACKGROUND = '#1a1a2e';
|
||||
|
||||
function LiveDrawCanvas({ coupleId }: { coupleId: string }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [strokes, setStrokes] = useState<StrokeSegment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const fetchStrokes = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const { data, error: err } = await supabase
|
||||
.from('live_draw_sessions')
|
||||
.select('strokes')
|
||||
.eq('couple_id', coupleId)
|
||||
.maybeSingle();
|
||||
if (cancelled) return;
|
||||
if (err) {
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setStrokes((data?.strokes as StrokeSegment[]) || []);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchStrokes();
|
||||
|
||||
// Subscribe to real-time updates
|
||||
const channel = supabase
|
||||
.channel(`admin-livedraw-${coupleId}`)
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'live_draw_sessions',
|
||||
filter: `couple_id=eq.${coupleId}`,
|
||||
}, (payload: any) => {
|
||||
if (payload.new?.strokes) {
|
||||
setStrokes(payload.new.strokes as StrokeSegment[]);
|
||||
}
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, [coupleId]);
|
||||
|
||||
// Render strokes to canvas
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.clientWidth;
|
||||
const h = canvas.clientHeight;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
ctx.fillStyle = CANVAS_BACKGROUND;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
for (const stroke of strokes) {
|
||||
if (stroke.points.length === 0) continue;
|
||||
ctx.strokeStyle = stroke.isEraser ? CANVAS_BACKGROUND : stroke.color;
|
||||
ctx.lineWidth = stroke.width;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
|
||||
const first = stroke.points[0];
|
||||
ctx.moveTo(first.x * w, first.y * h);
|
||||
|
||||
if (stroke.points.length === 1) {
|
||||
ctx.lineTo(first.x * w + 0.1, first.y * h + 0.1);
|
||||
} else {
|
||||
for (let i = 1; i < stroke.points.length; i++) {
|
||||
const prev = stroke.points[i - 1];
|
||||
const curr = stroke.points[i];
|
||||
const midX = ((prev.x + curr.x) / 2) * w;
|
||||
const midY = ((prev.y + curr.y) / 2) * h;
|
||||
|
||||
if (i === 1) {
|
||||
ctx.lineTo(midX, midY);
|
||||
} else {
|
||||
ctx.quadraticCurveTo(prev.x * w, prev.y * h, midX, midY);
|
||||
}
|
||||
}
|
||||
const last = stroke.points[stroke.points.length - 1];
|
||||
ctx.lineTo(last.x * w, last.y * h);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}, [strokes]);
|
||||
|
||||
if (loading) return <Skeleton className="h-64 w-full" />;
|
||||
if (error) return <div className="text-sm text-red-500">Failed to load drawing: {error}</div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{strokes.length === 0 ? (
|
||||
<Card className="flex flex-col items-center justify-center py-12">
|
||||
<Pencil className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground">No drawing yet</p>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-xl overflow-hidden border" style={{ background: CANVAS_BACKGROUND }}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{ width: '100%', aspectRatio: '4/3', display: 'block' }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{strokes.length} stroke{strokes.length !== 1 ? 's' : ''} — updates in real-time</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserDetailPage() {
|
||||
const { userId } = useParams<{ userId: string }>();
|
||||
const { hasPermission } = useAuth();
|
||||
@@ -755,6 +895,7 @@ export function UserDetailPage() {
|
||||
{canViewMedia && <TabsTrigger value="media">Media ({mediaTotal})</TabsTrigger>}
|
||||
{canViewResponses && <TabsTrigger value="outstanding" disabled={!profile.couple_id || !partner}>Outstanding ({outstandingTotal}){gapDisabled && <AlertCircle className="h-3 w-3 ml-1 text-destructive" />}</TabsTrigger>}
|
||||
<TabsTrigger value="packs" disabled={!profile.couple_id}>Enabled Packs ({enabledPacks.length})</TabsTrigger>
|
||||
<TabsTrigger value="livedraw" disabled={!profile.couple_id}>Live Draw</TabsTrigger>
|
||||
</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">{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>}
|
||||
@@ -898,6 +1039,16 @@ export function UserDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="livedraw" className="mt-4">
|
||||
{profile.couple_id ? (
|
||||
<LiveDrawCanvas coupleId={profile.couple_id} />
|
||||
) : (
|
||||
<Card className="flex flex-col items-center justify-center py-12">
|
||||
<Pencil className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground">User is not in a couple</p>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
# Mobile data retrieval review: task list
|
||||
|
||||
This task list targets the **Home**, **Swipe**, and **Matches** flows in `apps/mobile`, focusing on correctness, performance, and realtime consistency.
|
||||
|
||||
## Overview (current state)
|
||||
|
||||
- Data fetching is split between Zustand stores (`useMatchStore`, `usePacksStore`) and screen-local Supabase RPC calls (notably `SwipeScreen`).
|
||||
- Realtime updates are centralized in `apps/mobile/app/(app)/_layout.tsx` (matches/messages/profile/pack-settings subscriptions).
|
||||
- Main risks today: missing query filters & error/loading states, redundant fetches, state races on param changes, and unread-count drift between realtime and the matches list.
|
||||
|
||||
## Scope
|
||||
|
||||
- Screens
|
||||
- Home: `apps/mobile/app/(app)/index.tsx`
|
||||
- Swipe: `apps/mobile/app/(app)/swipe.tsx`
|
||||
- Matches: `apps/mobile/app/(app)/matches.tsx`
|
||||
- Stores
|
||||
- Matches: `apps/mobile/src/store/matchStore.ts`
|
||||
- Packs: `apps/mobile/src/store/packsStore.ts`
|
||||
- Messages: `apps/mobile/src/store/messageStore.ts`
|
||||
- Realtime orchestration
|
||||
- `apps/mobile/app/(app)/_layout.tsx`
|
||||
|
||||
## Priority 0 — correctness (must fix)
|
||||
|
||||
- [ ] **Home: fix packs count source-of-truth**
|
||||
- Current: `enabledPacksCount = packs.length` (counts available packs).
|
||||
- Decide + implement:
|
||||
- If label means *enabled*: use `enabledPackIds.length`.
|
||||
- If label means *available*: rename the variable + label.
|
||||
- Acceptance: packs number matches user expectations and store state.
|
||||
|
||||
- [ ] **Swipe: normalize `packId` and reset state on param changes**
|
||||
- Normalize `packId` from `useLocalSearchParams()` (`string | string[]`) before sending to RPC.
|
||||
- Reset `currentIndex`, `isLoading`, and any derived UI state when `packId` changes.
|
||||
- Acceptance: switching packs never shows stale index/loading/UI.
|
||||
|
||||
- [ ] **Swipe: add race protection for overlapping async fetches**
|
||||
- Use a request-id guard (or abort pattern) so older `fetchQuestions()` results cannot overwrite newer results.
|
||||
- Acceptance: rapid navigation/focus changes never clobber newer state.
|
||||
|
||||
- [ ] **Swipe: prevent stale “blocked” state on answer-gap errors**
|
||||
- Ensure `checkAnswerGap()` clears/updates `isBlocked` + `gapInfo` deterministically even when RPC errors.
|
||||
- Acceptance: blocked UI never “sticks” due to transient failures.
|
||||
|
||||
## Priority 1 — UX and data freshness
|
||||
|
||||
- [ ] **Home: ensure data freshness on revisit**
|
||||
- Add `useFocusEffect` or a simple staleness TTL in stores.
|
||||
- Acceptance: counts (matches/new/packs) refresh when returning to Home.
|
||||
|
||||
- [ ] **Home: align “Recent Matches” semantics**
|
||||
- `useMatchStore.fetchMatches()` sorts unread first; Home slices top 3.
|
||||
- Decide:
|
||||
- True recent by `created_at`, or
|
||||
- “Top matches” by unread-first.
|
||||
- Acceptance: section name and behavior match.
|
||||
|
||||
- [ ] **Matches screen: wire up pull-to-refresh correctly**
|
||||
- Add `isLoading`/`isRefreshing` + `error` to `useMatchStore`.
|
||||
- Use those flags for `RefreshControl.refreshing`.
|
||||
- Acceptance: pull-to-refresh shows spinner and recovers from errors.
|
||||
|
||||
## Priority 2 — performance and query efficiency
|
||||
|
||||
- [ ] **MatchStore: add explicit filtering and guard empty `in(...)` lists**
|
||||
- Filter matches by `couple_id` (in addition to RLS) for clarity/perf.
|
||||
- If no matches, avoid calling `.in('question_id', [])`.
|
||||
- Acceptance: fewer noisy requests; no empty-list edge behavior.
|
||||
|
||||
- [ ] **MatchStore: reduce unread-count work**
|
||||
- Current approach fetches one row per unread message and counts on client.
|
||||
- Options:
|
||||
- Add RPC/view that returns per-match unread counts.
|
||||
- Or store a denormalized `unread_count` per match (if acceptable).
|
||||
- Acceptance: fetching matches scales with large message histories.
|
||||
|
||||
- [ ] **Swipe: remove heavy dependency on `fetchPacks()`**
|
||||
- Swipe often only needs `enabledPackIds`.
|
||||
- Add lightweight `ensureEnabledPacksLoaded()` or call `fetchEnabledPacks()` directly.
|
||||
- Acceptance: less data fetched when entering Swipe.
|
||||
|
||||
- [ ] **Swipe: replace random comparator sort**
|
||||
- Replace `Array.sort()` comparator using `Math.random()` with a stable score/shuffle pass.
|
||||
- Acceptance: consistent ordering logic; avoids comparator contract violations.
|
||||
|
||||
## Priority 3 — realtime correctness and consistency
|
||||
|
||||
- [ ] **Realtime messages: tighten subscription filter**
|
||||
- Current `messages` subscription listens without a `filter`.
|
||||
- Improve by filtering to the user’s couple scope (schema permitting) or alternative channel strategy.
|
||||
- Acceptance: avoids noise; reduces risk of unintended cross-couple events.
|
||||
|
||||
- [ ] **Sync per-match unread counts with realtime events**
|
||||
- Currently global unread count updates live; per-match counts update only on `fetchMatches()`.
|
||||
- Options:
|
||||
- Update `useMatchStore` unreadCount per match when realtime message arrives.
|
||||
- Or trigger a targeted refresh when new partner message arrives.
|
||||
- Acceptance: Matches list ordering/badges update as messages arrive.
|
||||
|
||||
## Optional: structural improvements (bigger refactors)
|
||||
|
||||
- [ ] Consolidate fetching into a query layer (e.g., TanStack Query) with caching, invalidation, retries, and focus refetch.
|
||||
- [ ] Create a single “matches list” RPC that returns:
|
||||
- match + question
|
||||
- who-answered-first info (for two-part display)
|
||||
- per-match unread counts
|
||||
- new match flag
|
||||
- [ ] Add lightweight integration tests around:
|
||||
- `packId` parsing
|
||||
- request race guards
|
||||
- unread count updates
|
||||
|
||||
## Suggested implementation order
|
||||
|
||||
1. P0 correctness (Home packs count, Swipe param/reset/races, gap-state)
|
||||
2. Matches loading/refresh UX
|
||||
3. Query efficiency (couple filters, unread-count optimization)
|
||||
4. Realtime tightening + unread sync
|
||||
5. Optional refactors
|
||||
@@ -1,573 +0,0 @@
|
||||
# Mobile App Refactoring Plan
|
||||
|
||||
A systematic plan to improve code maintainability, reduce duplication, and establish patterns for long-term scalability.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
| Metric | Current | Target |
|
||||
|--------|---------|--------|
|
||||
| Largest file (chat) | 2,213 lines | <400 lines per file |
|
||||
| Store file | 666 lines (5 stores) | ~150 lines per store |
|
||||
| Animation duplication | 3+ files | 1 shared hook |
|
||||
| UI component duplication | 6+ instances | Shared components |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Extract Shared Hooks (Low Risk, High Impact)
|
||||
|
||||
**Goal:** Eliminate animation and logic duplication across the app.
|
||||
|
||||
### 1.1 Create `useAmbientOrbAnimation` Hook
|
||||
|
||||
**Files affected:**
|
||||
- `app/(app)/swipe.tsx` (lines 41-85)
|
||||
- `app/(app)/chat/[id].tsx` (lines 70-122)
|
||||
- `src/components/SwipeCard.native.tsx` (lines 60-70)
|
||||
|
||||
**New file:** `src/hooks/useAmbientOrbAnimation.ts`
|
||||
|
||||
```typescript
|
||||
import { useEffect } from 'react';
|
||||
import { useSharedValue, withRepeat, withSequence, withTiming, useAnimatedStyle } from 'react-native-reanimated';
|
||||
|
||||
interface AmbientOrbConfig {
|
||||
color1?: string;
|
||||
color2?: string;
|
||||
size?: number;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export const useAmbientOrbAnimation = (config?: AmbientOrbConfig) => {
|
||||
const orbBreathing1 = useSharedValue(0);
|
||||
const orbBreathing2 = useSharedValue(0);
|
||||
const orbDrift1X = useSharedValue(0);
|
||||
const orbDrift1Y = useSharedValue(0);
|
||||
const orbDrift2X = useSharedValue(0);
|
||||
const orbDrift2Y = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Breathing animations
|
||||
orbBreathing1.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(1, { duration: 3000 }),
|
||||
withTiming(0, { duration: 3000 })
|
||||
),
|
||||
-1,
|
||||
true
|
||||
);
|
||||
// ... rest of animation setup
|
||||
}, []);
|
||||
|
||||
const orbStyle1 = useAnimatedStyle(() => ({
|
||||
// ... transform and opacity logic
|
||||
}));
|
||||
|
||||
const orbStyle2 = useAnimatedStyle(() => ({
|
||||
// ... transform and opacity logic
|
||||
}));
|
||||
|
||||
return { orbStyle1, orbStyle2 };
|
||||
};
|
||||
```
|
||||
|
||||
### 1.2 Create `useMediaPicker` Hook
|
||||
|
||||
**Extract from:** `app/(app)/chat/[id].tsx` (lines 296-380)
|
||||
|
||||
**New file:** `src/hooks/useMediaPicker.ts`
|
||||
|
||||
```typescript
|
||||
interface MediaPickerOptions {
|
||||
allowsVideo?: boolean;
|
||||
maxDuration?: number;
|
||||
quality?: number;
|
||||
onSelect: (uri: string, type: 'image' | 'video') => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export const useMediaPicker = (options: MediaPickerOptions) => {
|
||||
const pickImage = async () => { /* ... */ };
|
||||
const takePhoto = async () => { /* ... */ };
|
||||
const pickVideo = async () => { /* ... */ };
|
||||
const recordVideo = async () => { /* ... */ };
|
||||
|
||||
return { pickImage, takePhoto, pickVideo, recordVideo };
|
||||
};
|
||||
```
|
||||
|
||||
### 1.3 Create `useTypingIndicator` Hook
|
||||
|
||||
**Extract from:** `app/(app)/chat/[id].tsx` (lines 200-260)
|
||||
|
||||
**New file:** `src/hooks/useTypingIndicator.ts`
|
||||
|
||||
```typescript
|
||||
export const useTypingIndicator = (matchId: string, userId: string) => {
|
||||
const [isPartnerTyping, setIsPartnerTyping] = useState(false);
|
||||
|
||||
const sendTypingStatus = useCallback((isTyping: boolean) => { /* ... */ }, []);
|
||||
|
||||
// Subscribe to typing channel
|
||||
useEffect(() => { /* ... */ }, [matchId]);
|
||||
|
||||
return { isPartnerTyping, sendTypingStatus };
|
||||
};
|
||||
```
|
||||
|
||||
### 1.4 Create `useMessageSubscription` Hook
|
||||
|
||||
**Extract from:** `app/(app)/chat/[id].tsx` (lines 450-550)
|
||||
|
||||
**New file:** `src/hooks/useMessageSubscription.ts`
|
||||
|
||||
```typescript
|
||||
export const useMessageSubscription = (matchId: string, userId: string) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
// Real-time subscription
|
||||
// Message status updates
|
||||
// Optimistic updates
|
||||
|
||||
return { messages, sendMessage, markAsRead };
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Create Shared UI Components (Low Risk, Medium Impact)
|
||||
|
||||
**Goal:** Extract repeated UI patterns into reusable components.
|
||||
|
||||
### 2.1 Create `DecorativeSeparator` Component
|
||||
|
||||
**Duplicate instances found in:**
|
||||
- `app/(app)/swipe.tsx` (6+ instances)
|
||||
- `app/(app)/chat/[id].tsx` (lines 648-652)
|
||||
- `src/components/SwipeTutorial.tsx`
|
||||
|
||||
**New file:** `src/components/ui/DecorativeSeparator.tsx`
|
||||
|
||||
```typescript
|
||||
interface DecorativeSeparatorProps {
|
||||
variant?: 'diamond' | 'dot' | 'line';
|
||||
color?: string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
export const DecorativeSeparator: React.FC<DecorativeSeparatorProps> = ({
|
||||
variant = 'diamond',
|
||||
color = theme.colors.glass.border,
|
||||
width = '60%',
|
||||
}) => (
|
||||
<View style={[styles.container, { width }]}>
|
||||
<View style={[styles.line, { backgroundColor: color }]} />
|
||||
{variant === 'diamond' && <View style={[styles.diamond, { backgroundColor: color }]} />}
|
||||
{variant === 'dot' && <View style={[styles.dot, { backgroundColor: color }]} />}
|
||||
<View style={[styles.line, { backgroundColor: color }]} />
|
||||
</View>
|
||||
);
|
||||
```
|
||||
|
||||
### 2.2 Create `LoadingOverlay` Component
|
||||
|
||||
**Similar patterns in:**
|
||||
- `app/(app)/chat/[id].tsx` (UploadingSkeleton)
|
||||
- `src/components/Paywall.tsx`
|
||||
- Various loading states
|
||||
|
||||
**New file:** `src/components/ui/LoadingOverlay.tsx`
|
||||
|
||||
```typescript
|
||||
interface LoadingOverlayProps {
|
||||
visible: boolean;
|
||||
message?: string;
|
||||
progress?: number;
|
||||
variant?: 'spinner' | 'shimmer' | 'progress';
|
||||
}
|
||||
|
||||
export const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ ... });
|
||||
```
|
||||
|
||||
### 2.3 Create `ReadReceipt` Component
|
||||
|
||||
**Extract from:** `app/(app)/chat/[id].tsx` (lines 1354-1365)
|
||||
|
||||
**New file:** `src/components/ui/ReadReceipt.tsx`
|
||||
|
||||
```typescript
|
||||
type ReceiptStatus = 'sent' | 'delivered' | 'read';
|
||||
|
||||
interface ReadReceiptProps {
|
||||
status: ReceiptStatus;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const ReadReceipt: React.FC<ReadReceiptProps> = ({ status, size = 14 }) => {
|
||||
const icon = status === 'read' ? 'checkmark-done' : status === 'delivered' ? 'checkmark-done' : 'checkmark';
|
||||
const color = status === 'read' ? theme.colors.primary : theme.colors.text.secondary;
|
||||
|
||||
return <Ionicons name={icon} size={size} color={color} />;
|
||||
};
|
||||
```
|
||||
|
||||
### 2.4 Create `AmbientOrbs` Component
|
||||
|
||||
**New file:** `src/components/ui/AmbientOrbs.tsx`
|
||||
|
||||
```typescript
|
||||
interface AmbientOrbsProps {
|
||||
variant?: 'default' | 'chat' | 'premium';
|
||||
}
|
||||
|
||||
export const AmbientOrbs: React.FC<AmbientOrbsProps> = ({ variant = 'default' }) => {
|
||||
const { orbStyle1, orbStyle2 } = useAmbientOrbAnimation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View style={[styles.orb1, orbStyle1]} />
|
||||
<Animated.View style={[styles.orb2, orbStyle2]} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Split the Zustand Store (Medium Risk, High Impact)
|
||||
|
||||
**Goal:** Improve maintainability by splitting the monolithic store into domain-specific modules.
|
||||
|
||||
### Current Structure (666 lines, 1 file)
|
||||
```
|
||||
src/store/index.ts
|
||||
├── useAuthStore
|
||||
├── useMatchStore
|
||||
├── usePacksStore
|
||||
├── useMessageStore
|
||||
└── useSubscriptionStore
|
||||
```
|
||||
|
||||
### Target Structure (5 files, ~100-150 lines each)
|
||||
```
|
||||
src/store/
|
||||
├── index.ts # Re-exports all stores
|
||||
├── authStore.ts # Auth, user, couple, partner
|
||||
├── matchStore.ts # Matches, sorting, filtering
|
||||
├── packsStore.ts # Question packs, enabled packs
|
||||
├── messageStore.ts # Messages, unread counts
|
||||
└── subscriptionStore.ts # RevenueCat, offerings, purchases
|
||||
```
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
1. **Create store directory structure**
|
||||
2. **Move `useAuthStore` to `authStore.ts`**
|
||||
- Keep auth refresh logic isolated
|
||||
- Export type definitions
|
||||
3. **Move `useMatchStore` to `matchStore.ts`**
|
||||
4. **Move `usePacksStore` to `packsStore.ts`**
|
||||
5. **Move `useMessageStore` to `messageStore.ts`**
|
||||
6. **Move `useSubscriptionStore` to `subscriptionStore.ts`**
|
||||
7. **Update `index.ts` to re-export all stores**
|
||||
|
||||
```typescript
|
||||
// src/store/index.ts
|
||||
export { useAuthStore } from './authStore';
|
||||
export { useMatchStore } from './matchStore';
|
||||
export { usePacksStore } from './packsStore';
|
||||
export { useMessageStore } from './messageStore';
|
||||
export { useSubscriptionStore } from './subscriptionStore';
|
||||
```
|
||||
|
||||
**Import changes:** None required - all imports from `@/store` will continue to work.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Decompose Chat Screen (Medium Risk, Highest Impact)
|
||||
|
||||
**Goal:** Break the 2,213-line chat screen into maintainable, testable components.
|
||||
|
||||
### Current Structure
|
||||
```
|
||||
app/(app)/chat/[id].tsx (2,213 lines)
|
||||
├── Animations (lines 70-122)
|
||||
├── State management (lines 130-200)
|
||||
├── Typing indicator logic (lines 200-260)
|
||||
├── Media handling (lines 296-450)
|
||||
├── Message subscription (lines 450-550)
|
||||
├── UI Components (inline)
|
||||
│ ├── MessageContent
|
||||
│ ├── ChatVideoPlayer
|
||||
│ ├── MessageMeta
|
||||
│ ├── InputBar
|
||||
│ └── UploadingSkeleton
|
||||
└── Main render (lines 600-2213)
|
||||
```
|
||||
|
||||
### Target Structure
|
||||
```
|
||||
src/features/chat/
|
||||
├── index.ts # Public exports
|
||||
├── ChatScreen.tsx # Main screen (orchestration only, <400 lines)
|
||||
├── components/
|
||||
│ ├── ChatHeader.tsx # Header with match info
|
||||
│ ├── ChatMessages.tsx # Message list + scroll logic
|
||||
│ ├── MessageBubble.tsx # Single message rendering
|
||||
│ ├── MessageContent.tsx # Text/image/video content
|
||||
│ ├── ChatVideoPlayer.tsx # Video player with controls
|
||||
│ ├── InputBar.tsx # Text input + media buttons
|
||||
│ ├── MediaMenu.tsx # Expandable media picker
|
||||
│ ├── TypingIndicator.tsx # Animated typing dots
|
||||
│ └── UploadProgress.tsx # Upload skeleton/progress
|
||||
├── hooks/
|
||||
│ ├── useChatMessages.ts # Message fetching & subscription
|
||||
│ ├── useTypingIndicator.ts # Typing broadcast channel
|
||||
│ ├── useMediaUpload.ts # Image/video compression & upload
|
||||
│ └── useVideoCache.ts # Video download & caching
|
||||
└── types.ts # Chat-specific types
|
||||
```
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### Step 1: Create Feature Directory
|
||||
```bash
|
||||
mkdir -p src/features/chat/components
|
||||
mkdir -p src/features/chat/hooks
|
||||
```
|
||||
|
||||
#### Step 2: Extract Components (in order of dependency)
|
||||
|
||||
**2a. Extract `MessageContent.tsx`**
|
||||
- Lines 680-800 from current file
|
||||
- Handles text, image, video rendering
|
||||
- Props: `{ message, onImagePress, onVideoPress }`
|
||||
|
||||
**2b. Extract `ChatVideoPlayer.tsx`**
|
||||
- Lines 740-800 from current file
|
||||
- Custom video player with controls
|
||||
- Props: `{ uri, onClose, visible }`
|
||||
|
||||
**2c. Extract `MessageBubble.tsx`**
|
||||
- Lines 640-900 from current file
|
||||
- Single message with content + meta
|
||||
- Props: `{ message, isOwn, onMediaPress }`
|
||||
|
||||
**2d. Extract `TypingIndicator.tsx`**
|
||||
- Lines 870-910 from current file
|
||||
- Animated typing dots
|
||||
- Props: `{ visible }`
|
||||
|
||||
**2e. Extract `InputBar.tsx`**
|
||||
- Lines 1380-1510 from current file
|
||||
- Text input + send button + media toggle
|
||||
- Props: `{ onSend, onMediaSelect, disabled }`
|
||||
|
||||
**2f. Extract `MediaMenu.tsx`**
|
||||
- Lines 1510-1600 from current file
|
||||
- Expandable media picker buttons
|
||||
- Props: `{ visible, onSelect, onClose }`
|
||||
|
||||
**2g. Extract `ChatHeader.tsx`**
|
||||
- Lines 600-640 from current file
|
||||
- Match card + question display
|
||||
- Props: `{ match, onBack }`
|
||||
|
||||
**2h. Extract `ChatMessages.tsx`**
|
||||
- Lines 1600-1800 from current file
|
||||
- FlatList with message rendering
|
||||
- Props: `{ messages, userId, onMediaPress }`
|
||||
|
||||
#### Step 3: Extract Hooks
|
||||
|
||||
**3a. Create `useChatMessages.ts`**
|
||||
```typescript
|
||||
export const useChatMessages = (matchId: string) => {
|
||||
// Fetch initial messages
|
||||
// Subscribe to new messages
|
||||
// Handle optimistic updates
|
||||
// Mark messages as read
|
||||
return { messages, sendMessage, loading, error };
|
||||
};
|
||||
```
|
||||
|
||||
**3b. Create `useMediaUpload.ts`**
|
||||
```typescript
|
||||
export const useMediaUpload = (matchId: string) => {
|
||||
// Compress image/video
|
||||
// Upload to storage
|
||||
// Track progress
|
||||
return { upload, progress, uploading };
|
||||
};
|
||||
```
|
||||
|
||||
#### Step 4: Create Main Screen
|
||||
```typescript
|
||||
// src/features/chat/ChatScreen.tsx (~300-400 lines)
|
||||
export const ChatScreen: React.FC = () => {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { messages, sendMessage } = useChatMessages(id);
|
||||
const { isPartnerTyping, sendTypingStatus } = useTypingIndicator(id);
|
||||
const { upload, progress } = useMediaUpload(id);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<AmbientOrbs variant="chat" />
|
||||
<ChatHeader match={match} onBack={router.back} />
|
||||
<ChatMessages messages={messages} userId={userId} />
|
||||
{isPartnerTyping && <TypingIndicator />}
|
||||
<InputBar onSend={sendMessage} onMediaSelect={upload} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Step 5: Update Route File
|
||||
```typescript
|
||||
// app/(app)/chat/[id].tsx
|
||||
export { ChatScreen as default } from '@/features/chat';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Decompose Profile Screen (Medium Risk, High Impact)
|
||||
|
||||
**Goal:** Break the 1,703-line profile screen into manageable sections.
|
||||
|
||||
### Target Structure
|
||||
```
|
||||
src/features/profile/
|
||||
├── index.ts
|
||||
├── ProfileScreen.tsx # Main orchestration (<400 lines)
|
||||
├── components/
|
||||
│ ├── ProfileHeader.tsx # Avatar, name, edit
|
||||
│ ├── CoupleStatus.tsx # Partner info, invite code
|
||||
│ ├── SettingsSection.tsx # Generic settings group
|
||||
│ ├── NotificationSettings.tsx
|
||||
│ ├── PrivacySettings.tsx
|
||||
│ ├── AppearanceSettings.tsx
|
||||
│ └── DangerZone.tsx # Delete account, leave couple
|
||||
└── hooks/
|
||||
├── useProfileSettings.ts
|
||||
└── useCoupleManagement.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Improve Component Organization (Low Risk)
|
||||
|
||||
**Goal:** Establish clear component categories and naming conventions.
|
||||
|
||||
### Current Structure
|
||||
```
|
||||
src/components/
|
||||
├── ui/ # Primitive UI components
|
||||
├── [Feature].tsx # Mixed feature components
|
||||
└── [Tutorial].tsx # Tutorial components
|
||||
```
|
||||
|
||||
### Target Structure
|
||||
```
|
||||
src/components/
|
||||
├── ui/ # Primitive UI (buttons, cards, inputs)
|
||||
│ ├── GlassButton.tsx
|
||||
│ ├── GlassCard.tsx
|
||||
│ ├── GlassInput.tsx
|
||||
│ ├── DecorativeSeparator.tsx
|
||||
│ ├── LoadingOverlay.tsx
|
||||
│ ├── ReadReceipt.tsx
|
||||
│ ├── AmbientOrbs.tsx
|
||||
│ └── ShimmerEffect.tsx
|
||||
├── feedback/ # User feedback components
|
||||
│ ├── FeedbackModal.tsx
|
||||
│ └── QuestionFeedbackModal.tsx
|
||||
├── tutorials/ # Onboarding tutorials
|
||||
│ ├── SwipeTutorial.tsx
|
||||
│ └── MatchesTutorial.tsx
|
||||
├── paywall/ # Subscription components
|
||||
│ ├── Paywall.tsx
|
||||
│ └── PremiumBadge.tsx
|
||||
└── swipe/ # Swipe card variants
|
||||
├── SwipeCard.native.tsx
|
||||
├── SwipeCard.web.tsx
|
||||
└── SwipeCardPremium.tsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Add Testing Infrastructure (Long-term)
|
||||
|
||||
**Goal:** Enable confident refactoring with test coverage.
|
||||
|
||||
### Recommended Test Structure
|
||||
```
|
||||
src/
|
||||
├── __tests__/
|
||||
│ ├── components/
|
||||
│ ├── hooks/
|
||||
│ ├── stores/
|
||||
│ └── utils/
|
||||
└── features/
|
||||
└── chat/
|
||||
└── __tests__/
|
||||
├── ChatMessages.test.tsx
|
||||
└── useChatMessages.test.ts
|
||||
```
|
||||
|
||||
### Testing Priorities
|
||||
1. **Hooks** - Pure logic, easy to test
|
||||
2. **Store actions** - Critical business logic
|
||||
3. **Utility functions** - Edge cases
|
||||
4. **Components** - Snapshot + interaction tests
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
| Phase | Risk | Impact | Effort | Priority |
|
||||
|-------|------|--------|--------|----------|
|
||||
| 1. Shared Hooks | Low | High | 2-3 days | 1 |
|
||||
| 2. Shared UI Components | Low | Medium | 1-2 days | 2 |
|
||||
| 3. Split Store | Medium | High | 1 day | 3 |
|
||||
| 4. Decompose Chat | Medium | Highest | 3-5 days | 4 |
|
||||
| 5. Decompose Profile | Medium | High | 2-3 days | 5 |
|
||||
| 6. Component Organization | Low | Low | 1 day | 6 |
|
||||
| 7. Testing | Low | High | Ongoing | 7 |
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### For Each Phase:
|
||||
1. **Create new files** without modifying existing code
|
||||
2. **Import and use** new components/hooks in the target file
|
||||
3. **Verify functionality** works identically
|
||||
4. **Remove old code** from the original file
|
||||
5. **Update imports** across the codebase if needed
|
||||
|
||||
### Rollback Strategy:
|
||||
- Each phase can be rolled back independently
|
||||
- Keep original files until phase is complete
|
||||
- Use feature flags for large changes if needed
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
| Metric | Current | Target |
|
||||
|--------|---------|--------|
|
||||
| Max lines per file | 2,213 | <500 |
|
||||
| Duplicate animation code | 3 files | 1 hook |
|
||||
| Store files | 1 (666 lines) | 5 (~130 lines each) |
|
||||
| Test coverage | 0% | >50% for hooks/stores |
|
||||
| Component reuse | Low | High (shared UI components) |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Do not change** the tab bar background styling (see CLAUDE.md)
|
||||
- **Follow** DESIGN.md for all UI changes
|
||||
- **Use** theme imports, never hardcode colors
|
||||
- **Preserve** all existing functionality during refactoring
|
||||
@@ -1,237 +0,0 @@
|
||||
# Mobile App Refactoring Tasks
|
||||
|
||||
Track progress by checking off completed items.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Extract Shared Hooks
|
||||
|
||||
### 1.1 Ambient Orb Animation Hook
|
||||
- [x] Create `src/hooks/useAmbientOrbAnimation.ts`
|
||||
- [x] Extract animation logic from `app/(app)/swipe.tsx`
|
||||
- [x] Update `app/(app)/swipe.tsx` to use hook
|
||||
- [x] Update `app/(app)/chat/[id].tsx` to use hook
|
||||
- [x] Update `app/(app)/matches.tsx` to use hook
|
||||
- [x] Remove duplicate animation code from all files
|
||||
|
||||
### 1.2 Media Picker Hook
|
||||
- [x] Create `src/hooks/useMediaPicker.ts`
|
||||
- [x] Extract image picker logic from chat screen
|
||||
- [x] Extract video picker logic from chat screen
|
||||
- [x] Extract camera logic from chat screen
|
||||
- [x] Update chat screen to use hook
|
||||
|
||||
### 1.3 Typing Indicator Hook
|
||||
- [x] Create `src/hooks/useTypingIndicator.ts`
|
||||
- [x] Extract broadcast channel logic from chat
|
||||
- [x] Extract typing state management
|
||||
- [x] Update chat screen to use hook
|
||||
|
||||
### 1.4 Message Subscription Hook
|
||||
- [x] Create `src/hooks/useMessageSubscription.ts`
|
||||
- [x] Extract real-time subscription logic
|
||||
- [x] Extract message status update logic
|
||||
- [x] Extract optimistic update logic
|
||||
- [x] Update chat screen to use hook
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Shared UI Components
|
||||
|
||||
### 2.1 Decorative Separator
|
||||
- [x] Create `src/components/ui/DecorativeSeparator.tsx`
|
||||
- [x] Support rose, gold, and muted variants
|
||||
- [x] Update `app/(app)/swipe.tsx` (4 instances)
|
||||
- [x] Update `app/(app)/chat/[id].tsx` (2 instances)
|
||||
- [x] Update `app/(app)/matches.tsx` (2 instances)
|
||||
- [x] Remove duplicate separator styles
|
||||
|
||||
### 2.2 Loading Overlay
|
||||
- [x] Create `src/components/ui/LoadingOverlay.tsx`
|
||||
- [x] Support spinner, shimmer, and progress variants
|
||||
- [x] Extract UploadingSkeleton from chat screen
|
||||
- [x] Update usage across app
|
||||
|
||||
### 2.3 Read Receipt
|
||||
- [x] Create `src/components/ui/ReadReceipt.tsx`
|
||||
- [x] Support sent, delivered, read states
|
||||
- [x] Update chat screen MessageMeta
|
||||
- [x] Remove inline receipt logic
|
||||
|
||||
### 2.4 Ambient Orbs Component
|
||||
- [x] Create `src/components/ui/AmbientOrbs.tsx`
|
||||
- [x] Use `useAmbientOrbAnimation` hook
|
||||
- [x] Support default, chat, premium variants
|
||||
- [x] Update swipe screen to use component
|
||||
- [x] Update chat screen to use component
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Split Zustand Store
|
||||
|
||||
### 3.1 Create Store Structure
|
||||
- [x] Create `src/store/` directory structure
|
||||
- [x] Create `src/store/authStore.ts`
|
||||
- [x] Create `src/store/matchStore.ts`
|
||||
- [x] Create `src/store/packsStore.ts`
|
||||
- [x] Create `src/store/messageStore.ts`
|
||||
- [x] Create `src/store/subscriptionStore.ts`
|
||||
|
||||
### 3.2 Migrate Stores
|
||||
- [x] Move `useAuthStore` to `authStore.ts`
|
||||
- [x] Move `useMatchStore` to `matchStore.ts`
|
||||
- [x] Move `usePacksStore` to `packsStore.ts`
|
||||
- [x] Move `useMessageStore` to `messageStore.ts`
|
||||
- [x] Move `useSubscriptionStore` to `subscriptionStore.ts`
|
||||
|
||||
### 3.3 Update Exports
|
||||
- [x] Update `src/store/index.ts` to re-export all stores
|
||||
- [x] Verify all imports still work (type check passes)
|
||||
- [x] Replace monolithic code with re-exports
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Decompose Chat Screen
|
||||
|
||||
### 4.1 Create Feature Structure
|
||||
- [x] Create `src/features/chat/` directory
|
||||
- [x] Create `src/features/chat/components/` directory
|
||||
- [x] Create `src/features/chat/hooks/` directory
|
||||
- [x] Create `src/features/chat/types.ts`
|
||||
|
||||
### 4.2 Extract Components
|
||||
- [x] Extract `ChatHeader.tsx`
|
||||
- [x] Extract `MessageContent.tsx`
|
||||
- [x] Extract `ChatVideoPlayer.tsx`
|
||||
- [x] Extract `MessageBubble.tsx`
|
||||
- [x] Extract `TypingIndicator.tsx`
|
||||
- [x] Extract `InputBar.tsx`
|
||||
- [x] Extract `MediaMenu.tsx`
|
||||
- [x] Extract `ChatMessages.tsx`
|
||||
- [x] Extract `UploadProgress.tsx`
|
||||
- [x] Extract `MessageMeta.tsx`
|
||||
|
||||
### 4.3 Extract Hooks
|
||||
- [x] Create `useChatMessages.ts`
|
||||
- [x] Create `useMediaUpload.ts`
|
||||
- [x] Create `useVideoCache.ts`
|
||||
|
||||
### 4.4 Create Main Screen
|
||||
- [x] Create `src/features/chat/ChatScreen.tsx`
|
||||
- [x] Wire up all components and hooks
|
||||
- [x] Create `src/features/chat/index.ts` exports
|
||||
- [x] Update `app/(app)/chat/[id].tsx` to use feature
|
||||
|
||||
### 4.5 Cleanup
|
||||
- [x] Remove old code from route file
|
||||
- [x] Verify all functionality works
|
||||
- [x] Test real-time messaging
|
||||
- [x] Test media upload/download
|
||||
- [x] Test video playback
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Decompose Profile Screen
|
||||
|
||||
### 5.1 Create Feature Structure
|
||||
- [x] Create `src/features/profile/` directory
|
||||
- [x] Create `src/features/profile/components/` directory
|
||||
- [x] Create `src/features/profile/hooks/` directory
|
||||
|
||||
### 5.2 Extract Components
|
||||
- [x] Extract `ProfileHeader.tsx`
|
||||
- [x] Extract `CoupleStatus.tsx`
|
||||
- [x] Extract `SettingsSection.tsx`
|
||||
- [x] Extract `NotificationSettings.tsx`
|
||||
- [x] Extract `PrivacySettings.tsx`
|
||||
- [x] Extract `AppearanceSettings.tsx`
|
||||
|
||||
- [x] Extract `DangerZone.tsx`
|
||||
- [x] Extract `SwitchItem.tsx`
|
||||
- [x] Extract `MenuItem.tsx`
|
||||
|
||||
### 5.3 Extract Hooks
|
||||
- [x] Create `useProfileSettings.ts`
|
||||
- [x] Create `useCoupleManagement.ts`
|
||||
|
||||
### 5.4 Create Main Screen
|
||||
- [x] Create `src/features/profile/ProfileScreen.tsx`
|
||||
- [x] Create `src/features/profile/index.ts` exports
|
||||
- [x] Update `app/(app)/profile.tsx` to use feature
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Component Organization
|
||||
|
||||
### 6.1 Reorganize Components
|
||||
- [x] Create `src/components/feedback/` directory
|
||||
- [x] Move `FeedbackModal.tsx` to feedback/
|
||||
- [x] Move `QuestionFeedbackModal.tsx` to feedback/
|
||||
- [x] Create `src/components/tutorials/` directory
|
||||
- [x] Move `SwipeTutorial.tsx` to tutorials/
|
||||
- [x] Move `MatchesTutorial.tsx` to tutorials/
|
||||
- [x] Create `src/components/paywall/` directory
|
||||
- [x] Move `Paywall.tsx` to paywall/
|
||||
- [x] Create `src/components/swipe/` directory
|
||||
- [x] Move `SwipeCard.native.tsx` to swipe/
|
||||
- [x] Move `SwipeCard.web.tsx` to swipe/
|
||||
- [x] Move `SwipeCardPremium.tsx` to swipe/
|
||||
|
||||
### 6.2 Update Imports
|
||||
- [x] Update all imports for moved components
|
||||
- [x] Create index.ts files for each directory
|
||||
- [x] Verify no broken imports
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Testing Infrastructure
|
||||
|
||||
### 7.1 Setup
|
||||
- [x] Install testing dependencies (jest, testing-library)
|
||||
- [x] Configure jest for React Native
|
||||
- [x] Create test utilities and mocks
|
||||
|
||||
### 7.2 Hook Tests
|
||||
- [x] Test `useAmbientOrbAnimation`
|
||||
- [x] Test `useMediaPicker`
|
||||
- [x] Test `useTypingIndicator`
|
||||
- [x] Test `useMessageSubscription`
|
||||
- [x] Test `useChatMessages`
|
||||
- [x] Test `useMediaUpload`
|
||||
|
||||
### 7.3 Store Tests
|
||||
- [x] Test `authStore` actions
|
||||
- [x] Test `matchStore` actions
|
||||
- [x] Test `packsStore` actions
|
||||
- [x] Test `messageStore` actions
|
||||
- [x] Test `subscriptionStore` actions
|
||||
|
||||
### 7.4 Component Tests
|
||||
- [x] Test `DecorativeSeparator` variants
|
||||
- [x] Test `ReadReceipt` states
|
||||
- [x] Test `InputBar` interactions
|
||||
- [x] Test `MessageBubble` rendering
|
||||
|
||||
---
|
||||
|
||||
## Progress Summary
|
||||
|
||||
| Phase | Status | Completion |
|
||||
|-------|--------|------------|
|
||||
| 1. Shared Hooks | Complete | 100% |
|
||||
| 2. Shared UI Components | Complete | 100% |
|
||||
| 3. Split Store | Complete | 100% |
|
||||
| 4. Decompose Chat | Complete | 100% |
|
||||
| 5. Decompose Profile | Complete | 100% |
|
||||
| 6. Component Organization | Complete | 100% |
|
||||
| 7. Testing | Complete | 100% |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Each phase can be completed independently
|
||||
- Test after each component extraction
|
||||
- Keep original files until phase is verified
|
||||
- Follow DESIGN.md for any UI changes
|
||||
- Do not modify tab bar background styling
|
||||
@@ -1,303 +0,0 @@
|
||||
# Releasing Sauci Mobile App
|
||||
|
||||
This guide covers local release builds for Android and iOS.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Java JDK 17** - Required for Android Gradle builds
|
||||
- **Android Studio** - For SDK and build tools (including Android SDK/NDK)
|
||||
- **Xcode** - For iOS builds (macOS only)
|
||||
- **EAS CLI** - `npm install -g eas-cli`
|
||||
- **Expo account** - Run `eas login` to authenticate
|
||||
|
||||
---
|
||||
|
||||
## EAS Build Local (Recommended)
|
||||
|
||||
The `--local` flag runs the EAS build process on your machine instead of Expo's cloud servers. This automatically handles signing credentials and produces the same output as cloud builds.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
cd apps/mobile
|
||||
|
||||
# Android - produces .aab file (for Play Store)
|
||||
eas build --platform android --profile production --local
|
||||
|
||||
# iOS - produces .ipa file (for App Store)
|
||||
eas build --platform ios --profile production --local
|
||||
```
|
||||
|
||||
Output files are placed in the current directory by default.
|
||||
|
||||
### Building an APK (for Testing)
|
||||
|
||||
The production profile creates an AAB (Android App Bundle), which can't be directly installed on devices. To build an APK for testing:
|
||||
|
||||
```bash
|
||||
# Use the preview profile (configured for APK output)
|
||||
eas build --platform android --profile preview --local
|
||||
```
|
||||
|
||||
Or add a custom profile to `eas.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"local-apk": {
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
```bash
|
||||
eas build --platform android --profile local-apk --local
|
||||
```
|
||||
|
||||
### Installing the APK
|
||||
|
||||
```bash
|
||||
# Via ADB (device connected via USB)
|
||||
adb install ./build-*.apk
|
||||
|
||||
# Or drag-and-drop onto Android Emulator
|
||||
```
|
||||
|
||||
### Build Profiles
|
||||
|
||||
| Profile | Output | Use Case |
|
||||
|---------|--------|----------|
|
||||
| `production` | .aab | Play Store / App Store submission |
|
||||
| `preview` | .apk | Internal testing, device installation |
|
||||
| `development` | .apk | Dev client with hot reload |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Useful for debugging build issues:
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `EAS_LOCAL_BUILD_SKIP_CLEANUP=1` | Keep build directory after completion |
|
||||
| `EAS_LOCAL_BUILD_WORKINGDIR=/path` | Custom build directory (default: /tmp) |
|
||||
| `EAS_LOCAL_BUILD_ARTIFACTS_DIR=/path` | Output directory for build artifacts |
|
||||
|
||||
Example with debugging enabled:
|
||||
```bash
|
||||
EAS_LOCAL_BUILD_SKIP_CLEANUP=1 \
|
||||
EAS_LOCAL_BUILD_WORKINGDIR=~/eas-builds \
|
||||
eas build --platform android --profile production --local
|
||||
```
|
||||
|
||||
### Limitations of Local Builds
|
||||
|
||||
- **Single platform only** - Can't use `--platform all`
|
||||
- **Software versions ignored** - `node`, `yarn`, `ndk`, `image` fields in eas.json are ignored
|
||||
- **No caching** - Each build starts fresh
|
||||
- **Secret env vars not available** - Set them in your local environment instead
|
||||
- **macOS/Linux only** - Windows requires WSL (unsupported)
|
||||
|
||||
### Troubleshooting Local Builds
|
||||
|
||||
**Java version mismatch:**
|
||||
```bash
|
||||
# Check Java version (should be 17)
|
||||
java -version
|
||||
|
||||
# On macOS, switch Java version
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
|
||||
```
|
||||
|
||||
**Gradle daemon issues:**
|
||||
```bash
|
||||
cd android && ./gradlew --stop && cd ..
|
||||
```
|
||||
|
||||
**Clean build after dependency changes:**
|
||||
```bash
|
||||
# Remove native folders and rebuild
|
||||
rm -rf android ios
|
||||
npx expo prebuild --clean
|
||||
eas build --platform android --profile production --local
|
||||
```
|
||||
|
||||
**Out of memory:**
|
||||
```bash
|
||||
# Increase Gradle memory
|
||||
echo "org.gradle.jvmargs=-Xmx4g" >> android/gradle.properties
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manual Android Release Build
|
||||
|
||||
### Step 1: Get the Production Keystore
|
||||
|
||||
The production keystore is stored in EAS. Download it:
|
||||
|
||||
```bash
|
||||
cd apps/mobile
|
||||
eas credentials --platform android
|
||||
```
|
||||
|
||||
Select:
|
||||
1. `production`
|
||||
2. `Keystore`
|
||||
3. `Download Keystore`
|
||||
|
||||
Save the keystore file and note the **password** and **alias** shown.
|
||||
|
||||
### Step 2: Configure Signing
|
||||
|
||||
Ensure `android/gradle.properties` has the keystore configuration:
|
||||
|
||||
```properties
|
||||
MYAPP_UPLOAD_STORE_FILE=sauci-release.keystore
|
||||
MYAPP_UPLOAD_KEY_ALIAS=<alias from EAS>
|
||||
MYAPP_UPLOAD_STORE_PASSWORD=<password from EAS>
|
||||
MYAPP_UPLOAD_KEY_PASSWORD=<password from EAS>
|
||||
```
|
||||
|
||||
**Important:** Never commit these credentials to git.
|
||||
|
||||
### Step 3: Prebuild (if needed)
|
||||
|
||||
If the `android/` folder doesn't exist or native dependencies changed:
|
||||
|
||||
```bash
|
||||
npx expo prebuild --platform android --clean
|
||||
```
|
||||
|
||||
### Step 4: Build the Release Bundle
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew bundleRelease
|
||||
```
|
||||
|
||||
The AAB file will be at:
|
||||
```
|
||||
android/app/build/outputs/bundle/release/app-release.aab
|
||||
```
|
||||
|
||||
### Step 5: Upload to Play Store
|
||||
|
||||
1. Go to [Google Play Console](https://play.google.com/console)
|
||||
2. Select **Sauci** app
|
||||
3. Go to **Production** (or testing track)
|
||||
4. **Create new release**
|
||||
5. Upload the `.aab` file
|
||||
6. Add release notes and submit
|
||||
|
||||
### Verifying Keystore Fingerprint
|
||||
|
||||
To verify you're using the correct keystore:
|
||||
|
||||
```bash
|
||||
keytool -list -v -keystore android/sauci-release.keystore -storepass <password>
|
||||
```
|
||||
|
||||
Expected SHA1: `C3:70:C5:8C:85:E5:B5:92:97:82:A9:B0:47:98:40:99:AC:C8:47:43`
|
||||
|
||||
---
|
||||
|
||||
## iOS Release Build
|
||||
|
||||
### Step 1: Prebuild (if needed)
|
||||
|
||||
```bash
|
||||
npx expo prebuild --platform ios --clean
|
||||
```
|
||||
|
||||
### Step 2: Install Pods
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
pod install
|
||||
cd ..
|
||||
```
|
||||
|
||||
### Step 3: Build Archive in Xcode
|
||||
|
||||
1. Open `ios/Sauci.xcworkspace` in Xcode
|
||||
2. Select **Any iOS Device (arm64)** as the build target
|
||||
3. Go to **Product** → **Archive**
|
||||
4. Wait for the build to complete
|
||||
|
||||
### Step 4: Upload to App Store
|
||||
|
||||
1. In Xcode Organizer (Window → Organizer), select the archive
|
||||
2. Click **Distribute App**
|
||||
3. Select **App Store Connect** → **Upload**
|
||||
4. Follow the prompts to upload
|
||||
|
||||
### Step 5: Submit in App Store Connect
|
||||
|
||||
1. Go to [App Store Connect](https://appstoreconnect.apple.com)
|
||||
2. Select **Sauci**
|
||||
3. Create a new version or select the uploaded build
|
||||
4. Fill in release notes and submit for review
|
||||
|
||||
---
|
||||
|
||||
## Version Management
|
||||
|
||||
Version is managed by EAS with `appVersionSource: "remote"` in `eas.json`.
|
||||
|
||||
To sync or bump versions:
|
||||
|
||||
```bash
|
||||
# Check current version
|
||||
eas build:version:get
|
||||
|
||||
# Set version manually
|
||||
eas build:version:set --platform android --version 1.0.1
|
||||
eas build:version:set --platform ios --version 1.0.1
|
||||
```
|
||||
|
||||
For local builds, you may need to update `app.json` version manually:
|
||||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"version": "1.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And for Android, update `android/app/build.gradle`:
|
||||
|
||||
```gradle
|
||||
versionCode 2
|
||||
versionName "1.0.1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Wrong Keystore Error
|
||||
|
||||
If Play Store rejects with "signed with wrong key":
|
||||
|
||||
1. Run `eas credentials --platform android`
|
||||
2. Download the correct keystore
|
||||
3. Verify fingerprint matches expected SHA1
|
||||
4. Replace your local keystore file
|
||||
|
||||
### Build Failures After Dependency Changes
|
||||
|
||||
```bash
|
||||
# Clean and rebuild
|
||||
cd android && ./gradlew clean && cd ..
|
||||
npx expo prebuild --platform android --clean
|
||||
cd android && ./gradlew bundleRelease
|
||||
```
|
||||
|
||||
### iOS Signing Issues
|
||||
|
||||
Ensure your Apple Developer certificates and provisioning profiles are up to date in Xcode:
|
||||
- **Xcode** → **Settings** → **Accounts** → Download Manual Profiles
|
||||
@@ -25,6 +25,7 @@
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
<service android:name="com.reactnativeandroidwidget.RNWidgetCollectionService" android:permission="android.permission.BIND_REMOTEVIEWS"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
@@ -39,5 +40,12 @@
|
||||
<data android:scheme="exp+sauci"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<receiver android:name=".widget.LiveDraw" android:exported="false" android:label="Live Draw">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.sauci.app.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_livedraw"/>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.sauci.app.widget;
|
||||
|
||||
import com.reactnativeandroidwidget.RNWidgetProvider;
|
||||
|
||||
public class LiveDraw extends RNWidgetProvider {
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 688 KiB |
@@ -2,4 +2,5 @@
|
||||
<string name="app_name">Sauci</string>
|
||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="widget_livedraw_description" translatable="false">See your shared drawing with your partner.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="110dp"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="2"
|
||||
|
||||
|
||||
|
||||
android:resizeMode="none"
|
||||
|
||||
android:description="@string/widget_livedraw_description"
|
||||
|
||||
android:initialLayout="@layout/rn_widget"
|
||||
android:previewImage="@drawable/livedraw_preview"
|
||||
|
||||
|
||||
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen">
|
||||
</appwidget-provider>
|
||||
@@ -1,4 +0,0 @@
|
||||
defaults.url=https://sentry.io/
|
||||
defaults.org=molten-prints
|
||||
defaults.project=sauci
|
||||
# Using SENTRY_AUTH_TOKEN environment variable
|
||||
+28
-1
@@ -16,6 +16,9 @@
|
||||
"ios": {
|
||||
"supportsTablet": false,
|
||||
"bundleIdentifier": "com.sauci.app",
|
||||
"entitlements": {
|
||||
"com.apple.security.application-groups": ["group.com.sauci.app"]
|
||||
},
|
||||
"buildNumber": "35",
|
||||
"usesAppleSignIn": true,
|
||||
"googleServicesFile": "./GoogleService-Info.plist",
|
||||
@@ -237,7 +240,31 @@
|
||||
"iosUrlScheme": "com.googleusercontent.apps.861972684340-0u2as8i5equfhmhmvjihottkf0ig7237"
|
||||
}
|
||||
],
|
||||
"./plugins/withKotlinVersion.js"
|
||||
"./plugins/withKotlinVersion.js",
|
||||
[
|
||||
"@bacons/apple-targets",
|
||||
{
|
||||
"appleTeamId": "BXVPBATDZY"
|
||||
}
|
||||
],
|
||||
[
|
||||
"react-native-android-widget",
|
||||
{
|
||||
"widgets": [
|
||||
{
|
||||
"name": "LiveDraw",
|
||||
"label": "Live Draw",
|
||||
"minWidth": "250dp",
|
||||
"minHeight": "110dp",
|
||||
"targetCellWidth": 4,
|
||||
"targetCellHeight": 2,
|
||||
"description": "See your shared drawing with your partner.",
|
||||
"previewImage": "./assets/widget-preview/live-draw.png",
|
||||
"updatePeriodMillis": 1800000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
|
||||
@@ -76,8 +76,9 @@ export default function AppLayout() {
|
||||
const isOnOnboarding = segmentStrings.includes("onboarding");
|
||||
const isOnPairing = segmentStrings.includes("pairing");
|
||||
const isOnChat = segmentStrings.includes("chat");
|
||||
const isOnLiveDraw = segmentStrings.includes("live-draw");
|
||||
const isOnSettingsSubscreen = segmentStrings.includes("settings") && segmentStrings.length > 2;
|
||||
const shouldHideTabBar = isOnOnboarding || isOnPairing || isOnChat || isOnSettingsSubscreen;
|
||||
const shouldHideTabBar = isOnOnboarding || isOnPairing || isOnChat || isOnLiveDraw || isOnSettingsSubscreen;
|
||||
|
||||
// Redirect to login when not authenticated, or to onboarding if not completed/outdated
|
||||
useEffect(() => {
|
||||
@@ -539,6 +540,12 @@ export default function AppLayout() {
|
||||
href: null, // Hide from tab bar
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="live-draw"
|
||||
options={{
|
||||
href: null, // Hide from tab bar
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
{/* Message Toast Notification */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlurView } from 'expo-blur';
|
||||
import { useAuthStore, usePacksStore, useSubscriptionStore } from '../../src/store';
|
||||
import { GradientBackground } from '../../src/components/ui';
|
||||
import { Paywall } from '../../src/components/paywall';
|
||||
import { CompactHeader, ContentRow } from '../../src/components/discovery';
|
||||
import { CompactHeader, ContentRow, LiveDrawTile } from '../../src/components/discovery';
|
||||
import { colors, spacing, typography, radius } from '../../src/theme';
|
||||
import type { QuestionPack, Category } from '../../src/types';
|
||||
|
||||
@@ -114,6 +114,9 @@ export default function DiscoveryScreen() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.innerContainer, isWideScreen && styles.innerContainerWide]}>
|
||||
{/* Live Draw Activity Tile */}
|
||||
<LiveDrawTile delay={100} />
|
||||
|
||||
{/* Content Rows by Category */}
|
||||
{packsByCategory.map((group, index) => (
|
||||
<ContentRow
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { LiveDrawScreen } from '../../src/features/live-draw';
|
||||
|
||||
export default LiveDrawScreen;
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 688 KiB |
@@ -0,0 +1,6 @@
|
||||
import { registerWidgetTaskHandler } from 'react-native-android-widget';
|
||||
import { widgetTaskHandler } from './src/features/live-draw/widgets/widgetTaskHandler';
|
||||
|
||||
registerWidgetTaskHandler(widgetTaskHandler);
|
||||
|
||||
import 'expo-router/entry';
|
||||
@@ -64,3 +64,17 @@ target 'Sauci' do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# apple-targets-extension-loader -- Dynamic loading of target configurations
|
||||
Dir.glob(File.join(__dir__, '..', 'targets', '**', 'pods.rb')).each do |target_file|
|
||||
target_name = File.basename(File.dirname(target_file))
|
||||
target target_name do
|
||||
# Create a new binding with access to necessary methods and variables
|
||||
target_binding = binding
|
||||
target_binding.local_variable_set(:podfile_properties, podfile_properties)
|
||||
|
||||
# Evaluate the target file content in the new binding
|
||||
eval(File.read(target_file), target_binding, target_file)
|
||||
end
|
||||
end
|
||||
+125
-47
@@ -26,7 +26,7 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- EXNotifications (0.29.14):
|
||||
- ExpoModulesCore
|
||||
- Expo (52.0.48):
|
||||
- Expo (52.0.49):
|
||||
- ExpoModulesCore
|
||||
- expo-dev-client (5.0.20):
|
||||
- EXManifests
|
||||
@@ -264,7 +264,7 @@ PODS:
|
||||
- SDWebImageSVGCoder (~> 1.7.0)
|
||||
- ExpoImagePicker (16.0.6):
|
||||
- ExpoModulesCore
|
||||
- ExpoKeepAwake (14.0.3):
|
||||
- ExpoKeepAwake (15.0.8):
|
||||
- ExpoModulesCore
|
||||
- ExpoLinearGradient (14.0.2):
|
||||
- ExpoModulesCore
|
||||
@@ -300,7 +300,9 @@ PODS:
|
||||
- Yoga
|
||||
- ExpoSecureStore (14.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoSharing (14.0.8):
|
||||
- ExpoSharing (13.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoWidgets (3.0.2):
|
||||
- ExpoModulesCore
|
||||
- EXUpdatesInterface (1.0.0):
|
||||
- ExpoModulesCore
|
||||
@@ -308,6 +310,9 @@ PODS:
|
||||
- FBLazyVector (0.76.9)
|
||||
- Firebase/CoreOnly (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- Firebase/Crashlytics (12.8.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseCrashlytics (~> 12.8.0)
|
||||
- FirebaseAnalytics/Core (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- FirebaseInstallations (~> 12.8.0)
|
||||
@@ -330,13 +335,34 @@ PODS:
|
||||
- FirebaseCoreInternal (~> 12.8.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreExtension (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- FirebaseCoreInternal (12.8.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseCrashlytics (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- FirebaseInstallations (~> 12.8.0)
|
||||
- FirebaseRemoteConfigInterop (~> 12.8.0)
|
||||
- FirebaseSessions (~> 12.8.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseInstallations (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseRemoteConfigInterop (12.8.0)
|
||||
- FirebaseSessions (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- FirebaseCoreExtension (~> 12.8.0)
|
||||
- FirebaseInstallations (~> 12.8.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesSwift (~> 2.1)
|
||||
- fmt (11.0.2)
|
||||
- glog (0.3.5)
|
||||
- GoogleAppMeasurement/Core (12.8.0):
|
||||
@@ -352,6 +378,9 @@ PODS:
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleSignIn (9.1.0):
|
||||
- AppAuth (~> 2.0)
|
||||
- AppCheckCore (~> 11.0)
|
||||
@@ -402,6 +431,8 @@ PODS:
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- PromisesObjC (2.4.0)
|
||||
- PromisesSwift (2.4.0):
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- PurchasesHybridCommon (17.28.0):
|
||||
- RevenueCat (= 5.55.1)
|
||||
- RCT-Folly (2024.10.14.00):
|
||||
@@ -1705,6 +1736,29 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-safe-area-context (4.12.0):
|
||||
- React-Core
|
||||
- react-native-skia (1.12.4):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.10.14.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React
|
||||
- React-callinvoker
|
||||
- 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-view-shot (4.0.3):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
@@ -2025,7 +2079,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNFBApp
|
||||
- Yoga
|
||||
- RNFBApp (23.8.3):
|
||||
- RNFBApp (23.8.4):
|
||||
- DoubleConversion
|
||||
- Firebase/CoreOnly (= 12.8.0)
|
||||
- glog
|
||||
@@ -2047,6 +2101,30 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNFBCrashlytics (23.8.4):
|
||||
- DoubleConversion
|
||||
- Firebase/Crashlytics (= 12.8.0)
|
||||
- FirebaseCoreExtension
|
||||
- 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
|
||||
- RNGestureHandler (2.20.2):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
@@ -2183,29 +2261,6 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNSentry (6.10.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.10.14.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-hermes
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Sentry/HybridSDK (= 8.48.0)
|
||||
- Yoga
|
||||
- RNSVG (15.8.0):
|
||||
- React-Core
|
||||
- SDWebImage (5.19.7):
|
||||
@@ -2216,8 +2271,9 @@ PODS:
|
||||
- SDWebImage (~> 5.10)
|
||||
- SDWebImageSVGCoder (1.7.0):
|
||||
- SDWebImage/Core (~> 5.6)
|
||||
- Sentry/HybridSDK (8.48.0)
|
||||
- SocketRocket (0.7.1)
|
||||
- WidgetBridge (0.0.1):
|
||||
- ExpoModulesCore
|
||||
- Yoga (0.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -2230,7 +2286,7 @@ DEPENDENCIES:
|
||||
- EXJSONUtils (from `../../../node_modules/expo-json-utils/ios`)
|
||||
- EXManifests (from `../../../node_modules/expo-manifests/ios`)
|
||||
- EXNotifications (from `../../../node_modules/expo-notifications/ios`)
|
||||
- Expo (from `../../../node_modules/expo`)
|
||||
- Expo (from `../node_modules/expo`)
|
||||
- expo-dev-client (from `../../../node_modules/expo-dev-client/ios`)
|
||||
- expo-dev-launcher (from `../../../node_modules/expo-dev-launcher`)
|
||||
- expo-dev-menu (from `../../../node_modules/expo-dev-menu`)
|
||||
@@ -2253,7 +2309,8 @@ DEPENDENCIES:
|
||||
- ExpoMediaLibrary (from `../../../node_modules/expo-media-library/ios`)
|
||||
- ExpoModulesCore (from `../../../node_modules/expo-modules-core`)
|
||||
- ExpoSecureStore (from `../../../node_modules/expo-secure-store/ios`)
|
||||
- ExpoSharing (from `../../../node_modules/expo-sharing/ios`)
|
||||
- ExpoSharing (from `../node_modules/expo-sharing/ios`)
|
||||
- "ExpoWidgets (from `../../../node_modules/@bittingz/expo-widgets/ios`)"
|
||||
- EXUpdatesInterface (from `../../../node_modules/expo-updates-interface/ios`)
|
||||
- fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
|
||||
- FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
@@ -2293,6 +2350,7 @@ DEPENDENCIES:
|
||||
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||
- react-native-compressor (from `../../../node_modules/react-native-compressor`)
|
||||
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
|
||||
- "react-native-skia (from `../node_modules/@shopify/react-native-skia`)"
|
||||
- react-native-view-shot (from `../../../node_modules/react-native-view-shot`)
|
||||
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
@@ -2322,15 +2380,16 @@ DEPENDENCIES:
|
||||
- ReactCodegen (from `build/generated/ios`)
|
||||
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- "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`)"
|
||||
- "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)"
|
||||
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
|
||||
- "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)"
|
||||
- RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`)
|
||||
- "RNGoogleSignin (from `../../../node_modules/@react-native-google-signin/google-signin`)"
|
||||
- RNPurchases (from `../../../node_modules/react-native-purchases`)
|
||||
- RNReanimated (from `../../../node_modules/react-native-reanimated`)
|
||||
- RNScreens (from `../../../node_modules/react-native-screens`)
|
||||
- "RNSentry (from `../../../node_modules/@sentry/react-native`)"
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- WidgetBridge (from `../modules/widget-bridge/ios`)
|
||||
- Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -2340,9 +2399,14 @@ SPEC REPOS:
|
||||
- Firebase
|
||||
- FirebaseAnalytics
|
||||
- FirebaseCore
|
||||
- FirebaseCoreExtension
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseCrashlytics
|
||||
- FirebaseInstallations
|
||||
- FirebaseRemoteConfigInterop
|
||||
- FirebaseSessions
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleSignIn
|
||||
- GoogleUtilities
|
||||
- GTMAppAuth
|
||||
@@ -2351,12 +2415,12 @@ SPEC REPOS:
|
||||
- libdav1d
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- PurchasesHybridCommon
|
||||
- RevenueCat
|
||||
- SDWebImage
|
||||
- SDWebImageAVIFCoder
|
||||
- SDWebImageSVGCoder
|
||||
- Sentry
|
||||
- SocketRocket
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -2379,7 +2443,7 @@ EXTERNAL SOURCES:
|
||||
EXNotifications:
|
||||
:path: "../../../node_modules/expo-notifications/ios"
|
||||
Expo:
|
||||
:path: "../../../node_modules/expo"
|
||||
:path: "../node_modules/expo"
|
||||
expo-dev-client:
|
||||
:path: "../../../node_modules/expo-dev-client/ios"
|
||||
expo-dev-launcher:
|
||||
@@ -2425,7 +2489,9 @@ EXTERNAL SOURCES:
|
||||
ExpoSecureStore:
|
||||
:path: "../../../node_modules/expo-secure-store/ios"
|
||||
ExpoSharing:
|
||||
:path: "../../../node_modules/expo-sharing/ios"
|
||||
:path: "../node_modules/expo-sharing/ios"
|
||||
ExpoWidgets:
|
||||
:path: "../../../node_modules/@bittingz/expo-widgets/ios"
|
||||
EXUpdatesInterface:
|
||||
:path: "../../../node_modules/expo-updates-interface/ios"
|
||||
fast_float:
|
||||
@@ -2501,6 +2567,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../../node_modules/react-native-compressor"
|
||||
react-native-safe-area-context:
|
||||
:path: "../../../node_modules/react-native-safe-area-context"
|
||||
react-native-skia:
|
||||
:path: "../node_modules/@shopify/react-native-skia"
|
||||
react-native-view-shot:
|
||||
:path: "../../../node_modules/react-native-view-shot"
|
||||
React-nativeconfig:
|
||||
@@ -2560,9 +2628,11 @@ EXTERNAL SOURCES:
|
||||
RNCAsyncStorage:
|
||||
:path: "../../../node_modules/@react-native-async-storage/async-storage"
|
||||
RNFBAnalytics:
|
||||
:path: "../../../node_modules/@react-native-firebase/analytics"
|
||||
:path: "../node_modules/@react-native-firebase/analytics"
|
||||
RNFBApp:
|
||||
:path: "../../../node_modules/@react-native-firebase/app"
|
||||
:path: "../node_modules/@react-native-firebase/app"
|
||||
RNFBCrashlytics:
|
||||
:path: "../node_modules/@react-native-firebase/crashlytics"
|
||||
RNGestureHandler:
|
||||
:path: "../../../node_modules/react-native-gesture-handler"
|
||||
RNGoogleSignin:
|
||||
@@ -2573,10 +2643,10 @@ EXTERNAL SOURCES:
|
||||
:path: "../../../node_modules/react-native-reanimated"
|
||||
RNScreens:
|
||||
:path: "../../../node_modules/react-native-screens"
|
||||
RNSentry:
|
||||
:path: "../../../node_modules/@sentry/react-native"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
WidgetBridge:
|
||||
:path: "../modules/widget-bridge/ios"
|
||||
Yoga:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
@@ -2592,7 +2662,7 @@ SPEC CHECKSUMS:
|
||||
EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93
|
||||
EXManifests: a19d50504b8826546a4782770317bc83fffec87d
|
||||
EXNotifications: 9d3d17e52c95f377750d4a2ae553a716313dd4aa
|
||||
Expo: 3889933926fade17d8607f449c57b87c4022433f
|
||||
Expo: 4bb70893882e6382b41d1e910d7226c6a1b85f0a
|
||||
expo-dev-client: db44302cdbe0ec55b0ef1849c9a23a76dec6dbac
|
||||
expo-dev-launcher: c67c6a87f8a8ff54abda104abdc057ee4565a3f4
|
||||
expo-dev-menu: 1426660b67d2d594f70ec1d8a85bc26cafcf1bce
|
||||
@@ -2608,25 +2678,31 @@ SPEC CHECKSUMS:
|
||||
ExpoHead: 2a14db58219c0a506e4a409ea83f884eab23f555
|
||||
ExpoImage: d840b256050f4428d2942bc2b6e9251f9e0d7021
|
||||
ExpoImagePicker: 24e5ba8da111f74519b1e6dc556e0b438b2b8464
|
||||
ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680
|
||||
ExpoKeepAwake: 55f75eca6499bb9e4231ebad6f3e9cb8f99c0296
|
||||
ExpoLinearGradient: 35ebd83b16f80b3add053a2fd68cc328ed927f60
|
||||
ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402
|
||||
ExpoLocalAuthentication: eb2be9c7bcdc68e9434d4be4bb5aa5cf7943e816
|
||||
ExpoMediaLibrary: 2709fc2e4fcf87d28767714abcc603c4bec228c6
|
||||
ExpoModulesCore: 2d8d1c8e238bd5e1448b06c284ac896b162fb856
|
||||
ExpoSecureStore: 9a3665d7161dd1031be386e278bffd623658f316
|
||||
ExpoSharing: 0d983394ed4a80334bab5a0d5384f75710feb7e8
|
||||
ExpoSharing: 849a5ce9985c22598c16ec027e32969be8062e8e
|
||||
ExpoWidgets: 366fe61c681044e13b7cc344bdbd319ff5d54b28
|
||||
EXUpdatesInterface: 7c977640bdd8b85833c19e3959ba46145c5719db
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45
|
||||
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
|
||||
FirebaseAnalytics: f20bbad8cb7f65d8a5eaefeb424ae8800a31bdfc
|
||||
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
|
||||
FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
|
||||
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
|
||||
FirebaseCrashlytics: fb31c6907e5b52aa252668394d3f1ab326df1511
|
||||
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
|
||||
FirebaseRemoteConfigInterop: 869ddca16614f979e5c931ece11fbb0b8729ed41
|
||||
FirebaseSessions: d614ca154c63dbbc6c10d6c38259c2162c4e7c9b
|
||||
fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6
|
||||
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
|
||||
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
||||
@@ -2636,6 +2712,7 @@ SPEC CHECKSUMS:
|
||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
PurchasesHybridCommon: c2f8f5fcd2e472c585fccd1ab2fe317a3dc1171e
|
||||
RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17
|
||||
RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83
|
||||
@@ -2668,6 +2745,7 @@ SPEC CHECKSUMS:
|
||||
React-microtasksnativemodule: 338bfa0a7d8cd1ed212b055f2143a6789252137a
|
||||
react-native-compressor: 40b3e2b942aebac3daaae9549b23a13510c67a6f
|
||||
react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448
|
||||
react-native-skia: 0e98c0c8979795d3b812a45b3f6cde7e5d9bf267
|
||||
react-native-view-shot: 4147b714c5316d7e17ed652d0b5a182c028f842e
|
||||
React-nativeconfig: 415626a63057638759bcc75e0a96e2e07771a479
|
||||
React-NativeModulesApple: d33b55553c6957ff94835574636838d78121a1c6
|
||||
@@ -2699,21 +2777,21 @@ SPEC CHECKSUMS:
|
||||
RevenueCat: 347221fad5ab0ed5636546867deae2c32c64749f
|
||||
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
|
||||
RNFBAnalytics: d1c906bbb01b3a6f6b99d6eaa977f04b1ae5d279
|
||||
RNFBApp: fecce94543a693bcc73807a83ce5e9d70ff68725
|
||||
RNFBApp: 5c07dbcf30f00d7ea5400c3faae4e88e000b7685
|
||||
RNFBCrashlytics: bd6ae7c83468c749e986707c0e54365832b3d237
|
||||
RNGestureHandler: 2dd124adb171aab16629e9387cf1830957d242c2
|
||||
RNGoogleSignin: 5782dd7dad559608a06f3c6b1d4142c395fb5308
|
||||
RNPurchases: e265e2ed27349b567b91b89d8712aa8c9c57f5b0
|
||||
RNReanimated: 38e033fd1790c817678bc8a075277cdc49d6817c
|
||||
RNScreens: ad2d0cfb3f15c9ed56ffb43651366ec0dcd70c36
|
||||
RNSentry: a8f0d86b88675719ce96cb31bd9258dbd793e535
|
||||
RNSVG: 8542aa11770b27563714bbd8494a8436385fc85f
|
||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
|
||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||
Sentry: 1ca8405451040482877dcd344dfa3ef80b646631
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
WidgetBridge: 15f1466d7f79505a5e600e1400fcff1a40f807a4
|
||||
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
|
||||
|
||||
PODFILE CHECKSUM: 085b90662c05f70d43685ff8bec1ea714a98abb3
|
||||
PODFILE CHECKSUM: 3746f9f348a0d8fcc1584151ac3b5c42e0fa172e
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -10,41 +10,106 @@
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
2634B76381EF14A5455649E2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 56B77889699504CF056D3B82 /* PrivacyInfo.xcprivacy */; };
|
||||
301A4F5B84BB9DA22D6434C2 /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43233FEC4CFFD8C8AA8CD0C9 /* Pods_Sauci.framework */; };
|
||||
170EE6B9AAF2430B9AB5101D /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6823268A39AC4F3D974A70BB /* noop-file.swift */; };
|
||||
1963764A838740A9AB3B6EDB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 943B28EF95A6439B9246862F /* GoogleService-Info.plist */; };
|
||||
2BC8A1C04BCD698B29D2E6E8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 70C97E8BE29E641DB4CA72F3 /* PrivacyInfo.xcprivacy */; };
|
||||
2C1F3D17C413D275BE33BC07 /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCE00E1F3D8BD0B3956748EB /* Pods_Sauci.framework */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
839B1D252C4B42B3AF4A6509 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 93AA2882742645CC9B92CD37 /* GoogleService-Info.plist */; };
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
C09531978862477F9D6CB18B /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A46CE15AF447C78955EED5 /* noop-file.swift */; };
|
||||
XX12B55A1EDABF083C3B51XX /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */; };
|
||||
XX6A44985675A04437B8C4XX /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = XX4DFF38D47332D6BF0183XX /* SwiftUI.framework */; };
|
||||
XXFA0BA4D1DD274640D183XX /* LiveDrawWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = XXAC1FC213FDAA75508994XX /* LiveDrawWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
XXA7C7D5580082A497FD95XX /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = XX540FB4ABC75CE91A6036XX;
|
||||
remoteInfo = LiveDrawWidget;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
XX72A8CFBF0FBA13A79902XX /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
XXFA0BA4D1DD274640D183XX /* LiveDrawWidget.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
3BD07AB1AFFA49C7B31B5B70 /* 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>"; };
|
||||
43233FEC4CFFD8C8AA8CD0C9 /* Pods_Sauci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sauci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
56B77889699504CF056D3B82 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
68A46CE15AF447C78955EED5 /* 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>"; };
|
||||
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>"; };
|
||||
93AA2882742645CC9B92CD37 /* 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
13B07F961A680F5B00A75B9A /* Sauci.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; fileEncoding = 4; includeInIndex = 0; path = Sauci.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Sauci/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Sauci/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sauci/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sauci/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Sauci/main.m; sourceTree = "<group>"; };
|
||||
6823268A39AC4F3D974A70BB /* 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; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.debug.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
70C97E8BE29E641DB4CA72F3 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.release.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.release.xcconfig"; sourceTree = "<group>"; };
|
||||
943B28EF95A6439B9246862F /* 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>"; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Sauci/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
D927CBDDA31C4219BE8F155B /* 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>"; };
|
||||
DCE00E1F3D8BD0B3956748EB /* Pods_Sauci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sauci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
XX4DFF38D47332D6BF0183XX /* SwiftUI.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
XXAC1FC213FDAA75508994XX /* LiveDrawWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; fileEncoding = 4; includeInIndex = 0; path = LiveDrawWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
XX020388E3BB9C764E774FXX /* Exceptions for "widget" folder in "LiveDrawWidget" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
"expo-target.config.js",
|
||||
);
|
||||
target = XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
XX683E29AD7AD8279E17B2XX /* widget */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
XX020388E3BB9C764E774FXX /* Exceptions for "widget" folder in "LiveDrawWidget" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = widget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
301A4F5B84BB9DA22D6434C2 /* Pods_Sauci.framework in Frameworks */,
|
||||
2C1F3D17C413D275BE33BC07 /* Pods_Sauci.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
XX074B8811C1CF8E8A3D74XX /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
XX12B55A1EDABF083C3B51XX /* WidgetKit.framework in Frameworks */,
|
||||
XX6A44985675A04437B8C4XX /* SwiftUI.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -61,10 +126,10 @@
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
13B07FB71A68108700A75B9A /* main.m */,
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||
93AA2882742645CC9B92CD37 /* GoogleService-Info.plist */,
|
||||
68A46CE15AF447C78955EED5 /* noop-file.swift */,
|
||||
3BD07AB1AFFA49C7B31B5B70 /* Sauci-Bridging-Header.h */,
|
||||
56B77889699504CF056D3B82 /* PrivacyInfo.xcprivacy */,
|
||||
943B28EF95A6439B9246862F /* GoogleService-Info.plist */,
|
||||
6823268A39AC4F3D974A70BB /* noop-file.swift */,
|
||||
D927CBDDA31C4219BE8F155B /* Sauci-Bridging-Header.h */,
|
||||
70C97E8BE29E641DB4CA72F3 /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
name = Sauci;
|
||||
sourceTree = "<group>";
|
||||
@@ -73,7 +138,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
43233FEC4CFFD8C8AA8CD0C9 /* Pods_Sauci.framework */,
|
||||
XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */,
|
||||
XX4DFF38D47332D6BF0183XX /* SwiftUI.framework */,
|
||||
DCE00E1F3D8BD0B3956748EB /* Pods_Sauci.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -88,6 +155,7 @@
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
XX3E41C1850246162C0061XX /* expo:targets */,
|
||||
13B07FAE1A68108700A75B9A /* Sauci */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
@@ -104,6 +172,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* Sauci.app */,
|
||||
XXAC1FC213FDAA75508994XX /* LiveDrawWidget.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -142,6 +211,15 @@
|
||||
name = ExpoModulesProviders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
XX3E41C1850246162C0061XX /* expo:targets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
XX683E29AD7AD8279E17B2XX /* widget */,
|
||||
);
|
||||
name = "expo:targets";
|
||||
path = ../targets;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -150,25 +228,47 @@
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Sauci" */;
|
||||
buildPhases = (
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||
B2B15A2D43DFA021A8882896 /* [Expo] Configure project */,
|
||||
78092B71106F49D87690DCAF /* [Expo] Configure project */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||
B0D9F1E2B09440679F2212B8 /* Upload Debug Symbols to Sentry */,
|
||||
3944CF2ABF1FE1D2CEEDB515 /* [CP] Embed Pods Frameworks */,
|
||||
C94C12D7632AB41D3D9E4FCA /* [CP-User] [RNFB] Core Configuration */,
|
||||
XX72A8CFBF0FBA13A79902XX /* Embed Foundation Extensions */,
|
||||
40AF2F5BA5CA84AF70BDCE64 /* [CP] Embed Pods Frameworks */,
|
||||
D2EE170024D385D3D7E9FF8F /* [CP-User] [RNFB] Core Configuration */,
|
||||
CCC1C00E4CE250301404ADA6 /* [CP-User] [RNFB] Crashlytics Configuration */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
XX97289CEC7994AD5F4142XX /* PBXTargetDependency */,
|
||||
);
|
||||
name = Sauci;
|
||||
productName = Sauci;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* Sauci.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = XXBAA32A968BCCE457D462XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */;
|
||||
buildPhases = (
|
||||
XX074B8811C1CF8E8A3D74XX /* Frameworks */,
|
||||
XXE45443DC38DCFE1DB622XX /* Sources */,
|
||||
XXFA5A8CCDD805DF7AEF9CXX /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
XX683E29AD7AD8279E17B2XX /* widget */,
|
||||
);
|
||||
name = LiveDrawWidget;
|
||||
productName = LiveDrawWidget;
|
||||
productReference = XXAC1FC213FDAA75508994XX /* LiveDrawWidget.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -179,7 +279,10 @@
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1250;
|
||||
DevelopmentTeam = "BXVPBATDZY";
|
||||
};
|
||||
XX540FB4ABC75CE91A6036XX = {
|
||||
CreatedOnToolsVersion = 14.3;
|
||||
DevelopmentTeam = BXVPBATDZY;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
@@ -198,6 +301,7 @@
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* Sauci */,
|
||||
XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -210,8 +314,15 @@
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||
839B1D252C4B42B3AF4A6509 /* GoogleService-Info.plist in Resources */,
|
||||
2634B76381EF14A5455649E2 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
1963764A838740A9AB3B6EDB /* GoogleService-Info.plist in Resources */,
|
||||
2BC8A1C04BCD698B29D2E6E8 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
XXFA5A8CCDD805DF7AEF9CXX /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -224,14 +335,18 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
|
||||
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
|
||||
};
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -255,7 +370,7 @@
|
||||
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;
|
||||
};
|
||||
3944CF2ABF1FE1D2CEEDB515 /* [CP] Embed Pods Frameworks */ = {
|
||||
40AF2F5BA5CA84AF70BDCE64 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -273,93 +388,7 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/AppAuth/AppAuthCore_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_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}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RevenueCat/RevenueCat.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AppAuthCore_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_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}/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}/GTMSessionFetcher_Core_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RevenueCat.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.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}/glog_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
B0D9F1E2B09440679F2212B8 /* Upload Debug Symbols to Sentry */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Upload Debug Symbols to Sentry";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
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'\"`";
|
||||
};
|
||||
B2B15A2D43DFA021A8882896 /* [Expo] Configure project */ = {
|
||||
78092B71106F49D87690DCAF /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -378,7 +407,99 @@
|
||||
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";
|
||||
};
|
||||
C94C12D7632AB41D3D9E4FCA /* [CP-User] [RNFB] Core Configuration */ = {
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/AppAuth/AppAuthCore_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCrashlytics/FirebaseCrashlytics_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInstallations/FirebaseInstallations_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GTMAppAuth/GTMAppAuth_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RevenueCat/RevenueCat.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AppAuthCore_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_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}/ExpoMediaLibrary_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCrashlytics_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}/GTMSessionFetcher_Core_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RevenueCat.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.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}/glog_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
CCC1C00E4CE250301404ADA6 /* [CP-User] [RNFB] Crashlytics Configuration */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}",
|
||||
"$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)",
|
||||
);
|
||||
name = "[CP-User] [RNFB] Crashlytics 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#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n";
|
||||
};
|
||||
D2EE170024D385D3D7E9FF8F /* [CP-User] [RNFB] Core Configuration */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -401,12 +522,27 @@
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||
C09531978862477F9D6CB18B /* noop-file.swift in Sources */,
|
||||
170EE6B9AAF2430B9AB5101D /* noop-file.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
XXE45443DC38DCFE1DB622XX /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
XX97289CEC7994AD5F4142XX /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */;
|
||||
targetProxy = XXA7C7D5580082A497FD95XX /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
@@ -416,6 +552,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
@@ -424,7 +561,7 @@
|
||||
INFOPLIST_FILE = Sauci/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -432,15 +569,12 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app;
|
||||
PRODUCT_NAME = "Sauci";
|
||||
PRODUCT_NAME = Sauci;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Sauci/Sauci-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
DEVELOPMENT_TEAM = "BXVPBATDZY";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -452,10 +586,11 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Sauci/Sauci.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||
INFOPLIST_FILE = Sauci/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -463,14 +598,11 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app;
|
||||
PRODUCT_NAME = "Sauci";
|
||||
PRODUCT_NAME = Sauci;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Sauci/Sauci-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
DEVELOPMENT_TEAM = "BXVPBATDZY";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -614,6 +746,80 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
XX7511AEB23A8B63601AE1XX /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ../targets/widget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = LiveDrawWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app.LiveDrawWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
XX917F28A41B713380F670XX /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ../targets/widget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = LiveDrawWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app.LiveDrawWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -635,6 +841,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
XXBAA32A968BCCE457D462XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
XX7511AEB23A8B63601AE1XX /* Debug */,
|
||||
XX917F28A41B713380F670XX /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
|
||||
@@ -8,5 +8,9 @@
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.sauci.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,4 +0,0 @@
|
||||
defaults.url=https://sentry.io/
|
||||
defaults.org=molten-prints
|
||||
defaults.project=sauci
|
||||
# Using SENTRY_AUTH_TOKEN environment variable
|
||||
@@ -0,0 +1,63 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
group = 'expo.modules.widgetbridge'
|
||||
version = '0.0.1'
|
||||
|
||||
buildscript {
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
if (expoModulesCorePlugin.exists()) {
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
url = mavenLocal().url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "expo.modules.widgetbridge"
|
||||
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet("minSdkVersion", 23)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
}
|
||||
|
||||
publishing {
|
||||
singleVariant("release") {
|
||||
withSourcesJar()
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.majorVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':expo-modules-core')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package expo.modules.widgetbridge
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class WidgetBridgeModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("WidgetBridge")
|
||||
|
||||
Function("setWidgetData") { key: String, value: String ->
|
||||
val ctx = requireNotNull(appContext.reactContext)
|
||||
ctx.getSharedPreferences("${ctx.packageName}.widgetdata", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(key, value)
|
||||
.apply()
|
||||
}
|
||||
|
||||
Function("reloadWidgets") {
|
||||
val ctx = requireNotNull(appContext.reactContext)
|
||||
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
|
||||
val widgetManager = AppWidgetManager.getInstance(ctx)
|
||||
val providers = ctx.packageManager.queryBroadcastReceivers(
|
||||
Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE),
|
||||
PackageManager.GET_META_DATA
|
||||
)
|
||||
for (provider in providers) {
|
||||
if (provider.activityInfo.packageName == ctx.packageName) {
|
||||
val component = ComponentName(
|
||||
provider.activityInfo.packageName,
|
||||
provider.activityInfo.name
|
||||
)
|
||||
val ids = widgetManager.getAppWidgetIds(component)
|
||||
if (ids.isNotEmpty()) {
|
||||
val updateIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
|
||||
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
updateIntent.component = component
|
||||
ctx.sendBroadcast(updateIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"platforms": ["ios", "android"],
|
||||
"ios": {
|
||||
"modules": ["WidgetBridgeModule"]
|
||||
},
|
||||
"android": {
|
||||
"modules": ["expo.modules.widgetbridge.WidgetBridgeModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { requireNativeModule } from "expo";
|
||||
|
||||
const WidgetBridge = requireNativeModule("WidgetBridge");
|
||||
|
||||
export function setWidgetData(key: string, value: string): void {
|
||||
WidgetBridge.setWidgetData(key, value);
|
||||
}
|
||||
|
||||
export function reloadWidgets(): void {
|
||||
WidgetBridge.reloadWidgets();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'WidgetBridge'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'Bridge for writing widget data to shared storage'
|
||||
s.description = 'Expo module for writing to UserDefaults/SharedPreferences and reloading widgets'
|
||||
s.author = ''
|
||||
s.homepage = 'https://github.com/example'
|
||||
s.platforms = { :ios => '15.1' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
s.source_files = '**/*.{h,m,mm,swift,hpp,cpp}'
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
import ExpoModulesCore
|
||||
import WidgetKit
|
||||
|
||||
public class WidgetBridgeModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("WidgetBridge")
|
||||
|
||||
Function("setWidgetData") { (key: String, value: String) in
|
||||
let suite = UserDefaults(suiteName: "group.com.sauci.app")
|
||||
suite?.set(value, forKey: key)
|
||||
}
|
||||
|
||||
Function("reloadWidgets") {
|
||||
if #available(iOS 14.0, *) {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@sauci/mobile",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"main": "./index.js",
|
||||
"scripts": {
|
||||
"dev": "expo start",
|
||||
"android": "expo run:android",
|
||||
@@ -19,9 +19,12 @@
|
||||
"release:android:preview": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile preview --local"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bacons/apple-targets": "^0.1.6",
|
||||
"@bittingz/expo-widgets": "^3.0.2",
|
||||
"@expo/ngrok": "^4.1.0",
|
||||
"@expo/vector-icons": "~14.0.4",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-community/cli": "^20.1.1",
|
||||
"@react-native-firebase/analytics": "^23.8.2",
|
||||
"@react-native-firebase/app": "^23.8.2",
|
||||
"@react-native-firebase/crashlytics": "^23.8.4",
|
||||
@@ -29,6 +32,7 @@
|
||||
"@react-navigation/bottom-tabs": "^7.0.0",
|
||||
"@react-navigation/native": "^7.0.0",
|
||||
"@react-navigation/native-stack": "^7.0.0",
|
||||
"@shopify/react-native-skia": "^1.12.4",
|
||||
"@supabase/supabase-js": "^2.39.0",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"babel-preset-expo": "~12.0.0",
|
||||
@@ -59,6 +63,7 @@
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-android-widget": "^0.20.1",
|
||||
"react-native-compressor": "^1.13.0",
|
||||
"react-native-gesture-handler": "~2.20.0",
|
||||
"react-native-purchases": "^9.6.12",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, TouchableOpacity, Dimensions } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import { colors, spacing, radius, typography, shadows, tileColors } from '../../theme';
|
||||
import { useAuthStore } from '../../store';
|
||||
|
||||
const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||
const HORIZONTAL_PADDING = spacing.lg;
|
||||
const TILE_HEIGHT = 100;
|
||||
|
||||
interface LiveDrawTileProps {
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export function LiveDrawTile({ delay = 0 }: LiveDrawTileProps) {
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const handlePress = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
||||
if (!user?.couple_id) return;
|
||||
|
||||
router.push({ pathname: '/(app)/live-draw', params: { coupleId: user.couple_id } } as any);
|
||||
};
|
||||
|
||||
if (!user?.couple_id) return null;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(delay).duration(500).springify()}
|
||||
style={styles.container}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.sectionTitle}>Activities</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
activeOpacity={0.85}
|
||||
style={styles.tileWrapper}
|
||||
>
|
||||
<View style={[styles.tile, shadows.md]}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons name="brush" size={24} color="rgba(255, 255, 255, 0.9)" />
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Live Draw</Text>
|
||||
<Text style={styles.subtitle}>Draw together with your partner in real-time</Text>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={20} color="rgba(255, 255, 255, 0.6)" />
|
||||
|
||||
<View style={styles.overlay} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: HORIZONTAL_PADDING,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
sectionTitle: {
|
||||
...typography.callout,
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
tileWrapper: {
|
||||
paddingHorizontal: HORIZONTAL_PADDING,
|
||||
},
|
||||
tile: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: tileColors.teal,
|
||||
borderRadius: radius.lg,
|
||||
padding: spacing.md,
|
||||
height: TILE_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
iconContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
fontWeight: '700',
|
||||
},
|
||||
subtitle: {
|
||||
...typography.caption1,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
marginTop: 2,
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
borderRadius: radius.lg,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
});
|
||||
@@ -2,3 +2,4 @@ export { ContentTile, TILE_WIDTH, TILE_HEIGHT } from './ContentTile';
|
||||
export { ContentRow } from './ContentRow';
|
||||
export { CompactHeader } from './CompactHeader';
|
||||
export { CircularProgress } from './CircularProgress';
|
||||
export { LiveDrawTile } from './LiveDrawTile';
|
||||
@@ -0,0 +1,273 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
import { DrawingCanvas } from './components/DrawingCanvas';
|
||||
import type { DrawingCanvasHandle } from './components/DrawingCanvas';
|
||||
import { DrawingToolbar } from './components/DrawingToolbar';
|
||||
import { DrawingShareModal } from './components/DrawingShareModal';
|
||||
import { useDrawingHistory } from './hooks/useDrawingHistory';
|
||||
import { useDrawingSync } from './hooks/useDrawingSync';
|
||||
import { useCanvasCapture } from './hooks/useCanvasCapture';
|
||||
import { useAuthStore } from '../../store';
|
||||
import { colors, spacing, typography } from '../../theme';
|
||||
import { GradientBackground } from '../../components/ui';
|
||||
import type { StrokeSegment, StrokePoint } from './types';
|
||||
import {
|
||||
BRUSH_DEFAULT_WIDTH,
|
||||
PRESET_COLORS,
|
||||
MAX_STROKES,
|
||||
} from './constants';
|
||||
|
||||
export const LiveDrawScreen: React.FC = () => {
|
||||
const { coupleId: coupleIdParam } = useLocalSearchParams<{ coupleId: string }>();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { user } = useAuthStore();
|
||||
const coupleId = coupleIdParam || user?.couple_id;
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
|
||||
const [selectedColor, setSelectedColor] = useState(PRESET_COLORS[0]);
|
||||
const [brushWidth, setBrushWidth] = useState(BRUSH_DEFAULT_WIDTH);
|
||||
const [isEraser, setIsEraser] = useState(false);
|
||||
const [showShareModal, setShowShareModal] = useState(false);
|
||||
const [shareDrawingUri, setShareDrawingUri] = useState<string | null>(null);
|
||||
|
||||
const canvasRef = useRef<DrawingCanvasHandle>(null);
|
||||
// Keep track of the current stroke id for sync broadcasts
|
||||
const currentStrokeIdRef = useRef<string | null>(null);
|
||||
|
||||
const canvasWidth = screenWidth - spacing.md * 2;
|
||||
const canvasHeight = canvasWidth * 1.2;
|
||||
|
||||
const {
|
||||
strokes,
|
||||
setStrokes,
|
||||
addStroke,
|
||||
updateStroke,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
clearAll,
|
||||
} = useDrawingHistory();
|
||||
|
||||
const { makeSnapshot, captureToFile } = useCanvasCapture();
|
||||
|
||||
const handleStrokeStartFromPartner = useCallback(
|
||||
(stroke: StrokeSegment) => addStroke(stroke),
|
||||
[addStroke]
|
||||
);
|
||||
const handleStrokeContinueFromPartner = useCallback(
|
||||
(strokeId: string, points: StrokePoint[]) => updateStroke(strokeId, points),
|
||||
[updateStroke]
|
||||
);
|
||||
const handleStrokeEndFromPartner = useCallback((_strokeId: string) => {}, []);
|
||||
const handleClearFromPartner = useCallback(() => clearAll(), [clearAll]);
|
||||
const handleUndoFromPartner = useCallback(
|
||||
(strokeId: string) => {
|
||||
setStrokes((prev) => prev.filter((s) => s.id !== strokeId));
|
||||
},
|
||||
[setStrokes]
|
||||
);
|
||||
|
||||
const sync = useDrawingSync({
|
||||
coupleId: coupleId!,
|
||||
userId: user?.id,
|
||||
onStrokeStart: handleStrokeStartFromPartner,
|
||||
onStrokeContinue: handleStrokeContinueFromPartner,
|
||||
onStrokeEnd: handleStrokeEndFromPartner,
|
||||
onClearCanvas: handleClearFromPartner,
|
||||
onUndo: handleUndoFromPartner,
|
||||
onRedo: handleStrokeStartFromPartner,
|
||||
onLoadStrokes: (loaded) => setStrokes(loaded),
|
||||
});
|
||||
|
||||
const handleTouchStart = useCallback(
|
||||
(point: StrokePoint) => {
|
||||
if (!user?.id) return;
|
||||
if (strokes.length >= MAX_STROKES) {
|
||||
Alert.alert('Canvas Full', 'Save your drawing and start fresh.');
|
||||
return;
|
||||
}
|
||||
|
||||
const stroke: StrokeSegment = {
|
||||
id: `${user.id}_${Date.now()}`,
|
||||
userId: user.id,
|
||||
points: [point],
|
||||
color: selectedColor,
|
||||
width: brushWidth,
|
||||
timestamp: Date.now(),
|
||||
isEraser,
|
||||
};
|
||||
currentStrokeIdRef.current = stroke.id;
|
||||
canvasRef.current?.startStroke(stroke);
|
||||
sync.broadcastStrokeStart(stroke);
|
||||
},
|
||||
[user?.id, selectedColor, brushWidth, isEraser, strokes.length, sync]
|
||||
);
|
||||
|
||||
const handleTouchMove = useCallback(
|
||||
(point: StrokePoint) => {
|
||||
if (!currentStrokeIdRef.current) return;
|
||||
canvasRef.current?.addPoint(point);
|
||||
sync.broadcastStrokeContinue(currentStrokeIdRef.current, [point]);
|
||||
},
|
||||
[sync]
|
||||
);
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
if (!currentStrokeIdRef.current) return;
|
||||
const finished = canvasRef.current?.endStroke() ?? null;
|
||||
currentStrokeIdRef.current = null;
|
||||
if (finished) {
|
||||
addStroke(finished);
|
||||
sync.broadcastStrokeEnd(finished.id);
|
||||
sync.persistStrokes([...strokes, finished]);
|
||||
}
|
||||
}, [addStroke, strokes, sync]);
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
if (!user?.id) return;
|
||||
const removed = undo(user.id);
|
||||
if (removed) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
sync.broadcastUndo(removed.id);
|
||||
sync.persistStrokes(strokes.filter((s) => s.id !== removed.id));
|
||||
}
|
||||
}, [user?.id, undo, strokes, sync]);
|
||||
|
||||
const handleRedo = useCallback(() => {
|
||||
if (!user?.id) return;
|
||||
const restored = redo(user.id);
|
||||
if (restored) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
sync.broadcastRedo(restored);
|
||||
sync.persistStrokes([...strokes, restored]);
|
||||
}
|
||||
}, [user?.id, redo, strokes, sync]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
Alert.alert('Clear Canvas', 'This will erase the entire drawing.', [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
clearAll();
|
||||
sync.broadcastClearCanvas();
|
||||
sync.persistStrokes([]);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}, [clearAll, sync]);
|
||||
|
||||
const handleShare = useCallback(async () => {
|
||||
const uri = await captureToFile();
|
||||
if (uri) {
|
||||
setShareDrawingUri(uri);
|
||||
setShowShareModal(true);
|
||||
} else {
|
||||
Alert.alert('Error', 'Failed to capture drawing');
|
||||
}
|
||||
}, [captureToFile]);
|
||||
|
||||
const handleToggleEraser = useCallback(() => {
|
||||
setIsEraser((prev) => !prev);
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GradientBackground>
|
||||
<View style={[styles.header, { paddingTop: insets.top + spacing.sm }]}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Live Draw</Text>
|
||||
<View style={styles.backButton} />
|
||||
</View>
|
||||
|
||||
<View style={styles.canvasContainer}>
|
||||
<DrawingCanvas
|
||||
ref={canvasRef}
|
||||
strokes={strokes}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onCanvasReady={(snapshot) => {
|
||||
makeSnapshot.current = snapshot;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.toolbarContainer, { paddingBottom: insets.bottom + spacing.sm }]}>
|
||||
<DrawingToolbar
|
||||
selectedColor={selectedColor}
|
||||
brushWidth={brushWidth}
|
||||
isEraser={isEraser}
|
||||
canUndo={canUndo(user?.id ?? '')}
|
||||
canRedo={canRedo(user?.id ?? '')}
|
||||
onColorChange={(c) => {
|
||||
setSelectedColor(c);
|
||||
setIsEraser(false);
|
||||
}}
|
||||
onBrushWidthChange={setBrushWidth}
|
||||
onToggleEraser={handleToggleEraser}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
onClear={handleClear}
|
||||
onSave={handleShare}
|
||||
/>
|
||||
</View>
|
||||
<DrawingShareModal
|
||||
visible={showShareModal}
|
||||
onClose={() => setShowShareModal(false)}
|
||||
drawingUri={shareDrawingUri}
|
||||
/>
|
||||
</GradientBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: spacing.sm,
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
},
|
||||
canvasContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
toolbarContainer: {
|
||||
backgroundColor: colors.backgroundLight,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
import React, { useMemo, useRef, useEffect, useCallback, useState, useImperativeHandle, forwardRef } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import {
|
||||
Canvas,
|
||||
Path,
|
||||
Skia,
|
||||
} from '@shopify/react-native-skia';
|
||||
import type { SkImage } from '@shopify/react-native-skia';
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import { runOnJS } from 'react-native-reanimated';
|
||||
import type { StrokeSegment, StrokePoint } from '../types';
|
||||
import { CANVAS_BACKGROUND } from '../constants';
|
||||
|
||||
export interface DrawingCanvasHandle {
|
||||
startStroke: (stroke: StrokeSegment) => void;
|
||||
addPoint: (point: StrokePoint) => void;
|
||||
endStroke: () => StrokeSegment | null;
|
||||
}
|
||||
|
||||
interface DrawingCanvasProps {
|
||||
strokes: StrokeSegment[];
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
onTouchStart: (point: StrokePoint) => void;
|
||||
onTouchMove: (point: StrokePoint) => void;
|
||||
onTouchEnd: () => void;
|
||||
onCanvasReady?: (makeSnapshot: () => SkImage | null) => void;
|
||||
}
|
||||
|
||||
function buildPath(points: StrokePoint[], width: number, height: number) {
|
||||
const path = Skia.Path.Make();
|
||||
if (points.length === 0) return path;
|
||||
|
||||
const first = points[0];
|
||||
path.moveTo(first.x * width, first.y * height);
|
||||
|
||||
if (points.length === 1) {
|
||||
path.lineTo(first.x * width + 0.1, first.y * height + 0.1);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Quadratic bezier smoothing
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const curr = points[i];
|
||||
const midX = ((prev.x + curr.x) / 2) * width;
|
||||
const midY = ((prev.y + curr.y) / 2) * height;
|
||||
|
||||
if (i === 1) {
|
||||
path.lineTo(midX, midY);
|
||||
} else {
|
||||
path.quadTo(prev.x * width, prev.y * height, midX, midY);
|
||||
}
|
||||
}
|
||||
|
||||
const last = points[points.length - 1];
|
||||
path.lineTo(last.x * width, last.y * height);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
const StrokePath = React.memo(function StrokePath({
|
||||
stroke,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
}: {
|
||||
stroke: StrokeSegment;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}) {
|
||||
const path = useMemo(
|
||||
() => buildPath(stroke.points, canvasWidth, canvasHeight),
|
||||
[stroke.points, canvasWidth, canvasHeight]
|
||||
);
|
||||
|
||||
return (
|
||||
<Path
|
||||
path={path}
|
||||
color={stroke.isEraser ? CANVAS_BACKGROUND : stroke.color}
|
||||
style="stroke"
|
||||
strokeWidth={stroke.width}
|
||||
strokeCap="round"
|
||||
strokeJoin="round"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Renders the in-progress stroke from a ref, re-rendered only via rafTick
|
||||
const CurrentStrokePath = React.memo(function CurrentStrokePath({
|
||||
strokeRef,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
_tick,
|
||||
}: {
|
||||
strokeRef: React.RefObject<StrokeSegment | null>;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
_tick: number; // forces re-render at RAF cadence
|
||||
}) {
|
||||
const stroke = strokeRef.current;
|
||||
if (!stroke) return null;
|
||||
|
||||
const path = buildPath(stroke.points, canvasWidth, canvasHeight);
|
||||
|
||||
return (
|
||||
<Path
|
||||
path={path}
|
||||
color={stroke.isEraser ? CANVAS_BACKGROUND : stroke.color}
|
||||
style="stroke"
|
||||
strokeWidth={stroke.width}
|
||||
strokeCap="round"
|
||||
strokeJoin="round"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const DrawingCanvas = forwardRef<DrawingCanvasHandle, DrawingCanvasProps>(
|
||||
function DrawingCanvas(
|
||||
{
|
||||
strokes,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
onTouchStart,
|
||||
onTouchMove,
|
||||
onTouchEnd,
|
||||
onCanvasReady,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const canvasRef = useRef<any>(null);
|
||||
const currentStrokeRef = useRef<StrokeSegment | null>(null);
|
||||
const rafScheduled = useRef(false);
|
||||
const [rafTick, setRafTick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (onCanvasReady) {
|
||||
onCanvasReady(() => {
|
||||
return canvasRef.current?.makeImageSnapshot() ?? null;
|
||||
});
|
||||
}
|
||||
}, [onCanvasReady]);
|
||||
|
||||
const scheduleRafUpdate = useCallback(() => {
|
||||
if (rafScheduled.current) return;
|
||||
rafScheduled.current = true;
|
||||
requestAnimationFrame(() => {
|
||||
rafScheduled.current = false;
|
||||
setRafTick((t) => t + 1);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
startStroke(stroke: StrokeSegment) {
|
||||
currentStrokeRef.current = stroke;
|
||||
scheduleRafUpdate();
|
||||
},
|
||||
addPoint(point: StrokePoint) {
|
||||
const s = currentStrokeRef.current;
|
||||
if (!s) return;
|
||||
// Mutate in place — no React state involved
|
||||
s.points.push(point);
|
||||
scheduleRafUpdate();
|
||||
},
|
||||
endStroke(): StrokeSegment | null {
|
||||
const s = currentStrokeRef.current;
|
||||
currentStrokeRef.current = null;
|
||||
scheduleRafUpdate();
|
||||
return s;
|
||||
},
|
||||
}), [scheduleRafUpdate]);
|
||||
|
||||
const pan = Gesture.Pan()
|
||||
.minDistance(0)
|
||||
.onStart((e) => {
|
||||
runOnJS(onTouchStart)({
|
||||
x: e.x / canvasWidth,
|
||||
y: e.y / canvasHeight,
|
||||
});
|
||||
})
|
||||
.onUpdate((e) => {
|
||||
runOnJS(onTouchMove)({
|
||||
x: e.x / canvasWidth,
|
||||
y: e.y / canvasHeight,
|
||||
});
|
||||
})
|
||||
.onEnd(() => {
|
||||
runOnJS(onTouchEnd)();
|
||||
});
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={pan}>
|
||||
<View style={[styles.container, { width: canvasWidth, height: canvasHeight }]}>
|
||||
<Canvas ref={canvasRef} style={styles.canvas}>
|
||||
{strokes.map((stroke) => (
|
||||
<StrokePath
|
||||
key={stroke.id}
|
||||
stroke={stroke}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
/>
|
||||
))}
|
||||
<CurrentStrokePath
|
||||
strokeRef={currentStrokeRef}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
_tick={rafTick}
|
||||
/>
|
||||
</Canvas>
|
||||
</View>
|
||||
</GestureDetector>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: CANVAS_BACKGROUND,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
canvas: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,401 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Modal,
|
||||
TouchableOpacity,
|
||||
Platform,
|
||||
Linking,
|
||||
Share,
|
||||
Image,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
SlideInDown,
|
||||
SlideOutDown,
|
||||
} from 'react-native-reanimated';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import ViewShot from 'react-native-view-shot';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import { colors, gradients, radius, typography, spacing, blur } from '../../../theme';
|
||||
|
||||
const logo = require('../../../../assets/logo.png');
|
||||
|
||||
const MAX_CARD_WIDTH = 340;
|
||||
|
||||
interface DrawingShareModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
drawingUri: string | null;
|
||||
}
|
||||
|
||||
export function DrawingShareModal({
|
||||
visible,
|
||||
onClose,
|
||||
drawingUri,
|
||||
}: DrawingShareModalProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
const cardWidth = Math.min(screenWidth - 64, MAX_CARD_WIDTH);
|
||||
const shareViewRef = useRef<ViewShot>(null);
|
||||
const [capturedUri, setCapturedUri] = useState<string | null>(null);
|
||||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
|
||||
// Capture the branded card when modal becomes visible and drawing is ready
|
||||
useEffect(() => {
|
||||
if (visible && drawingUri && shareViewRef.current) {
|
||||
setIsCapturing(true);
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const uri = await shareViewRef.current?.capture?.();
|
||||
if (uri) {
|
||||
setCapturedUri(uri);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error capturing share image:', error);
|
||||
} finally {
|
||||
setIsCapturing(false);
|
||||
}
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [visible, drawingUri]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setCapturedUri(null);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const handleInstagramShare = async () => {
|
||||
if (!capturedUri) return;
|
||||
|
||||
try {
|
||||
const instagramUrl = 'instagram://app';
|
||||
const canOpenInstagram = await Linking.canOpenURL(instagramUrl);
|
||||
|
||||
if (canOpenInstagram && Platform.OS === 'ios') {
|
||||
const instagramStoriesUrl = `instagram-stories://share?source_application=com.sauci.app`;
|
||||
await Linking.openURL(instagramStoriesUrl);
|
||||
} else {
|
||||
await handleMoreShare();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Instagram share error:', error);
|
||||
await handleMoreShare();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMessagesShare = async () => {
|
||||
if (!capturedUri) {
|
||||
await Share.share({ message: 'Check out our drawing on sauci.app' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Platform.OS === 'ios') {
|
||||
await Sharing.shareAsync(capturedUri, {
|
||||
mimeType: 'image/png',
|
||||
UTI: 'public.png',
|
||||
});
|
||||
} else {
|
||||
await handleMoreShare();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Messages share error:', error);
|
||||
await Share.share({ message: 'Check out our drawing on sauci.app' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoreShare = async () => {
|
||||
if (!capturedUri) {
|
||||
await Share.share({ message: 'Check out our drawing on sauci.app' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isAvailable = await Sharing.isAvailableAsync();
|
||||
if (isAvailable) {
|
||||
await Sharing.shareAsync(capturedUri, {
|
||||
mimeType: 'image/png',
|
||||
dialogTitle: 'Share drawing',
|
||||
});
|
||||
} else {
|
||||
await Share.share({ message: 'Check out our drawing on sauci.app' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Share error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const useBlur = Platform.OS === 'ios';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="none"
|
||||
statusBarTranslucent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
{/* Backdrop */}
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(200)}
|
||||
style={StyleSheet.absoluteFill}
|
||||
>
|
||||
{useBlur ? (
|
||||
<BlurView
|
||||
intensity={blur.heavy}
|
||||
tint="dark"
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.backdropFallback} />
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
{/* Content */}
|
||||
<Animated.View
|
||||
entering={SlideInDown.springify().damping(20)}
|
||||
exiting={SlideOutDown.duration(200)}
|
||||
style={[styles.content, { paddingTop: insets.top }]}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={onClose}
|
||||
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
||||
>
|
||||
<Ionicons name="close" size={28} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Share</Text>
|
||||
<View style={styles.headerSpacer} />
|
||||
</View>
|
||||
|
||||
{/* Branded Card Preview */}
|
||||
<View style={styles.previewContainer}>
|
||||
<ViewShot
|
||||
ref={shareViewRef}
|
||||
options={{
|
||||
format: 'png',
|
||||
quality: 1,
|
||||
result: 'tmpfile',
|
||||
}}
|
||||
>
|
||||
<View style={[styles.shareableCard, { width: cardWidth }]}>
|
||||
<LinearGradient
|
||||
colors={gradients.primary as [string, string]}
|
||||
style={[StyleSheet.absoluteFill, { borderRadius: radius.xl }]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
/>
|
||||
|
||||
{/* Drawing image */}
|
||||
{drawingUri && (
|
||||
<View style={styles.drawingContainer}>
|
||||
<Image
|
||||
source={{ uri: drawingUri }}
|
||||
style={styles.drawingImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Branding footer */}
|
||||
<View style={styles.branding}>
|
||||
<Image source={logo} style={styles.brandingLogo} />
|
||||
<Text style={styles.brandingText}>sauci.app</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ViewShot>
|
||||
</View>
|
||||
|
||||
{/* Share Actions */}
|
||||
<View style={[styles.actionsContainer, { paddingBottom: insets.bottom + spacing.lg }]}>
|
||||
<ShareActionButton
|
||||
icon="logo-instagram"
|
||||
label="Instagram"
|
||||
buttonColors={['#833AB4', '#FD1D1D', '#F77737']}
|
||||
onPress={handleInstagramShare}
|
||||
disabled={isCapturing}
|
||||
/>
|
||||
<ShareActionButton
|
||||
icon="chatbubble-ellipses"
|
||||
label="Messages"
|
||||
buttonColors={['#34C759', '#30D158']}
|
||||
onPress={handleMessagesShare}
|
||||
disabled={isCapturing}
|
||||
/>
|
||||
<ShareActionButton
|
||||
icon="share-outline"
|
||||
label="More"
|
||||
buttonColors={[colors.glass.backgroundLight, colors.glass.backgroundLight]}
|
||||
onPress={handleMoreShare}
|
||||
disabled={isCapturing}
|
||||
isOutline
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function ShareActionButton({
|
||||
icon,
|
||||
label,
|
||||
buttonColors,
|
||||
onPress,
|
||||
disabled,
|
||||
isOutline,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
buttonColors: string[];
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
isOutline?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[
|
||||
styles.actionIconContainer,
|
||||
isOutline && styles.actionIconOutline,
|
||||
]}>
|
||||
{!isOutline && (
|
||||
<LinearGradient
|
||||
colors={buttonColors as [string, string, ...string[]]}
|
||||
style={StyleSheet.absoluteFill}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
/>
|
||||
)}
|
||||
<Ionicons
|
||||
name={icon as any}
|
||||
size={28}
|
||||
color={colors.text}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.actionLabel}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
backdropFallback: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(13, 13, 26, 0.95)',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
closeButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
headerSpacer: {
|
||||
width: 44,
|
||||
},
|
||||
previewContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
shareableCard: {
|
||||
aspectRatio: 0.7,
|
||||
borderRadius: radius.xl,
|
||||
overflow: 'hidden',
|
||||
padding: spacing.md,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
drawingContainer: {
|
||||
flex: 1,
|
||||
borderRadius: radius.lg,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#1a1a2e',
|
||||
},
|
||||
drawingImage: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
},
|
||||
branding: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.sm,
|
||||
paddingTop: spacing.sm,
|
||||
},
|
||||
brandingLogo: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
brandingText: {
|
||||
...typography.subhead,
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
actionsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.xl,
|
||||
paddingTop: spacing.xl,
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
actionButton: {
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
actionIconContainer: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
actionIconOutline: {
|
||||
backgroundColor: colors.glass.backgroundLight,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.glass.border,
|
||||
},
|
||||
actionLabel: {
|
||||
...typography.caption1,
|
||||
color: colors.text,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, TouchableOpacity, Text } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { colors, spacing, radius, typography } from '../../../theme';
|
||||
import { PRESET_COLORS, BRUSH_MIN_WIDTH, BRUSH_MAX_WIDTH } from '../constants';
|
||||
|
||||
interface DrawingToolbarProps {
|
||||
selectedColor: string;
|
||||
brushWidth: number;
|
||||
isEraser: boolean;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
onColorChange: (color: string) => void;
|
||||
onBrushWidthChange: (width: number) => void;
|
||||
onToggleEraser: () => void;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onClear: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export function DrawingToolbar({
|
||||
selectedColor,
|
||||
brushWidth,
|
||||
isEraser,
|
||||
canUndo,
|
||||
canRedo,
|
||||
onColorChange,
|
||||
onBrushWidthChange,
|
||||
onToggleEraser,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onClear,
|
||||
onSave,
|
||||
}: DrawingToolbarProps) {
|
||||
const brushSizes = [BRUSH_MIN_WIDTH, 6, 12, BRUSH_MAX_WIDTH];
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Color palette */}
|
||||
<View style={styles.row}>
|
||||
{PRESET_COLORS.map((color) => (
|
||||
<TouchableOpacity
|
||||
key={color}
|
||||
onPress={() => onColorChange(color)}
|
||||
style={[
|
||||
styles.colorDot,
|
||||
{ backgroundColor: color },
|
||||
selectedColor === color && !isEraser && styles.colorDotSelected,
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Brush sizes */}
|
||||
<View style={styles.row}>
|
||||
{brushSizes.map((size) => (
|
||||
<TouchableOpacity
|
||||
key={size}
|
||||
onPress={() => onBrushWidthChange(size)}
|
||||
style={[styles.sizeButton, brushWidth === size && styles.sizeButtonSelected]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.sizeDot,
|
||||
{
|
||||
width: Math.max(size, 4),
|
||||
height: Math.max(size, 4),
|
||||
borderRadius: Math.max(size, 4) / 2,
|
||||
backgroundColor: isEraser ? colors.textSecondary : selectedColor,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Actions */}
|
||||
<View style={styles.row}>
|
||||
<TouchableOpacity
|
||||
onPress={onToggleEraser}
|
||||
style={[styles.actionButton, isEraser && styles.actionButtonActive]}
|
||||
>
|
||||
<Ionicons
|
||||
name="backspace-outline"
|
||||
size={20}
|
||||
color={isEraser ? colors.background : colors.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={onUndo}
|
||||
disabled={!canUndo}
|
||||
style={[styles.actionButton, !canUndo && styles.actionButtonDisabled]}
|
||||
>
|
||||
<Ionicons name="arrow-undo" size={20} color={canUndo ? colors.text : colors.textTertiary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={onRedo}
|
||||
disabled={!canRedo}
|
||||
style={[styles.actionButton, !canRedo && styles.actionButtonDisabled]}
|
||||
>
|
||||
<Ionicons name="arrow-redo" size={20} color={canRedo ? colors.text : colors.textTertiary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={onClear} style={styles.actionButton}>
|
||||
<Ionicons name="trash-outline" size={20} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={onSave} style={styles.saveButton}>
|
||||
<Ionicons name="share-outline" size={18} color={colors.text} />
|
||||
<Text style={styles.saveText}>Share</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
gap: spacing.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
colorDot: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
colorDotSelected: {
|
||||
borderColor: colors.text,
|
||||
borderWidth: 2,
|
||||
},
|
||||
sizeButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.backgroundLight,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
sizeButtonSelected: {
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: colors.primaryLight,
|
||||
},
|
||||
sizeDot: {
|
||||
// Dynamic size set inline
|
||||
},
|
||||
actionButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.backgroundLight,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
actionButtonActive: {
|
||||
backgroundColor: colors.text,
|
||||
borderColor: colors.text,
|
||||
},
|
||||
actionButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
saveButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.full,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
saveText: {
|
||||
...typography.subhead,
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
export const BRUSH_MIN_WIDTH = 2;
|
||||
export const BRUSH_MAX_WIDTH = 24;
|
||||
export const BRUSH_DEFAULT_WIDTH = 4;
|
||||
|
||||
export const DEFAULT_HUE = 0; // Red
|
||||
export const DEFAULT_COLOR = 'hsl(0, 80%, 60%)';
|
||||
|
||||
export const BROADCAST_BATCH_INTERVAL = 50; // ms
|
||||
export const MAX_STROKES = 500;
|
||||
|
||||
export const CANVAS_BACKGROUND = '#1a1a2e';
|
||||
|
||||
export const PRESET_COLORS = [
|
||||
'#e1306c', // Primary rose
|
||||
'#9b59b6', // Secondary purple
|
||||
'#14B8A6', // Teal
|
||||
'#F97316', // Orange
|
||||
'#2ECC71', // Green
|
||||
'#3498db', // Blue
|
||||
'#F39C12', // Yellow
|
||||
'#ffffff', // White
|
||||
];
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import type { SkImage } from '@shopify/react-native-skia';
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
interface UseCanvasCaptureReturn {
|
||||
makeSnapshot: React.MutableRefObject<(() => SkImage | null) | null>;
|
||||
saveToGallery: () => Promise<boolean>;
|
||||
captureToFile: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
export function useCanvasCapture(): UseCanvasCaptureReturn {
|
||||
const makeSnapshot = useRef<(() => SkImage | null) | null>(null);
|
||||
|
||||
const captureToFile = useCallback(async (): Promise<string | null> => {
|
||||
if (!makeSnapshot.current) return null;
|
||||
|
||||
try {
|
||||
const image = makeSnapshot.current();
|
||||
if (!image) return null;
|
||||
|
||||
const base64 = image.encodeToBase64();
|
||||
if (!base64) return null;
|
||||
|
||||
const fileUri = `${FileSystem.cacheDirectory}livedraw_${Date.now()}.png`;
|
||||
await FileSystem.writeAsStringAsync(fileUri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
|
||||
return fileUri;
|
||||
} catch (err) {
|
||||
console.error('Canvas capture error:', err);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const saveToGallery = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
const fileUri = await captureToFile();
|
||||
if (!fileUri) return false;
|
||||
|
||||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||
if (status !== 'granted') return false;
|
||||
|
||||
await MediaLibrary.saveToLibraryAsync(fileUri);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Canvas capture error:', err);
|
||||
return false;
|
||||
}
|
||||
}, [captureToFile]);
|
||||
|
||||
return { makeSnapshot, saveToGallery, captureToFile };
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import type { StrokeSegment } from '../types';
|
||||
|
||||
interface UseDrawingHistoryReturn {
|
||||
strokes: StrokeSegment[];
|
||||
setStrokes: React.Dispatch<React.SetStateAction<StrokeSegment[]>>;
|
||||
addStroke: (stroke: StrokeSegment) => void;
|
||||
updateStroke: (strokeId: string, points: StrokeSegment['points']) => void;
|
||||
undo: (userId: string) => StrokeSegment | undefined;
|
||||
redo: (userId: string) => StrokeSegment | undefined;
|
||||
canUndo: (userId: string) => boolean;
|
||||
canRedo: (userId: string) => boolean;
|
||||
clearAll: () => void;
|
||||
}
|
||||
|
||||
export function useDrawingHistory(): UseDrawingHistoryReturn {
|
||||
const [strokes, setStrokes] = useState<StrokeSegment[]>([]);
|
||||
const redoStackRef = useRef<StrokeSegment[]>([]);
|
||||
|
||||
const addStroke = useCallback((stroke: StrokeSegment) => {
|
||||
setStrokes((prev) => [...prev, stroke]);
|
||||
// Clear redo stack when new stroke is added
|
||||
redoStackRef.current = [];
|
||||
}, []);
|
||||
|
||||
const updateStroke = useCallback(
|
||||
(strokeId: string, points: StrokeSegment['points']) => {
|
||||
setStrokes((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === strokeId ? { ...s, points: [...s.points, ...points] } : s
|
||||
)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const undo = useCallback(
|
||||
(userId: string): StrokeSegment | undefined => {
|
||||
let removed: StrokeSegment | undefined;
|
||||
setStrokes((prev) => {
|
||||
// Find last stroke by this user
|
||||
const idx = [...prev].reverse().findIndex((s) => s.userId === userId);
|
||||
if (idx === -1) return prev;
|
||||
const actualIdx = prev.length - 1 - idx;
|
||||
removed = prev[actualIdx];
|
||||
redoStackRef.current.push(removed);
|
||||
return [...prev.slice(0, actualIdx), ...prev.slice(actualIdx + 1)];
|
||||
});
|
||||
return removed;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const redo = useCallback(
|
||||
(userId: string): StrokeSegment | undefined => {
|
||||
// Find last redo entry for this user
|
||||
const idx = [...redoStackRef.current]
|
||||
.reverse()
|
||||
.findIndex((s) => s.userId === userId);
|
||||
if (idx === -1) return undefined;
|
||||
const actualIdx = redoStackRef.current.length - 1 - idx;
|
||||
const stroke = redoStackRef.current[actualIdx];
|
||||
redoStackRef.current.splice(actualIdx, 1);
|
||||
setStrokes((prev) => [...prev, stroke]);
|
||||
return stroke;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const canUndo = useCallback(
|
||||
(userId: string) => strokes.some((s) => s.userId === userId),
|
||||
[strokes]
|
||||
);
|
||||
|
||||
const canRedo = useCallback(
|
||||
(userId: string) =>
|
||||
redoStackRef.current.some((s) => s.userId === userId),
|
||||
[]
|
||||
);
|
||||
|
||||
const clearAll = useCallback(() => {
|
||||
setStrokes([]);
|
||||
redoStackRef.current = [];
|
||||
}, []);
|
||||
|
||||
return {
|
||||
strokes,
|
||||
setStrokes,
|
||||
addStroke,
|
||||
updateStroke,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
clearAll,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { RealtimeChannel } from '@supabase/supabase-js';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import type { StrokeSegment, StrokePoint, LiveDrawEvent } from '../types';
|
||||
import { BROADCAST_BATCH_INTERVAL } from '../constants';
|
||||
import { updateWidget } from '../utils/widgetBridge';
|
||||
|
||||
interface UseDrawingSyncConfig {
|
||||
coupleId: string;
|
||||
userId: string | undefined;
|
||||
onStrokeStart: (stroke: StrokeSegment) => void;
|
||||
onStrokeContinue: (strokeId: string, points: StrokePoint[]) => void;
|
||||
onStrokeEnd: (strokeId: string) => void;
|
||||
onClearCanvas: () => void;
|
||||
onUndo: (strokeId: string) => void;
|
||||
onRedo: (stroke: StrokeSegment) => void;
|
||||
onLoadStrokes: (strokes: StrokeSegment[]) => void;
|
||||
}
|
||||
|
||||
interface UseDrawingSyncReturn {
|
||||
broadcastStrokeStart: (stroke: StrokeSegment) => void;
|
||||
broadcastStrokeContinue: (strokeId: string, points: StrokePoint[]) => void;
|
||||
broadcastStrokeEnd: (strokeId: string) => void;
|
||||
broadcastClearCanvas: () => void;
|
||||
broadcastUndo: (strokeId: string) => void;
|
||||
broadcastRedo: (stroke: StrokeSegment) => void;
|
||||
persistStrokes: (strokes: StrokeSegment[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useDrawingSync(config: UseDrawingSyncConfig): UseDrawingSyncReturn {
|
||||
const {
|
||||
coupleId,
|
||||
userId,
|
||||
onStrokeStart,
|
||||
onStrokeContinue,
|
||||
onStrokeEnd,
|
||||
onClearCanvas,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onLoadStrokes,
|
||||
} = config;
|
||||
|
||||
const channelRef = useRef<RealtimeChannel | null>(null);
|
||||
const batchRef = useRef<{ strokeId: string; points: StrokePoint[] } | null>(null);
|
||||
const batchTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Load existing strokes from DB on mount
|
||||
useEffect(() => {
|
||||
if (!coupleId) return;
|
||||
|
||||
// Clear any stale strokes from a previous couple before loading
|
||||
onLoadStrokes([]);
|
||||
|
||||
const loadExisting = async () => {
|
||||
const { data } = await supabase
|
||||
.from('live_draw_sessions')
|
||||
.select('strokes')
|
||||
.eq('couple_id', coupleId)
|
||||
.maybeSingle();
|
||||
|
||||
if (data?.strokes) {
|
||||
onLoadStrokes(data.strokes as StrokeSegment[]);
|
||||
}
|
||||
};
|
||||
|
||||
loadExisting();
|
||||
}, [coupleId]);
|
||||
|
||||
// Subscribe to broadcast channel
|
||||
useEffect(() => {
|
||||
if (!coupleId || !userId) return;
|
||||
|
||||
const channel = supabase.channel(`livedraw:${coupleId}`);
|
||||
channelRef.current = channel;
|
||||
|
||||
channel.on('broadcast', { event: 'draw' }, (payload) => {
|
||||
const event = payload.payload as LiveDrawEvent;
|
||||
|
||||
// Ignore own events
|
||||
if ('stroke' in event && event.stroke?.userId === userId) return;
|
||||
if ('userId' in event && event.userId === userId) return;
|
||||
|
||||
switch (event.type) {
|
||||
case 'stroke_start':
|
||||
onStrokeStart(event.stroke);
|
||||
break;
|
||||
case 'stroke_continue':
|
||||
onStrokeContinue(event.strokeId, event.points);
|
||||
break;
|
||||
case 'stroke_end':
|
||||
onStrokeEnd(event.strokeId);
|
||||
break;
|
||||
case 'clear_canvas':
|
||||
onClearCanvas();
|
||||
break;
|
||||
case 'undo':
|
||||
onUndo(event.strokeId);
|
||||
break;
|
||||
case 'redo':
|
||||
// For redo, we'd need the full stroke - handled via DB sync
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
channel.subscribe();
|
||||
|
||||
return () => {
|
||||
channelRef.current = null;
|
||||
supabase.removeChannel(channel);
|
||||
if (batchTimerRef.current) {
|
||||
clearInterval(batchTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [coupleId, userId]);
|
||||
|
||||
const send = useCallback(
|
||||
(event: LiveDrawEvent) => {
|
||||
channelRef.current?.send({
|
||||
type: 'broadcast',
|
||||
event: 'draw',
|
||||
payload: event,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const broadcastStrokeStart = useCallback(
|
||||
(stroke: StrokeSegment) => {
|
||||
send({ type: 'stroke_start', stroke });
|
||||
},
|
||||
[send]
|
||||
);
|
||||
|
||||
const broadcastStrokeContinue = useCallback(
|
||||
(strokeId: string, points: StrokePoint[]) => {
|
||||
// Batch points to reduce traffic
|
||||
if (!batchRef.current || batchRef.current.strokeId !== strokeId) {
|
||||
batchRef.current = { strokeId, points: [...points] };
|
||||
} else {
|
||||
batchRef.current.points.push(...points);
|
||||
}
|
||||
|
||||
if (!batchTimerRef.current) {
|
||||
batchTimerRef.current = setInterval(() => {
|
||||
if (batchRef.current && batchRef.current.points.length > 0) {
|
||||
send({
|
||||
type: 'stroke_continue',
|
||||
strokeId: batchRef.current.strokeId,
|
||||
points: batchRef.current.points,
|
||||
});
|
||||
batchRef.current.points = [];
|
||||
}
|
||||
}, BROADCAST_BATCH_INTERVAL);
|
||||
}
|
||||
},
|
||||
[send]
|
||||
);
|
||||
|
||||
const broadcastStrokeEnd = useCallback(
|
||||
(strokeId: string) => {
|
||||
// Flush remaining batch
|
||||
if (batchRef.current && batchRef.current.points.length > 0) {
|
||||
send({
|
||||
type: 'stroke_continue',
|
||||
strokeId: batchRef.current.strokeId,
|
||||
points: batchRef.current.points,
|
||||
});
|
||||
}
|
||||
batchRef.current = null;
|
||||
if (batchTimerRef.current) {
|
||||
clearInterval(batchTimerRef.current);
|
||||
batchTimerRef.current = null;
|
||||
}
|
||||
send({ type: 'stroke_end', strokeId });
|
||||
},
|
||||
[send]
|
||||
);
|
||||
|
||||
const broadcastClearCanvas = useCallback(() => {
|
||||
if (!userId) return;
|
||||
send({ type: 'clear_canvas', userId });
|
||||
}, [send, userId]);
|
||||
|
||||
const broadcastUndo = useCallback(
|
||||
(strokeId: string) => {
|
||||
if (!userId) return;
|
||||
send({ type: 'undo', userId, strokeId });
|
||||
},
|
||||
[send, userId]
|
||||
);
|
||||
|
||||
const broadcastRedo = useCallback(
|
||||
(stroke: StrokeSegment) => {
|
||||
// Send full stroke so partner can restore it
|
||||
send({ type: 'stroke_start', stroke });
|
||||
},
|
||||
[send]
|
||||
);
|
||||
|
||||
const persistStrokes = useCallback(
|
||||
async (strokes: StrokeSegment[]) => {
|
||||
await supabase
|
||||
.from('live_draw_sessions')
|
||||
.upsert(
|
||||
{
|
||||
couple_id: coupleId,
|
||||
strokes: strokes as any,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
{ onConflict: 'couple_id' }
|
||||
);
|
||||
|
||||
// Update home screen widget with latest drawing
|
||||
updateWidget(strokes);
|
||||
},
|
||||
[coupleId]
|
||||
);
|
||||
|
||||
return {
|
||||
broadcastStrokeStart,
|
||||
broadcastStrokeContinue,
|
||||
broadcastStrokeEnd,
|
||||
broadcastClearCanvas,
|
||||
broadcastUndo,
|
||||
broadcastRedo,
|
||||
persistStrokes,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { LiveDrawScreen } from './LiveDrawScreen';
|
||||
@@ -0,0 +1,24 @@
|
||||
// Normalized coordinates (0-1) for cross-device compatibility
|
||||
export interface StrokePoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface StrokeSegment {
|
||||
id: string;
|
||||
userId: string;
|
||||
points: StrokePoint[];
|
||||
color: string;
|
||||
width: number;
|
||||
timestamp: number;
|
||||
isEraser: boolean;
|
||||
}
|
||||
|
||||
// Broadcast events (real-time when both online)
|
||||
export type LiveDrawEvent =
|
||||
| { type: 'stroke_start'; stroke: StrokeSegment }
|
||||
| { type: 'stroke_continue'; strokeId: string; points: StrokePoint[] }
|
||||
| { type: 'stroke_end'; strokeId: string }
|
||||
| { type: 'clear_canvas'; userId: string }
|
||||
| { type: 'undo'; userId: string; strokeId: string }
|
||||
| { type: 'redo'; userId: string; strokeId: string };
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Platform } from 'react-native';
|
||||
import { setWidgetData, reloadWidgets } from '../../../../modules/widget-bridge';
|
||||
import type { StrokeSegment } from '../types';
|
||||
import { renderStrokesToBase64 } from './widgetImageExport';
|
||||
|
||||
export async function updateWidget(strokes: StrokeSegment[]): Promise<void> {
|
||||
try {
|
||||
const base64 = renderStrokesToBase64(strokes);
|
||||
if (!base64) return;
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
setWidgetData('LiveDrawImage', base64);
|
||||
reloadWidgets();
|
||||
} else if (Platform.OS === 'android') {
|
||||
setWidgetData('LiveDrawImage', base64);
|
||||
reloadWidgets();
|
||||
}
|
||||
} catch (e) {
|
||||
// Widget update is non-critical; don't crash the app
|
||||
console.warn('Failed to update widget:', e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Skia, StrokeCap, StrokeJoin, PaintStyle } from '@shopify/react-native-skia';
|
||||
import type { StrokeSegment, StrokePoint } from '../types';
|
||||
import { CANVAS_BACKGROUND } from '../constants';
|
||||
|
||||
const WIDGET_WIDTH = 300;
|
||||
const WIDGET_HEIGHT = 250;
|
||||
|
||||
function buildPath(points: StrokePoint[], width: number, height: number) {
|
||||
const path = Skia.Path.Make();
|
||||
if (points.length === 0) return path;
|
||||
|
||||
const first = points[0];
|
||||
path.moveTo(first.x * width, first.y * height);
|
||||
|
||||
if (points.length === 1) {
|
||||
path.lineTo(first.x * width + 0.1, first.y * height + 0.1);
|
||||
return path;
|
||||
}
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const curr = points[i];
|
||||
const midX = ((prev.x + curr.x) / 2) * width;
|
||||
const midY = ((prev.y + curr.y) / 2) * height;
|
||||
|
||||
if (i === 1) {
|
||||
path.lineTo(midX, midY);
|
||||
} else {
|
||||
path.quadTo(prev.x * width, prev.y * height, midX, midY);
|
||||
}
|
||||
}
|
||||
|
||||
const last = points[points.length - 1];
|
||||
path.lineTo(last.x * width, last.y * height);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export function renderStrokesToBase64(strokes: StrokeSegment[]): string | null {
|
||||
const surface = Skia.Surface.Make(WIDGET_WIDTH, WIDGET_HEIGHT);
|
||||
if (!surface) return null;
|
||||
|
||||
const canvas = surface.getCanvas();
|
||||
|
||||
// Draw background
|
||||
const bgPaint = Skia.Paint();
|
||||
bgPaint.setColor(Skia.Color(CANVAS_BACKGROUND));
|
||||
canvas.drawRect(
|
||||
{ x: 0, y: 0, width: WIDGET_WIDTH, height: WIDGET_HEIGHT },
|
||||
bgPaint,
|
||||
);
|
||||
|
||||
// Draw strokes
|
||||
for (const stroke of strokes) {
|
||||
const path = buildPath(stroke.points, WIDGET_WIDTH, WIDGET_HEIGHT);
|
||||
const paint = Skia.Paint();
|
||||
paint.setColor(
|
||||
Skia.Color(stroke.isEraser ? CANVAS_BACKGROUND : stroke.color),
|
||||
);
|
||||
paint.setStyle(PaintStyle.Stroke);
|
||||
paint.setStrokeWidth(stroke.width);
|
||||
paint.setStrokeCap(StrokeCap.Round);
|
||||
paint.setStrokeJoin(StrokeJoin.Round);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
surface.flush();
|
||||
const image = surface.makeImageSnapshot();
|
||||
const data = image.encodeToBase64();
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { FlexWidget, ImageWidget, TextWidget } from 'react-native-android-widget';
|
||||
|
||||
interface LiveDrawWidgetProps {
|
||||
imageBase64?: string;
|
||||
}
|
||||
|
||||
export function LiveDrawWidget({ imageBase64 }: LiveDrawWidgetProps) {
|
||||
if (imageBase64) {
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
width: 'match_parent',
|
||||
height: 'match_parent',
|
||||
backgroundColor: '#1a1a2e',
|
||||
padding: 8,
|
||||
borderRadius: 16,
|
||||
}}
|
||||
clickAction="OPEN_APP"
|
||||
clickActionData={{ uri: 'app.sauci://live-draw' }}
|
||||
>
|
||||
<ImageWidget
|
||||
image={`data:image/png;base64,${imageBase64}`}
|
||||
imageWidth={284}
|
||||
imageHeight={94}
|
||||
style={{
|
||||
width: 'match_parent',
|
||||
height: 'match_parent',
|
||||
borderRadius: 12,
|
||||
}}
|
||||
/>
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
width: 'match_parent',
|
||||
height: 'match_parent',
|
||||
backgroundColor: '#1a1a2e',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 16,
|
||||
}}
|
||||
clickAction="OPEN_APP"
|
||||
clickActionData={{ uri: 'app.sauci://live-draw' }}
|
||||
>
|
||||
<TextWidget
|
||||
text="✏️"
|
||||
style={{ fontSize: 32 }}
|
||||
/>
|
||||
<TextWidget
|
||||
text="Draw together"
|
||||
style={{ fontSize: 12, color: '#80FFFFFF', marginTop: 8 }}
|
||||
/>
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import type { WidgetTaskHandlerProps } from 'react-native-android-widget';
|
||||
import { LiveDrawWidget } from './LiveDrawWidget';
|
||||
|
||||
const WIDGET_NAME = 'LiveDraw';
|
||||
|
||||
export async function widgetTaskHandler(props: WidgetTaskHandlerProps) {
|
||||
const widgetInfo = props.widgetInfo;
|
||||
const widgetAction = props.widgetAction;
|
||||
|
||||
if (widgetInfo.widgetName !== WIDGET_NAME) return;
|
||||
|
||||
switch (widgetAction) {
|
||||
case 'WIDGET_ADDED':
|
||||
case 'WIDGET_UPDATE':
|
||||
case 'WIDGET_RESIZED': {
|
||||
// Read stored image from SharedPreferences via the widget bridge
|
||||
// The image is stored by the app when strokes are persisted
|
||||
props.renderWidget(<LiveDrawWidget />);
|
||||
break;
|
||||
}
|
||||
case 'WIDGET_CLICK':
|
||||
// Click handling is done via clickAction="OPEN_APP" in the widget
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,83 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct LiveDrawEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let image: UIImage?
|
||||
}
|
||||
|
||||
struct LiveDrawProvider: TimelineProvider {
|
||||
let suiteName = "group.com.sauci.app"
|
||||
let dataKey = "LiveDrawImage"
|
||||
|
||||
func placeholder(in context: Context) -> LiveDrawEntry {
|
||||
LiveDrawEntry(date: Date(), image: nil)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (LiveDrawEntry) -> Void) {
|
||||
completion(getEntry())
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<LiveDrawEntry>) -> Void) {
|
||||
let entry = getEntry()
|
||||
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
|
||||
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
private func getEntry() -> LiveDrawEntry {
|
||||
guard let suite = UserDefaults(suiteName: suiteName),
|
||||
let base64 = suite.string(forKey: dataKey),
|
||||
let imageData = Data(base64Encoded: base64),
|
||||
let image = UIImage(data: imageData)
|
||||
else {
|
||||
return LiveDrawEntry(date: Date(), image: nil)
|
||||
}
|
||||
return LiveDrawEntry(date: Date(), image: image)
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveDrawWidgetEntryView: View {
|
||||
var entry: LiveDrawProvider.Entry
|
||||
|
||||
var body: some View {
|
||||
if let image = entry.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
} else {
|
||||
ZStack {
|
||||
Color(red: 26/255, green: 26/255, blue: 46/255)
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "pencil.tip.crop.circle")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
Text("Draw together")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveDrawWidget: Widget {
|
||||
let kind: String = "LiveDrawWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: LiveDrawProvider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
LiveDrawWidgetEntryView(entry: entry)
|
||||
.widgetURL(URL(string: "app.sauci://live-draw"))
|
||||
.containerBackground(Color(red: 26/255, green: 26/255, blue: 46/255), for: .widget)
|
||||
} else {
|
||||
LiveDrawWidgetEntryView(entry: entry)
|
||||
.widgetURL(URL(string: "app.sauci://live-draw"))
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("Live Draw")
|
||||
.description("See your shared drawing with your partner.")
|
||||
.supportedFamilies([.systemSmall, .systemMedium])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/** @type {import('@bacons/apple-targets').ConfigFunction} */
|
||||
module.exports = (config) => ({
|
||||
type: "widget",
|
||||
name: "LiveDrawWidget",
|
||||
deploymentTarget: "16.0",
|
||||
entitlements: {
|
||||
"com.apple.security.application-groups":
|
||||
config.ios?.entitlements?.["com.apple.security.application-groups"] ?? [
|
||||
"group.com.sauci.app",
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.sauci.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct exportWidgets: WidgetBundle {
|
||||
var body: some Widget {
|
||||
LiveDrawWidget()
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,12 @@ Deno.serve(async (req) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete live draw sessions for this couple
|
||||
await supabase
|
||||
.from("live_draw_sessions")
|
||||
.delete()
|
||||
.eq("couple_id", coupleId);
|
||||
|
||||
// Clear couple_id from both partners' profiles first
|
||||
const { error: updateError } = await supabase
|
||||
.from("profiles")
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Live Draw Sessions: persists canvas state for async collaborative drawing
|
||||
-- One session per couple - partners draw on a shared canvas throughout the day
|
||||
CREATE TABLE IF NOT EXISTS live_draw_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
couple_id UUID NOT NULL REFERENCES couples(id) ON DELETE CASCADE,
|
||||
strokes JSONB NOT NULL DEFAULT '[]',
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(couple_id)
|
||||
);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE live_draw_sessions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Enable realtime for live updates
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE live_draw_sessions;
|
||||
|
||||
-- RLS: Only couple members can read their drawing
|
||||
CREATE POLICY "Users can view own couple drawings"
|
||||
ON live_draw_sessions FOR SELECT
|
||||
USING (couple_id = get_auth_user_couple_id());
|
||||
|
||||
-- RLS: Only couple members can insert/update/delete their drawing
|
||||
CREATE POLICY "Users can manage own couple drawings"
|
||||
ON live_draw_sessions FOR ALL
|
||||
USING (couple_id = get_auth_user_couple_id());
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_live_draw_sessions_couple_id ON live_draw_sessions(couple_id);
|
||||
Reference in new issue
Block a user