mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10: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,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
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
File renamed without changes.
@@ -1,522 +0,0 @@
|
||||
# Frontend-Backend Communication Optimization Plan
|
||||
|
||||
This document outlines identified chattiness issues in the Sauci mobile app and provides a prioritized implementation plan for optimizations.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Estimated Extra Traffic:** ~40-50% of current API calls are redundant or suboptimal
|
||||
|
||||
**Key Problem Areas:**
|
||||
- Partner profile refreshed on every message send
|
||||
- Match details fetched from DB instead of using cached store data
|
||||
- Redundant polling alongside real-time subscriptions
|
||||
- N+1 query patterns in match/response fetching
|
||||
- Missing caching strategies across stores
|
||||
|
||||
---
|
||||
|
||||
## High Priority Tasks
|
||||
|
||||
### Task 1: Cache Partner Public Key with TTL
|
||||
|
||||
**Impact:** Eliminates 1 database query per message sent
|
||||
|
||||
**Current Behavior:**
|
||||
- `useEncryptedSend.ts:84-85` calls `refreshPartner()` on every text message
|
||||
- `useMediaUpload.ts:80-81` calls `refreshPartner()` on every media upload
|
||||
- No caching - always hits database
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/src/store/authStore.ts`
|
||||
- `apps/mobile/src/hooks/useEncryptedSend.ts`
|
||||
- `apps/mobile/src/features/chat/hooks/useMediaUpload.ts`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// In authStore.ts - add to state
|
||||
partnerLastFetched: number | null;
|
||||
|
||||
// In refreshPartner action
|
||||
refreshPartner: async () => {
|
||||
const now = Date.now();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
const { partner, partnerLastFetched } = get();
|
||||
|
||||
// Return early if cache is fresh and partner has public key
|
||||
if (partner?.public_key_jwk &&
|
||||
partnerLastFetched &&
|
||||
now - partnerLastFetched < CACHE_TTL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ... existing fetch logic
|
||||
set({ partner: data, partnerLastFetched: now });
|
||||
}
|
||||
```
|
||||
|
||||
**In hooks - add force refresh option:**
|
||||
```typescript
|
||||
// Only force refresh if encryption fails due to key mismatch
|
||||
const sendEncrypted = async (content: string, forceKeyRefresh = false) => {
|
||||
if (forceKeyRefresh) {
|
||||
await refreshPartner();
|
||||
}
|
||||
// ... rest of logic
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Partner profile cached for 5 minutes
|
||||
- [ ] Cache bypassed if `public_key_jwk` is null
|
||||
- [ ] Force refresh available for encryption failures
|
||||
- [ ] No regression in E2EE functionality
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Use Store Data for Message Notifications
|
||||
|
||||
**Impact:** Eliminates 1 database query per incoming message
|
||||
|
||||
**Current Behavior:**
|
||||
- `_layout.tsx:462-466` fetches match details from DB for every incoming message
|
||||
- Match data already exists in `matchStore`
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/app/(app)/_layout.tsx`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Replace DB query with store lookup
|
||||
const handleNewMessage = async (newMessage: Message) => {
|
||||
// Try to get match from store first
|
||||
const { matches } = useMatchStore.getState();
|
||||
const existingMatch = matches.find(m => m.id === newMessage.match_id);
|
||||
|
||||
if (existingMatch) {
|
||||
// Use cached data
|
||||
showNotificationToast(existingMatch.question.text, newMessage);
|
||||
} else {
|
||||
// Fallback: fetch from DB only if not in store (rare edge case)
|
||||
const { data: match } = await supabase
|
||||
.from("matches")
|
||||
.select("id, question:questions(text)")
|
||||
.eq("id", newMessage.match_id)
|
||||
.single();
|
||||
|
||||
if (match) {
|
||||
showNotificationToast(match.question.text, newMessage);
|
||||
// Also refresh matches store since we're missing data
|
||||
fetchMatches();
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Message notifications use store data when available
|
||||
- [ ] Fallback to DB query only when match not in store
|
||||
- [ ] Store refreshed if match is missing (data consistency)
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Remove Redundant Polling in Pairing Screen
|
||||
|
||||
**Impact:** Eliminates polling every 5 seconds when waiting for partner
|
||||
|
||||
**Current Behavior:**
|
||||
- `pairing.tsx:39-45` polls `fetchCouple()` every 5 seconds
|
||||
- `pairing.tsx:47-71` also subscribes to real-time profile updates
|
||||
- Both doing the same job - redundant
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/app/(app)/pairing.tsx`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// DELETE the polling interval (lines 39-45)
|
||||
// KEEP only the real-time subscription
|
||||
|
||||
useEffect(() => {
|
||||
if (!couple || partner) return;
|
||||
|
||||
// Real-time subscription handles partner joining
|
||||
const subscription = supabase
|
||||
.channel(`couple-${couple.id}`)
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{
|
||||
event: "UPDATE",
|
||||
schema: "public",
|
||||
table: "profiles",
|
||||
filter: `couple_id=eq.${couple.id}`,
|
||||
},
|
||||
(payload) => {
|
||||
if (payload.new.id !== user?.id) {
|
||||
fetchCouple(); // Partner joined
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [couple, partner, user?.id, fetchCouple]);
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Polling removed
|
||||
- [ ] Real-time subscription still detects partner joining
|
||||
- [ ] No increase in time to detect partner
|
||||
|
||||
---
|
||||
|
||||
## Medium Priority Tasks
|
||||
|
||||
### Task 4: Optimize Match Responses Query (N+1 Fix)
|
||||
|
||||
**Impact:** Reduces 2 queries to 1, eliminates client-side filtering
|
||||
|
||||
**Current Behavior:**
|
||||
- `matchStore.ts:23-29` fetches all matches
|
||||
- `matchStore.ts:35-38` fetches ALL responses separately
|
||||
- Client-side filtering to match responses to questions
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/src/store/matchStore.ts`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Option A: Join responses in single query
|
||||
const fetchMatches = async () => {
|
||||
const { data: matches } = await supabase
|
||||
.from("matches")
|
||||
.select(`
|
||||
*,
|
||||
question:questions(*),
|
||||
user_response:responses!inner(
|
||||
id, answer, user_id
|
||||
)
|
||||
`)
|
||||
.eq("couple_id", coupleId)
|
||||
.in("responses.user_id", [userId, partnerId])
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
// Process matches with embedded responses
|
||||
const processedMatches = matches?.map(match => ({
|
||||
...match,
|
||||
userAnswer: match.user_response.find(r => r.user_id === userId)?.answer,
|
||||
partnerAnswer: match.user_response.find(r => r.user_id === partnerId)?.answer,
|
||||
}));
|
||||
|
||||
set({ matches: processedMatches });
|
||||
};
|
||||
```
|
||||
|
||||
**Alternative - Database View:**
|
||||
```sql
|
||||
-- Create view that joins match with responses
|
||||
CREATE VIEW match_with_responses AS
|
||||
SELECT
|
||||
m.*,
|
||||
q.text as question_text,
|
||||
q.intensity,
|
||||
ur.answer as user_answer,
|
||||
pr.answer as partner_answer
|
||||
FROM matches m
|
||||
JOIN questions q ON m.question_id = q.id
|
||||
LEFT JOIN responses ur ON m.question_id = ur.question_id AND ur.user_id = m.user1_id
|
||||
LEFT JOIN responses pr ON m.question_id = pr.question_id AND pr.user_id = m.user2_id;
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Single query fetches matches with responses
|
||||
- [ ] No client-side filtering of responses
|
||||
- [ ] Match list still displays correctly
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Fix Mark All As Seen Trigger
|
||||
|
||||
**Impact:** Eliminates unnecessary DB updates on every render
|
||||
|
||||
**Current Behavior:**
|
||||
- `matches.tsx:66-69` calls `markAllAsSeen()` whenever `matches.length` changes
|
||||
- Triggers even when navigating back to screen with same matches
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/app/(app)/matches.tsx`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Move markAllAsSeen to useFocusEffect, run once per focus
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchMatches();
|
||||
|
||||
// Mark as seen after fetch completes
|
||||
return () => {
|
||||
// Optionally mark as seen on blur instead
|
||||
};
|
||||
}, [])
|
||||
);
|
||||
|
||||
// Separate effect for marking seen - runs once after initial fetch
|
||||
const [hasMarkedSeen, setHasMarkedSeen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (matches.length > 0 && !hasMarkedSeen) {
|
||||
markAllAsSeen();
|
||||
setHasMarkedSeen(true);
|
||||
}
|
||||
}, [matches.length, hasMarkedSeen]);
|
||||
|
||||
// Reset on screen blur
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
return () => setHasMarkedSeen(false);
|
||||
}, [])
|
||||
);
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] `markAllAsSeen()` only called once per screen visit
|
||||
- [ ] New matches still get marked as seen
|
||||
- [ ] No regression in notification badge behavior
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Batch Message Status Updates
|
||||
|
||||
**Impact:** Reduces 2 queries to 1 when opening chat
|
||||
|
||||
**Current Behavior:**
|
||||
- `useMessageSubscription.ts:125-143` makes separate updates for:
|
||||
1. Messages to mark as read
|
||||
2. Messages to mark as delivered
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/src/hooks/useMessageSubscription.ts`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Combine into single update operation
|
||||
const markMessagesAsReadAndDelivered = async (messages: Message[]) => {
|
||||
const now = new Date().toISOString();
|
||||
const userId = useAuthStore.getState().user?.id;
|
||||
|
||||
// Find messages that need updating
|
||||
const messagesToUpdate = messages.filter(
|
||||
m => m.user_id !== userId && (!m.read_at || !m.delivered_at)
|
||||
);
|
||||
|
||||
if (messagesToUpdate.length === 0) return;
|
||||
|
||||
const messageIds = messagesToUpdate.map(m => m.id);
|
||||
|
||||
// Single update query
|
||||
await supabase
|
||||
.from("messages")
|
||||
.update({
|
||||
delivered_at: now,
|
||||
read_at: now
|
||||
})
|
||||
.in("id", messageIds)
|
||||
.is("read_at", null); // Only update unread
|
||||
};
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Single query updates both delivered_at and read_at
|
||||
- [ ] Read receipts still work correctly
|
||||
- [ ] No regression in message status display
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Debounce Answer Gap Check
|
||||
|
||||
**Impact:** Reduces RPC calls during rapid swiping
|
||||
|
||||
**Current Behavior:**
|
||||
- `swipe.tsx:171` checks answer gap on mount
|
||||
- `swipe.tsx:228` checks again after EVERY swipe response
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/app/(app)/swipe.tsx`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Add debounced version
|
||||
const debouncedCheckAnswerGap = useMemo(
|
||||
() => debounce(checkAnswerGap, 2000), // 2 second debounce
|
||||
[checkAnswerGap]
|
||||
);
|
||||
|
||||
// On response submit
|
||||
const handleResponse = async (answer: Answer) => {
|
||||
await submitResponse(questionId, answer);
|
||||
|
||||
// Debounced check - won't fire if user swipes again within 2s
|
||||
debouncedCheckAnswerGap();
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => debouncedCheckAnswerGap.cancel();
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Answer gap only checked after swiping pauses
|
||||
- [ ] Still checks on initial mount
|
||||
- [ ] Gap modal still appears when threshold exceeded
|
||||
|
||||
---
|
||||
|
||||
## Low Priority Tasks
|
||||
|
||||
### Task 8: Add Stale Time to Packs Store
|
||||
|
||||
**Impact:** Prevents unnecessary refetches of static data
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/src/store/packsStore.ts`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Add to store state
|
||||
packsLastFetched: number | null;
|
||||
|
||||
// In fetchPacks
|
||||
fetchPacks: async () => {
|
||||
const STALE_TIME = 10 * 60 * 1000; // 10 minutes
|
||||
const { packsLastFetched, packs } = get();
|
||||
|
||||
if (packs.length > 0 &&
|
||||
packsLastFetched &&
|
||||
Date.now() - packsLastFetched < STALE_TIME) {
|
||||
return; // Use cached data
|
||||
}
|
||||
|
||||
// ... existing fetch logic
|
||||
set({ packs: data, packsLastFetched: Date.now() });
|
||||
};
|
||||
|
||||
// Add force refresh for when user explicitly requests
|
||||
fetchPacksForce: async () => {
|
||||
set({ packsLastFetched: null });
|
||||
return get().fetchPacks();
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Persist Matches to AsyncStorage
|
||||
|
||||
**Impact:** Faster initial load, offline capability
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/src/store/matchStore.ts`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export const useMatchStore = create<MatchStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// ... existing store
|
||||
}),
|
||||
{
|
||||
name: "match-storage",
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
partialize: (state) => ({
|
||||
matches: state.matches,
|
||||
// Don't persist loading states
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Combine Couple + Partner Fetch
|
||||
|
||||
**Impact:** Reduces 2 queries to 1 in fetchCouple
|
||||
|
||||
**Current Behavior:**
|
||||
- `authStore.ts:137-149` fetches couple, then partner separately
|
||||
|
||||
**Files to Modify:**
|
||||
- `apps/mobile/src/store/authStore.ts`
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Single query with join
|
||||
const { data: coupleData } = await supabase
|
||||
.from("couples")
|
||||
.select(`
|
||||
*,
|
||||
profiles!inner(*)
|
||||
`)
|
||||
.eq("id", user.couple_id)
|
||||
.single();
|
||||
|
||||
const partner = coupleData.profiles.find(p => p.id !== user.id);
|
||||
set({ couple: coupleData, partner });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
| Priority | Task | Effort | Impact |
|
||||
|----------|------|--------|--------|
|
||||
| 1 | Task 1: Cache partner key | Low | High |
|
||||
| 2 | Task 2: Store-based notifications | Low | High |
|
||||
| 3 | Task 3: Remove polling | Low | Medium |
|
||||
| 4 | Task 5: Fix mark as seen | Low | Medium |
|
||||
| 5 | Task 6: Batch message updates | Low | Low |
|
||||
| 6 | Task 7: Debounce gap check | Low | Low |
|
||||
| 7 | Task 4: N+1 fix | Medium | Medium |
|
||||
| 8 | Task 8: Packs stale time | Low | Low |
|
||||
| 9 | Task 10: Combine couple fetch | Low | Low |
|
||||
| 10 | Task 9: Persist matches | Medium | Low |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After implementing optimizations:
|
||||
|
||||
- [ ] E2EE messaging still works (send/receive encrypted messages)
|
||||
- [ ] Partner joining couple is detected in real-time
|
||||
- [ ] Match notifications display correctly
|
||||
- [ ] New matches appear and are marked as seen
|
||||
- [ ] Message read receipts work
|
||||
- [ ] Answer gap modal appears when appropriate
|
||||
- [ ] Packs load and display correctly
|
||||
- [ ] No increase in error rates (monitor logs)
|
||||
|
||||
---
|
||||
|
||||
## Metrics to Track
|
||||
|
||||
Before/after comparison:
|
||||
|
||||
1. **API calls per session** - Target: 40% reduction
|
||||
2. **Time to interactive on home screen** - Target: <500ms
|
||||
3. **Message send latency** - Target: No increase
|
||||
4. **Battery usage** - Target: 20% reduction in background activity
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All caching should have invalidation strategies
|
||||
- Consider adding React Query or TanStack Query for more sophisticated caching
|
||||
- Real-time subscriptions are preferred over polling
|
||||
- Database views can simplify complex queries but add maintenance overhead
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,427 +0,0 @@
|
||||
# Plan: Remove E2EE and Switch to Server-Side Security
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the plan to remove end-to-end encryption (E2EE) from Sauci's chat system and rely on Supabase's built-in security instead.
|
||||
|
||||
### Current State
|
||||
|
||||
- Messages are encrypted client-side using AES-256-GCM with triple-wrapped RSA keys
|
||||
- Each message has sender, recipient, and admin wrapped keys
|
||||
- Complex key management: generation, storage, rotation, recovery
|
||||
- ~2500+ lines of encryption code across 21 files
|
||||
- Edge functions handle key rotation and pending message re-encryption
|
||||
|
||||
### Target State
|
||||
|
||||
- Messages stored as plaintext in Supabase (protected by RLS policies)
|
||||
- TLS encryption in transit (Supabase default)
|
||||
- Encryption at rest (Supabase infrastructure)
|
||||
- No client-side key management
|
||||
- Simplified chat code
|
||||
|
||||
### Why Remove E2EE?
|
||||
|
||||
1. **Complexity** - Triple-wrapped keys, device recovery, key rotation adds significant maintenance burden
|
||||
2. **Overkill** - For a couples intimacy app, server-side security is sufficient
|
||||
3. **User Experience** - E2EE adds latency, recovery flows, and potential failure modes
|
||||
4. **Development Velocity** - Simpler code = faster iteration
|
||||
|
||||
### Security Model After Removal
|
||||
|
||||
- **In Transit**: TLS 1.3 (Supabase default)
|
||||
- **At Rest**: Supabase encrypts data at rest
|
||||
- **Access Control**: RLS policies restrict access to couple's own messages
|
||||
- **Admin Access**: Admins can read messages (same as current E2EE with admin key)
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy for Existing Messages
|
||||
|
||||
Existing v2 (encrypted) messages need handling. Options:
|
||||
|
||||
### Option A: Decrypt and Convert (Recommended)
|
||||
|
||||
Run a one-time migration to decrypt all v2 messages using admin key and store as v1 plaintext.
|
||||
|
||||
- Pros: Clean slate, no legacy code needed
|
||||
- Cons: Requires admin private key, one-time effort
|
||||
|
||||
### Option B: Leave v2 Messages As-Is
|
||||
|
||||
Keep encrypted messages in database but stop creating new ones. Old messages become unreadable.
|
||||
|
||||
- Pros: Simpler migration
|
||||
- Cons: Users lose access to old messages
|
||||
|
||||
### Option C: Hybrid Display
|
||||
|
||||
Keep decryption code for reading old v2 messages, but send new messages as v1.
|
||||
|
||||
- Pros: No data loss
|
||||
- Cons: Maintains complexity, defeats purpose of removal
|
||||
|
||||
**Recommendation**: Option A - decrypt existing messages during migration window.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Phase 1: Database Migration (Estimated: 2-3 hours)
|
||||
|
||||
#### Task 1.1: Create Migration to Decrypt Existing v2 Messages
|
||||
|
||||
Create an edge function that:
|
||||
|
||||
1. Fetches all messages with `version = 2`
|
||||
2. Decrypts each using admin private key
|
||||
3. Updates to `version = 1`, moves decrypted content to `content` column
|
||||
4. Clears `encrypted_content`, `encryption_iv`, `keys_metadata`
|
||||
|
||||
**Files to create:**
|
||||
|
||||
- `apps/supabase/functions/migrate-e2ee-to-plaintext/index.ts`
|
||||
|
||||
#### Task 1.2: Run Migration
|
||||
|
||||
Execute the migration function against both non-prod and prod databases.
|
||||
|
||||
#### Task 1.3: Verify Migration
|
||||
|
||||
Query to confirm no v2 messages remain:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*) FROM messages WHERE version = 2;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Remove Client-Side Encryption Code (Estimated: 4-6 hours)
|
||||
|
||||
#### Task 2.1: Remove Encryption Library
|
||||
|
||||
Delete the entire encryption directory:
|
||||
|
||||
- `apps/mobile/src/lib/encryption/` (14 files)
|
||||
|
||||
**Files to delete:**
|
||||
|
||||
- `adminKeys.ts`
|
||||
- `constants.ts`
|
||||
- `crypto.ts`
|
||||
- `index.ts`
|
||||
- `keyManager.ts`
|
||||
- `mediaDecryption.ts`
|
||||
- `mediaEncryption.ts`
|
||||
- `messageDecryption.ts`
|
||||
- `messageEncryption.ts`
|
||||
- `reencryptPendingMessages.ts`
|
||||
- `repairStaleKey.ts`
|
||||
- `triggerKeyRotation.ts`
|
||||
- `types.ts`
|
||||
|
||||
#### Task 2.2: Remove Encryption Hooks
|
||||
|
||||
Delete the E2EE-specific hooks:
|
||||
|
||||
**Files to delete:**
|
||||
|
||||
- `apps/mobile/src/hooks/useDecryptedMessage.ts`
|
||||
- `apps/mobile/src/hooks/useDecryptedMedia.ts`
|
||||
- `apps/mobile/src/hooks/useEncryptedSend.ts`
|
||||
|
||||
#### Task 2.3: Simplify useEncryptionKeys Hook
|
||||
|
||||
Either delete entirely or convert to a no-op stub that returns `{ hasKeys: true }` for compatibility.
|
||||
|
||||
**File to modify/delete:**
|
||||
|
||||
- `apps/mobile/src/hooks/useEncryptionKeys.ts`
|
||||
|
||||
#### Task 2.4: Remove EncryptionKeyInitializer Component
|
||||
|
||||
Delete the component that initializes E2EE keys on app startup.
|
||||
|
||||
**File to delete:**
|
||||
|
||||
- `apps/mobile/src/components/EncryptionKeyInitializer.tsx`
|
||||
|
||||
#### Task 2.5: Remove from App Layout
|
||||
|
||||
Remove `EncryptionKeyInitializer` from the app layout.
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/mobile/app/(app)/_layout.tsx`
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Simplify Chat Components (Estimated: 3-4 hours)
|
||||
|
||||
#### Task 3.1: Simplify ChatScreen.tsx
|
||||
|
||||
Remove:
|
||||
|
||||
- `useEncryptedSend` hook usage
|
||||
- `encryptionKeys` from store
|
||||
- `secureReady` checks
|
||||
- Recovery UI (`encryptionKeys.isRecovering`)
|
||||
- Encryption readiness blocking
|
||||
|
||||
Change message sending to use plaintext directly.
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/mobile/src/features/chat/ChatScreen.tsx`
|
||||
|
||||
#### Task 3.2: Simplify MessageContent.tsx
|
||||
|
||||
Remove:
|
||||
|
||||
- `useDecryptedMessage` hook usage
|
||||
- `useDecryptedMedia` hook usage
|
||||
- Decryption loading states
|
||||
- Decryption error states and retry buttons
|
||||
|
||||
Display `message.content` directly.
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/mobile/src/features/chat/MessageContent.tsx`
|
||||
|
||||
#### Task 3.3: Simplify Media Upload
|
||||
|
||||
Remove encryption from media upload flow.
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/mobile/src/hooks/useMediaUpload.ts`
|
||||
|
||||
Changes:
|
||||
|
||||
- Remove `encryptMediaFile` call
|
||||
- Upload files directly without `.enc` extension
|
||||
- Remove E2EE readiness checks
|
||||
|
||||
#### Task 3.4: Simplify InputBar (if applicable)
|
||||
|
||||
Remove any "security not ready" blocking logic.
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/mobile/src/features/chat/InputBar.tsx`
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Clean Up State Management (Estimated: 1-2 hours)
|
||||
|
||||
#### Task 4.1: Remove encryptionKeys from Auth Store
|
||||
|
||||
Remove the entire `encryptionKeys` state object and related actions.
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/mobile/src/store/authStore.ts`
|
||||
|
||||
Remove:
|
||||
|
||||
- `EncryptionKeyState` interface
|
||||
- `encryptionKeys` state
|
||||
- `setEncryptionKeys` action
|
||||
- `beginEncryptionRecovery` / `endEncryptionRecovery` actions
|
||||
- Reset logic in logout
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Remove Edge Functions (Estimated: 1 hour)
|
||||
|
||||
#### Task 5.1: Delete E2EE Edge Functions
|
||||
|
||||
Remove the edge functions that handle key rotation and re-encryption.
|
||||
|
||||
**Files to delete:**
|
||||
|
||||
- `apps/supabase/functions/handle-key-rotation/`
|
||||
- `apps/supabase/functions/reencrypt-pending-messages/`
|
||||
|
||||
#### Task 5.2: Remove from Supabase Dashboard
|
||||
|
||||
If deployed, remove these functions from Supabase dashboard or let them remain (they won't be called).
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Database Cleanup (Optional) (Estimated: 1 hour)
|
||||
|
||||
#### Task 6.1: Remove E2EE Columns (Optional)
|
||||
|
||||
Create migration to drop unused columns. Can be deferred.
|
||||
|
||||
```sql
|
||||
-- Optional cleanup migration
|
||||
ALTER TABLE messages
|
||||
DROP COLUMN IF EXISTS encrypted_content,
|
||||
DROP COLUMN IF EXISTS encryption_iv,
|
||||
DROP COLUMN IF EXISTS keys_metadata;
|
||||
|
||||
ALTER TABLE messages
|
||||
DROP CONSTRAINT IF EXISTS messages_e2ee_fields_check;
|
||||
|
||||
ALTER TABLE profiles
|
||||
DROP COLUMN IF EXISTS public_key_jwk;
|
||||
|
||||
DROP TABLE IF EXISTS master_keys;
|
||||
```
|
||||
|
||||
**Note**: Can leave columns in place - they'll just be NULL for new messages.
|
||||
|
||||
#### Task 6.2: Update Message Version Default
|
||||
|
||||
Ensure new messages use version 1 (or remove version concept entirely).
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: Cleanup and Testing (Estimated: 2-3 hours)
|
||||
|
||||
#### Task 7.1: Remove Unused Dependencies
|
||||
|
||||
Check if `react-native-quick-crypto` can be removed (may be needed for other crypto).
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/mobile/package.json`
|
||||
|
||||
#### Task 7.2: Update CLAUDE.md
|
||||
|
||||
Remove E2EE documentation section.
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `CLAUDE.md`
|
||||
|
||||
#### Task 7.3: Delete E2EE Documentation
|
||||
|
||||
Remove the E2EE implementation docs.
|
||||
|
||||
**File to delete:**
|
||||
|
||||
- `docs/e2ee-implementation.md`
|
||||
|
||||
#### Task 7.4: Update iOS Permission Strings
|
||||
|
||||
Remove "encrypted" from iOS permission descriptions. These strings mention encryption:
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/mobile/app.json`
|
||||
|
||||
**Current strings (lines 26-27):**
|
||||
|
||||
```json
|
||||
"NSCameraUsageDescription": "Sauci uses your camera to take photos and videos that you can share privately with your partner in encrypted chat messages.",
|
||||
"NSPhotoLibraryUsageDescription": "Sauci uses your photo library so you can select photos and videos to share privately with your partner in encrypted chat messages.",
|
||||
```
|
||||
|
||||
**Updated strings:**
|
||||
|
||||
```json
|
||||
"NSCameraUsageDescription": "Sauci uses your camera to take photos and videos that you can share privately with your partner.",
|
||||
"NSPhotoLibraryUsageDescription": "Sauci uses your photo library so you can select photos and videos to share privately with your partner.",
|
||||
```
|
||||
|
||||
After updating `app.json`, run `npx expo prebuild --clean` to regenerate `ios/Sauci/Info.plist`.
|
||||
|
||||
#### Task 7.5: Full Testing
|
||||
|
||||
Test all chat functionality:
|
||||
|
||||
- [ ] Send text message
|
||||
- [ ] Receive text message
|
||||
- [ ] Send image
|
||||
- [ ] Receive image
|
||||
- [ ] Send video
|
||||
- [ ] Receive video
|
||||
- [ ] View old messages (post-migration)
|
||||
- [ ] New user signup and chat
|
||||
- [ ] Device change / reinstall
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
|
||||
### Files to Delete (19 files)
|
||||
|
||||
```text
|
||||
apps/mobile/src/lib/encryption/adminKeys.ts
|
||||
apps/mobile/src/lib/encryption/constants.ts
|
||||
apps/mobile/src/lib/encryption/crypto.ts
|
||||
apps/mobile/src/lib/encryption/index.ts
|
||||
apps/mobile/src/lib/encryption/keyManager.ts
|
||||
apps/mobile/src/lib/encryption/mediaDecryption.ts
|
||||
apps/mobile/src/lib/encryption/mediaEncryption.ts
|
||||
apps/mobile/src/lib/encryption/messageDecryption.ts
|
||||
apps/mobile/src/lib/encryption/messageEncryption.ts
|
||||
apps/mobile/src/lib/encryption/reencryptPendingMessages.ts
|
||||
apps/mobile/src/lib/encryption/repairStaleKey.ts
|
||||
apps/mobile/src/lib/encryption/triggerKeyRotation.ts
|
||||
apps/mobile/src/lib/encryption/types.ts
|
||||
apps/mobile/src/hooks/useDecryptedMessage.ts
|
||||
apps/mobile/src/hooks/useDecryptedMedia.ts
|
||||
apps/mobile/src/hooks/useEncryptedSend.ts
|
||||
apps/mobile/src/hooks/useEncryptionKeys.ts
|
||||
apps/mobile/src/components/EncryptionKeyInitializer.tsx
|
||||
docs/e2ee-implementation.md
|
||||
```
|
||||
|
||||
### Files to Modify (8 files)
|
||||
|
||||
```text
|
||||
apps/mobile/src/features/chat/ChatScreen.tsx
|
||||
apps/mobile/src/features/chat/MessageContent.tsx
|
||||
apps/mobile/src/features/chat/InputBar.tsx
|
||||
apps/mobile/src/hooks/useMediaUpload.ts
|
||||
apps/mobile/src/store/authStore.ts
|
||||
apps/mobile/app/(app)/_layout.tsx
|
||||
apps/mobile/app.json
|
||||
CLAUDE.md
|
||||
```
|
||||
|
||||
### Edge Functions to Delete (2 functions)
|
||||
|
||||
```text
|
||||
apps/supabase/functions/handle-key-rotation/
|
||||
apps/supabase/functions/reencrypt-pending-messages/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estimated Total Effort
|
||||
|
||||
| Phase | Estimated Time |
|
||||
| ------------------------------- | ---------------- |
|
||||
| Phase 1: Database Migration | 2-3 hours |
|
||||
| Phase 2: Remove Encryption Code | 4-6 hours |
|
||||
| Phase 3: Simplify Chat Components | 3-4 hours |
|
||||
| Phase 4: Clean Up State | 1-2 hours |
|
||||
| Phase 5: Remove Edge Functions | 1 hour |
|
||||
| Phase 6: Database Cleanup | 1 hour (optional)|
|
||||
| Phase 7: Testing | 2-3 hours |
|
||||
| **Total** | **14-20 hours** |
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise after deployment:
|
||||
|
||||
1. **Messages are preserved** - Content column still has plaintext for v1, migration preserves decrypted content
|
||||
2. **Re-enable E2EE** - Git revert the client changes
|
||||
3. **No data loss** - The migration only adds data (decrypts), doesn't delete
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Timeline**: When to execute the migration? During maintenance window?
|
||||
2. **Notification**: Should users be notified about the security model change?
|
||||
3. **Privacy Policy**: Does the privacy policy mention E2EE? May need update.
|
||||
4. **App Store**: Any App Store claims about E2EE that need updating?
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,796 +0,0 @@
|
||||
# Missing Inverse Pairs - Review Document
|
||||
|
||||
This document lists all asymmetric questions that are missing their inverse pair.
|
||||
For each question, the **inverse** would swap the `text` and `partner_text` fields,
|
||||
allowing both partners to experience both roles.
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total missing inverses:** 607
|
||||
- **Categories affected:** 10
|
||||
- **Packs affected:** 28
|
||||
|
||||
## How Inverse Pairs Work
|
||||
|
||||
For an asymmetric question like:
|
||||
```
|
||||
text: "Spank your partner"
|
||||
partner_text: "Be spanked by your partner"
|
||||
```
|
||||
|
||||
The inverse would be:
|
||||
```
|
||||
text: "Be spanked by your partner"
|
||||
partner_text: "Spank your partner"
|
||||
```
|
||||
|
||||
This ensures both partners get asked about both roles.
|
||||
|
||||
---
|
||||
|
||||
# Adventure & Travel (1 missing)
|
||||
|
||||
## ✨ Food & Culture (1 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Cook a traditional dish from your partner's heritage | Teach your partner to cook a dish from your heritage | 1 | text: "Teach your partner to cook a dish from your heritage" / partner: "Cook a traditional dish from your partner's heritage" |
|
||||
|
||||
# Bedroom Adventures (40 missing)
|
||||
|
||||
## 🔞 Intimate Games (3 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Take turns being the 'sex instructor' giving commands | Follow your partner's commands as they instruct | 4 | text: "Follow your partner's commands as they instruct" / partner: "Take turns being the 'sex instructor' giving commands" |
|
||||
| 2 | Blindfold your partner and have them guess what you're doing | Be blindfolded and guess what your partner is doing | 4 | text: "Be blindfolded and guess what your partner is doing" / partner: "Blindfold your partner and have them guess what you're doing" |
|
||||
| 3 | Use a timer and give your partner pleasure for exactly 5 minutes | Receive exactly 5 minutes of pleasure from your partner | 4 | text: "Receive exactly 5 minutes of pleasure from your partner" / partner: "Use a timer and give your partner pleasure for exactly 5 minutes" |
|
||||
|
||||
## 🔞 Position Exploration (10 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Have sex in missionary with a pillow under your partner's hips | Have sex in missionary with a pillow under your hips | 4 | text: "Have sex in missionary with a pillow under your hips" / partner: "Have sex in missionary with a pillow under your partner's hips" |
|
||||
| 2 | Let your partner choose three sex positions for tonight | Choose three sex positions for tonight | 3 | text: "Choose three sex positions for tonight" / partner: "Let your partner choose three sex positions for tonight" |
|
||||
| 3 | Have sex with your partner bent over furniture | Be bent over furniture while your partner has sex with you | 4 | text: "Be bent over furniture while your partner has sex with you" / partner: "Have sex with your partner bent over furniture" |
|
||||
| 4 | Lift your partner while having sex | Be lifted by your partner while having sex | 4 | text: "Be lifted by your partner while having sex" / partner: "Lift your partner while having sex" |
|
||||
| 5 | Have sex with your partner on top facing you | Ride your partner facing them | 5 | text: "Ride your partner facing them" / partner: "Have sex with your partner on top facing you" |
|
||||
| 6 | Have sex with your partner's legs on your shoulders | Have sex with your legs on your partner's shoulders | 4 | text: "Have sex with your legs on your partner's shoulders" / partner: "Have sex with your partner's legs on your shoulders" |
|
||||
| 7 | Have sex with your partner's ankles by their ears | Have sex with your ankles by your ears | 4 | text: "Have sex with your ankles by your ears" / partner: "Have sex with your partner's ankles by their ears" |
|
||||
| 8 | Have your partner wrap their legs around you during sex | Wrap your legs around your partner during sex | 4 | text: "Wrap your legs around your partner during sex" / partner: "Have your partner wrap their legs around you during sex" |
|
||||
| 9 | Perform oral while your partner is seated | Receive oral while seated | 3 | text: "Receive oral while seated" / partner: "Perform oral while your partner is seated" |
|
||||
| 10 | Have sex in a chair with your partner in your lap | Have sex in your partner's lap while sitting in a chair | 4 | text: "Have sex in your partner's lap while sitting in a chair" / partner: "Have sex in a chair with your partner in your lap" |
|
||||
|
||||
## 🔞 Sensory Play (27 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Apply menthol or tingling lube to sensitive areas | Have tingling lube applied to sensitive areas | 4 | text: "Have tingling lube applied to sensitive areas" / partner: "Apply menthol or tingling lube to sensitive areas" |
|
||||
| 2 | Use noise-canceling headphones and surprise your partner | Wear headphones and be surprised | 4 | text: "Wear headphones and be surprised" / partner: "Use noise-canceling headphones and surprise your partner" |
|
||||
| 3 | Kiss your partner everywhere while they keep their eyes closed | Keep your eyes closed while being kissed everywhere | 4 | text: "Keep your eyes closed while being kissed everywhere" / partner: "Kiss your partner everywhere while they keep their eyes closed" |
|
||||
| 4 | Trace words or patterns on your partner's back and have them guess | Guess what your partner is tracing on your back | 3 | text: "Guess what your partner is tracing on your back" / partner: "Trace words or patterns on your partner's back and have them guess" |
|
||||
| 5 | Use a pinwheel or scratchy texture on your partner's skin | Feel a pinwheel or scratchy texture on your skin | 4 | text: "Feel a pinwheel or scratchy texture on your skin" / partner: "Use a pinwheel or scratchy texture on your partner's skin" |
|
||||
| 6 | Blindfold your partner and use only your tongue | Be blindfolded while your partner uses only their tongue | 4 | text: "Be blindfolded while your partner uses only their tongue" / partner: "Blindfold your partner and use only your tongue" |
|
||||
| 7 | Cover your partner's body in whipped cream and lick it off | Have whipped cream licked off your body | 4 | text: "Have whipped cream licked off your body" / partner: "Cover your partner's body in whipped cream and lick it off" |
|
||||
| 8 | Use your hair to tickle and tease your partner | Be tickled and teased with your partner's hair | 3 | text: "Be tickled and teased with your partner's hair" / partner: "Use your hair to tickle and tease your partner" |
|
||||
| 9 | Apply ice to your lips before kissing your partner everywhere | Be kissed everywhere with cold lips | 4 | text: "Be kissed everywhere with cold lips" / partner: "Apply ice to your lips before kissing your partner everywhere" |
|
||||
| 10 | Use a vibrator on your partner while they're blindfolded | Experience a vibrator while blindfolded | 4 | text: "Experience a vibrator while blindfolded" / partner: "Use a vibrator on your partner while they're blindfolded" |
|
||||
| 11 | Cover your partner's eyes and whisper dirty talk | Have your eyes covered while hearing dirty talk | 4 | text: "Have your eyes covered while hearing dirty talk" / partner: "Cover your partner's eyes and whisper dirty talk" |
|
||||
| 12 | Use different textures on your partner's skin during foreplay | Experience different textures during foreplay | 4 | text: "Experience different textures during foreplay" / partner: "Use different textures on your partner's skin during foreplay" |
|
||||
| 13 | Drip candle wax on your partner (massage candle) | Have massage candle wax dripped on you | 4 | text: "Have massage candle wax dripped on you" / partner: "Drip candle wax on your partner (massage candle)" |
|
||||
| 14 | Use a blindfold and have your partner guess what toy you're using | Guess what toy is being used on you while blindfolded | 4 | text: "Guess what toy is being used on you while blindfolded" / partner: "Use a blindfold and have your partner guess what toy you're using" |
|
||||
| 15 | Use silk scarves to restrain your partner lightly | Be lightly restrained with silk scarves | 5 | text: "Be lightly restrained with silk scarves" / partner: "Use silk scarves to restrain your partner lightly" |
|
||||
| 16 | Blindfold your partner and feed them aphrodisiacs | Be blindfolded and fed aphrodisiacs | 3 | text: "Be blindfolded and fed aphrodisiacs" / partner: "Blindfold your partner and feed them aphrodisiacs" |
|
||||
| 17 | Use a wartenberg wheel on your partner's skin | Have a wartenberg wheel used on your skin | 4 | text: "Have a wartenberg wheel used on your skin" / partner: "Use a wartenberg wheel on your partner's skin" |
|
||||
| 18 | Use flavored lube and let your partner guess the flavor | Guess the flavor of lube your partner is using | 4 | text: "Guess the flavor of lube your partner is using" / partner: "Use flavored lube and let your partner guess the flavor" |
|
||||
| 19 | Put earplugs in your partner and surprise them with touch | Wear earplugs and be surprised by touch | 4 | text: "Wear earplugs and be surprised by touch" / partner: "Put earplugs in your partner and surprise them with touch" |
|
||||
| 20 | Use a feather to tease your partner's most sensitive areas | Be teased with a feather on your sensitive areas | 4 | text: "Be teased with a feather on your sensitive areas" / partner: "Use a feather to tease your partner's most sensitive areas" |
|
||||
| 21 | Drip warm massage oil on your partner | Have warm oil dripped on your body | 4 | text: "Have warm oil dripped on your body" / partner: "Drip warm massage oil on your partner" |
|
||||
| 22 | Use ice on your partner's body during foreplay | Have ice used on your body during foreplay | 4 | text: "Have ice used on your body during foreplay" / partner: "Use ice on your partner's body during foreplay" |
|
||||
| 23 | Blindfold your partner during sex | Be blindfolded during sex | 5 | text: "Be blindfolded during sex" / partner: "Blindfold your partner during sex" |
|
||||
| 24 | Use a fur mitt or soft glove during foreplay | Experience fur or soft glove during foreplay | 3 | text: "Experience fur or soft glove during foreplay" / partner: "Use a fur mitt or soft glove during foreplay" |
|
||||
| 25 | Blindfold your partner in the shower and wash them sensually | Be blindfolded in the shower and washed sensually | 4 | text: "Be blindfolded in the shower and washed sensually" / partner: "Blindfold your partner in the shower and wash them sensually" |
|
||||
| 26 | Tie your partner's hands loosely and tease them | Have your hands tied loosely and be teased | 5 | text: "Have your hands tied loosely and be teased" / partner: "Tie your partner's hands loosely and tease them" |
|
||||
| 27 | Use your breath to create sensations on your partner's body | Feel your partner's breath creating sensations | 4 | text: "Feel your partner's breath creating sensations" / partner: "Use your breath to create sensations on your partner's body" |
|
||||
|
||||
# Fantasy Exploration (46 missing)
|
||||
|
||||
## 🔞 Dream Scenarios (4 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Share a fantasy about your partner surprising you sexually | Share a fantasy about surprising your partner sexually | 4 | text: "Share a fantasy about surprising your partner sexually" / partner: "Share a fantasy about your partner surprising you sexually" |
|
||||
| 2 | Share your biggest sexual fantasy with your partner | Hear your partner's biggest sexual fantasy | 4 | text: "Hear your partner's biggest sexual fantasy" / partner: "Share your biggest sexual fantasy with your partner" |
|
||||
| 3 | Share a fantasy about seducing your partner | Share a fantasy about being seduced | 3 | text: "Share a fantasy about being seduced" / partner: "Share a fantasy about seducing your partner" |
|
||||
| 4 | Describe a scenario where you worship your partner's body | Describe a scenario where your body is worshipped | 4 | text: "Describe a scenario where your body is worshipped" / partner: "Describe a scenario where you worship your partner's body" |
|
||||
|
||||
## 🔞 Language Exploration (14 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Degrade your partner with dirty talk | Be sexually degraded by your partner's dirty talk | 5 | text: "Be sexually degraded by your partner's dirty talk" / partner: "Degrade your partner with dirty talk" |
|
||||
| 2 | Use praise like 'good girl' or 'good boy' on your partner | Be praised as a 'good girl' or 'good boy' by your partner | 3 | text: "Be praised as a 'good girl' or 'good boy' by your partner" / partner: "Use praise like 'good girl' or 'good boy' on your partner" |
|
||||
| 3 | Make your partner call you 'Daddy' | Let your partner make you call them 'Daddy' | 5 | text: "Let your partner make you call them 'Daddy'" / partner: "Make your partner call you 'Daddy'" |
|
||||
| 4 | Dictate captions for your partner's nude poses | Pose for nudes while your partner dictates the captions | 4 | text: "Pose for nudes while your partner dictates the captions" / partner: "Dictate captions for your partner's nude poses" |
|
||||
| 5 | Narrate graphic fantasies while edging your partner | Be edged while listening to your partner's graphic fantasies | 5 | text: "Be edged while listening to your partner's graphic fantasies" / partner: "Narrate graphic fantasies while edging your partner" |
|
||||
| 6 | Make your partner beg for release | Beg your partner for permission to cum | 5 | text: "Beg your partner for permission to cum" / partner: "Make your partner beg for release" |
|
||||
| 7 | Whisper vivid descriptions of your partnerâs physical submission | Listen to your partner describe your body's total surrender | 4 | text: "Listen to your partner describe your body's total surrender" / partner: "Whisper vivid descriptions of your partnerâs physical submission" |
|
||||
| 8 | Whisper forbidden fantasies into your partner's ear | Listen to your partner's darkest desires | 3 | text: "Listen to your partner's darkest desires" / partner: "Whisper forbidden fantasies into your partner's ear" |
|
||||
| 9 | Describe your partner's body in explicit detail | Have your body described in explicit detail by your partner | 3 | text: "Have your body described in explicit detail by your partner" / partner: "Describe your partner's body in explicit detail" |
|
||||
| 10 | Correct your partnerâs language using physical discipline | Receive physical correction for using the wrong words | 5 | text: "Receive physical correction for using the wrong words" / partner: "Correct your partnerâs language using physical discipline" |
|
||||
| 11 | Use a ball gag to silence your partner's protests | Experience being silenced and controlled with a ball gag | 5 | text: "Experience being silenced and controlled with a ball gag" / partner: "Use a ball gag to silence your partner's protests" |
|
||||
| 12 | Narrate exactly how you will fuck your partner | Listen to your partner's detailed fucking plan | 4 | text: "Listen to your partner's detailed fucking plan" / partner: "Narrate exactly how you will fuck your partner" |
|
||||
| 13 | Record a dirty voice memo of your explicit plans for your partner | Receive a dirty voice memo detailing your partner's explicit plans | 3 | text: "Receive a dirty voice memo detailing your partner's explicit plans" / partner: "Record a dirty voice memo of your explicit plans for your partner" |
|
||||
| 14 | Describe your partner's taste in graphic detail | Listen to your partner describe your taste | 4 | text: "Listen to your partner describe your taste" / partner: "Describe your partner's taste in graphic detail" |
|
||||
|
||||
## 🔞 Role Play Ideas (3 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Act out being seduced by a mysterious stranger | Act out seducing someone as a mysterious stranger | 4 | text: "Act out seducing someone as a mysterious stranger" / partner: "Act out being seduced by a mysterious stranger" |
|
||||
| 2 | Role play as a stripper giving a private dance | Role play as someone receiving a private dance | 4 | text: "Role play as someone receiving a private dance" / partner: "Role play as a stripper giving a private dance" |
|
||||
| 3 | Role play as a doctor giving an examination | Role play as a patient getting an examination | 5 | text: "Role play as a patient getting an examination" / partner: "Role play as a doctor giving an examination" |
|
||||
|
||||
## 🔞 Secret Desires (25 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Tell your partner what you want them to do more often | Hear what your partner wants you to do more often | 2 | text: "Hear what your partner wants you to do more often" / partner: "Tell your partner what you want them to do more often" |
|
||||
| 2 | Confess a desire you've been too shy to mention | Hear a desire your partner has been too shy to mention | 3 | text: "Hear a desire your partner has been too shy to mention" / partner: "Confess a desire you've been too shy to mention" |
|
||||
| 3 | Share a sexual 'bucket list' experience you want to have | Hear a sexual 'bucket list' experience your partner wants | 3 | text: "Hear a sexual 'bucket list' experience your partner wants" / partner: "Share a sexual 'bucket list' experience you want to have" |
|
||||
| 4 | Reveal what you secretly want to hear during sex | Learn what your partner wants to hear during sex | 3 | text: "Learn what your partner wants to hear during sex" / partner: "Reveal what you secretly want to hear during sex" |
|
||||
| 5 | Share a fantasy involving voyeurism | Hear your partner's fantasy about being watched or watching | 3 | text: "Hear your partner's fantasy about being watched or watching" / partner: "Share a fantasy involving voyeurism" |
|
||||
| 6 | Describe what you want to say or hear during dirty talk | Hear what your partner wants to say or hear during dirty talk | 3 | text: "Hear what your partner wants to say or hear during dirty talk" / partner: "Describe what you want to say or hear during dirty talk" |
|
||||
| 7 | Describe a specific fantasy involving oral sex | Hear your partner's fantasy about giving or receiving oral | 3 | text: "Hear your partner's fantasy about giving or receiving oral" / partner: "Describe a specific fantasy involving oral sex" |
|
||||
| 8 | Describe your desire for slow, gentle lovemaking | Hear your partner's desire for slow, gentle lovemaking | 2 | text: "Hear your partner's desire for slow, gentle lovemaking" / partner: "Describe your desire for slow, gentle lovemaking" |
|
||||
| 9 | Share a fantasy involving exhibitionism | Hear your partner's fantasy about being seen | 3 | text: "Hear your partner's fantasy about being seen" / partner: "Share a fantasy involving exhibitionism" |
|
||||
| 10 | Share what you fantasize about while masturbating | Hear what your partner fantasizes about while masturbating | 3 | text: "Hear what your partner fantasizes about while masturbating" / partner: "Share what you fantasize about while masturbating" |
|
||||
| 11 | Describe your desire for rougher sex | Hear your partner's desire for rougher sex | 3 | text: "Hear your partner's desire for rougher sex" / partner: "Describe your desire for rougher sex" |
|
||||
| 12 | Describe your hottest sexual fantasy involving your partner | Hear your partner's hottest sexual fantasy about you | 3 | text: "Hear your partner's hottest sexual fantasy about you" / partner: "Describe your hottest sexual fantasy involving your partner" |
|
||||
| 13 | Share a desire about how you want to be touched | Learn how your partner wants to be touched | 4 | text: "Learn how your partner wants to be touched" / partner: "Share a desire about how you want to be touched" |
|
||||
| 14 | Share a secret turn-on that might surprise your partner | Learn a secret turn-on from your partner | 4 | text: "Learn a secret turn-on from your partner" / partner: "Share a secret turn-on that might surprise your partner" |
|
||||
| 15 | Tell your partner the one thing that would make sex perfect | Hear what would make sex perfect for your partner | 4 | text: "Hear what would make sex perfect for your partner" / partner: "Tell your partner the one thing that would make sex perfect" |
|
||||
| 16 | Explain your desire to be the submissive one | Hear about your partner's desire to be submissive | 3 | text: "Hear about your partner's desire to be submissive" / partner: "Explain your desire to be the submissive one" |
|
||||
| 17 | Describe the type of porn that turns you on | Hear about the porn that turns your partner on | 3 | text: "Hear about the porn that turns your partner on" / partner: "Describe the type of porn that turns you on" |
|
||||
| 18 | Describe a taboo fantasy that excites you | Hear about a taboo fantasy that excites your partner | 3 | text: "Hear about a taboo fantasy that excites your partner" / partner: "Describe a taboo fantasy that excites you" |
|
||||
| 19 | Reveal a kink you're curious about exploring | Hear about a kink your partner wants to explore | 3 | text: "Hear about a kink your partner wants to explore" / partner: "Reveal a kink you're curious about exploring" |
|
||||
| 20 | Describe your desire to be worshipped in bed | Hear about your partner's desire to be worshipped | 3 | text: "Hear about your partner's desire to be worshipped" / partner: "Describe your desire to be worshipped in bed" |
|
||||
| 21 | Reveal a location where you want to have sex | Hear where your partner secretly wants to have sex | 3 | text: "Hear where your partner secretly wants to have sex" / partner: "Reveal a location where you want to have sex" |
|
||||
| 22 | Explain your desire to be the dominant one | Hear about your partner's desire to be dominant | 3 | text: "Hear about your partner's desire to be dominant" / partner: "Explain your desire to be the dominant one" |
|
||||
| 23 | Share a desire to have sex multiple times in one day | Hear your partner's desire for multiple rounds of sex | 3 | text: "Hear your partner's desire for multiple rounds of sex" / partner: "Share a desire to have sex multiple times in one day" |
|
||||
| 24 | Describe your ultimate sexual fantasy | Hear your partner's ultimate sexual fantasy in detail | 3 | text: "Hear your partner's ultimate sexual fantasy in detail" / partner: "Describe your ultimate sexual fantasy" |
|
||||
| 25 | Describe a fantasy involving teasing and denial | Hear your partner's fantasy about teasing and denial | 3 | text: "Hear your partner's fantasy about teasing and denial" / partner: "Describe a fantasy involving teasing and denial" |
|
||||
|
||||
# Getting Started (5 missing)
|
||||
|
||||
## 🔞 Know Your Limits (5 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Wear a butt plug during sex | Have your partner wear a butt plug during sex | 4 | text: "Have your partner wear a butt plug during sex" / partner: "Wear a butt plug during sex" |
|
||||
| 2 | Swallow when giving your partner oral | Have your partner swallow | 4 | text: "Have your partner swallow" / partner: "Swallow when giving your partner oral" |
|
||||
| 3 | Deep throat your partner | Have your partner deep throat you | 4 | text: "Have your partner deep throat you" / partner: "Deep throat your partner" |
|
||||
| 4 | Have your partner cum inside you | Cum inside your partner | 4 | text: "Cum inside your partner" / partner: "Have your partner cum inside you" |
|
||||
| 5 | Have your partner cum on your body | Cum on your partner's body | 4 | text: "Cum on your partner's body" / partner: "Have your partner cum on your body" |
|
||||
|
||||
# Long Distance (106 missing)
|
||||
|
||||
## ✨ Missing You (30 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Send a photo of your hand and say you wish you were holding theirs | Receive a sweet reminder of how much your partner wants to hold you | 1 | text: "Receive a sweet reminder of how much your partner wants to hold you" / partner: "Send a photo of your hand and say you wish you were holding theirs" |
|
||||
| 2 | Tell your partner you saw someone who reminded you of them | Hear how often your partner has you on their mind | 1 | text: "Hear how often your partner has you on their mind" / partner: "Tell your partner you saw someone who reminded you of them" |
|
||||
| 3 | Tell your partner what you'd be doing right now if they were there | Hear what your partner wishes you were doing together | 2 | text: "Hear what your partner wishes you were doing together" / partner: "Tell your partner what you'd be doing right now if they were there" |
|
||||
| 4 | Send a tipsy voice note telling them you miss them | Get a slightly drunk "I miss you" message | 2 | text: "Get a slightly drunk "I miss you" message" / partner: "Send a tipsy voice note telling them you miss them" |
|
||||
| 5 | Send a photo of your bed and say it feels empty without them | Receive a sweet photo of your partner's bed and a message that you're missed | 2 | text: "Receive a sweet photo of your partner's bed and a message that you're missed" / partner: "Send a photo of your bed and say it feels empty without them" |
|
||||
| 6 | Send a photo of an empty seat and say you saved it for them | Receive a photo of a seat saved just for you | 1 | text: "Receive a photo of a seat saved just for you" / partner: "Send a photo of an empty seat and say you saved it for them" |
|
||||
| 7 | Send a lazy selfie and say you wish you were doing nothing together | Receive a lazy selfie and an invitation to do nothing together | 1 | text: "Receive a lazy selfie and an invitation to do nothing together" / partner: "Send a lazy selfie and say you wish you were doing nothing together" |
|
||||
| 8 | Text your partner from an event that it would be better with them | Know your partner wishes you were there | 2 | text: "Know your partner wishes you were there" / partner: "Text your partner from an event that it would be better with them" |
|
||||
| 9 | Tell your partner something small they do that you miss unexpectedly | Hear what little thing your partner misses about you | 2 | text: "Hear what little thing your partner misses about you" / partner: "Tell your partner something small they do that you miss unexpectedly" |
|
||||
| 10 | FaceTime your partner just to see their face for a few minutes | Get an "I just needed to see you" call | 2 | text: "Get an "I just needed to see you" call" / partner: "FaceTime your partner just to see their face for a few minutes" |
|
||||
| 11 | Tell your partner what you're counting down to about seeing them | Build excitement for the reunion | 2 | text: "Build excitement for the reunion" / partner: "Tell your partner what you're counting down to about seeing them" |
|
||||
| 12 | Voice note your partner something you've been meaning to tell them | Hear something meaningful from your partner | 2 | text: "Hear something meaningful from your partner" / partner: "Voice note your partner something you've been meaning to tell them" |
|
||||
| 13 | Send a voice note from bed saying you miss sleeping next to them | Hear a sleepy voice note about how much you are missed in bed | 2 | text: "Hear a sleepy voice note about how much you are missed in bed" / partner: "Send a voice note from bed saying you miss sleeping next to them" |
|
||||
| 14 | Tell your partner one thing you'll do to them when you're alone | Hear exactly what your partner has planned for your next reunion | 3 | text: "Hear exactly what your partner has planned for your next reunion" / partner: "Tell your partner one thing you'll do to them when you're alone" |
|
||||
| 15 | Send a mirror selfie when you are dressed up to go out | Receive a photo of your partner looking their best | 2 | text: "Receive a photo of your partner looking their best" / partner: "Send a mirror selfie when you are dressed up to go out" |
|
||||
| 16 | Tell your partner what random thing reminded you of them today | Hear that random things trigger memories of you | 2 | text: "Hear that random things trigger memories of you" / partner: "Tell your partner what random thing reminded you of them today" |
|
||||
| 17 | Text your partner that you caught yourself smiling at your phone because of them | Know you made your partner smile | 2 | text: "Know you made your partner smile" / partner: "Text your partner that you caught yourself smiling at your phone because of them" |
|
||||
| 18 | Describe the exact way you want them to hold you when you reunite | Feel the anticipation build for their touch | 2 | text: "Feel the anticipation build for their touch" / partner: "Describe the exact way you want them to hold you when you reunite" |
|
||||
| 19 | Send a sleepy morning selfie, messy hair and all | See your partner's unfiltered morning face | 2 | text: "See your partner's unfiltered morning face" / partner: "Send a sleepy morning selfie, messy hair and all" |
|
||||
| 20 | Tell your partner what outfit of theirs you can't stop thinking about | Hear what look of yours drives them crazy | 2 | text: "Hear what look of yours drives them crazy" / partner: "Tell your partner what outfit of theirs you can't stop thinking about" |
|
||||
| 21 | Text your partner at a party that you'd rather be home calling them | Feel chosen over a social event | 2 | text: "Feel chosen over a social event" / partner: "Text your partner at a party that you'd rather be home calling them" |
|
||||
| 22 | Text your partner first thing in the morning that they were your first thought | Wake up knowing you're on their mind | 2 | text: "Wake up knowing you're on their mind" / partner: "Text your partner first thing in the morning that they were your first thought" |
|
||||
| 23 | Send a photo of you wearing something of theirs | See your partner in your hoodie | 2 | text: "See your partner in your hoodie" / partner: "Send a photo of you wearing something of theirs" |
|
||||
| 24 | Tell your partner about a dream you had about them | Hear you were in your partner's dreams | 2 | text: "Hear you were in your partner's dreams" / partner: "Tell your partner about a dream you had about them" |
|
||||
| 25 | Send a getting-ready selfie and ask if they approve | Give your partner's outfit the seal of approval | 2 | text: "Give your partner's outfit the seal of approval" / partner: "Send a getting-ready selfie and ask if they approve" |
|
||||
| 26 | Send a voice note when you're half asleep saying goodnight | Hear your partner's sleepy voice | 2 | text: "Hear your partner's sleepy voice" / partner: "Send a voice note when you're half asleep saying goodnight" |
|
||||
| 27 | Tell your partner something specific about their body you've been thinking about | Hear what physical thing your partner misses | 2 | text: "Hear what physical thing your partner misses" / partner: "Tell your partner something specific about their body you've been thinking about" |
|
||||
| 28 | Describe exactly how you'd greet them at the airport | Build anticipation for the reunion | 2 | text: "Build anticipation for the reunion" / partner: "Describe exactly how you'd greet them at the airport" |
|
||||
| 29 | Text your partner during a boring meeting that you're thinking about them | Get a random "thinking of you" during work | 2 | text: "Get a random "thinking of you" during work" / partner: "Text your partner during a boring meeting that you're thinking about them" |
|
||||
| 30 | Text your partner "wish you were here" from somewhere fun | Hear that your partner wishes you were by their side | 1 | text: "Hear that your partner wishes you were by their side" / partner: "Text your partner "wish you were here" from somewhere fun" |
|
||||
|
||||
## 🔞 Private Line (27 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Text your partner what you're wearing, then what you're taking off | Get a strip tease via text | 3 | text: "Get a strip tease via text" / partner: "Text your partner what you're wearing, then what you're taking off" |
|
||||
| 2 | Send a post-climax photo to your partner | See your partner's glow in a post-climax photo | 4 | text: "See your partner's glow in a post-climax photo" / partner: "Send a post-climax photo to your partner" |
|
||||
| 3 | Text your partner a play-by-play while you pleasure yourself | Read a play-by-play as your partner pleasures themselves | 3 | text: "Read a play-by-play as your partner pleasures themselves" / partner: "Text your partner a play-by-play while you pleasure yourself" |
|
||||
| 4 | Voice note your partner while touching yourself - no words, just sounds | Hear your partner pleasure themselves | 4 | text: "Hear your partner pleasure themselves" / partner: "Voice note your partner while touching yourself - no words, just sounds" |
|
||||
| 5 | Send a voice note of your release while saying their name | Hear your partner's release as they say your name | 4 | text: "Hear your partner's release as they say your name" / partner: "Send a voice note of your release while saying their name" |
|
||||
| 6 | Share a secret fantasy with your partner in detail | Listen to your partner share a secret fantasy in detail | 2 | text: "Listen to your partner share a secret fantasy in detail" / partner: "Share a secret fantasy with your partner in detail" |
|
||||
| 7 | Talk your partner through how to touch themselves over the phone | Let your partner guide your touch over the phone | 4 | text: "Let your partner guide your touch over the phone" / partner: "Talk your partner through how to touch themselves over the phone" |
|
||||
| 8 | Send a photo from the shower thinking about your partner | See your partner wet and thinking of you | 3 | text: "See your partner wet and thinking of you" / partner: "Send a photo from the shower thinking about your partner" |
|
||||
| 9 | Ask your partner for a specific photo you've been craving | Send your partner a specific photo they've been craving | 4 | text: "Send your partner a specific photo they've been craving" / partner: "Ask your partner for a specific photo you've been craving" |
|
||||
| 10 | Send your partner a short video of your hand where you wish theirs was | Watch your partner touch themselves for you | 4 | text: "Watch your partner touch themselves for you" / partner: "Send your partner a short video of your hand where you wish theirs was" |
|
||||
| 11 | Send your partner a video of you getting undressed | Watch your partner undress for you | 3 | text: "Watch your partner undress for you" / partner: "Send your partner a video of you getting undressed" |
|
||||
| 12 | Send a photo in bed after finishing and tell them it should've been them | See your partner satisfied but wishing you were there | 4 | text: "See your partner satisfied but wishing you were there" / partner: "Send a photo in bed after finishing and tell them it should've been them" |
|
||||
| 13 | Tell your partner step-by-step what you're going to do when you reunite | Hear your partner's detailed plan for your body | 3 | text: "Hear your partner's detailed plan for your body" / partner: "Tell your partner step-by-step what you're going to do when you reunite" |
|
||||
| 14 | Video call and make your partner watch without touching until you say so | Be made to watch without touching yourself | 4 | text: "Be made to watch without touching yourself" / partner: "Video call and make your partner watch without touching until you say so" |
|
||||
| 15 | Send a video of you in their favorite position, waiting | See your partner ready and waiting for you | 4 | text: "See your partner ready and waiting for you" / partner: "Send a video of you in their favorite position, waiting" |
|
||||
| 16 | Record yourself saying exactly what you'd whisper in their ear in bed | Hear your partner's voice like they're right next to you | 3 | text: "Hear your partner's voice like they're right next to you" / partner: "Record yourself saying exactly what you'd whisper in their ear in bed" |
|
||||
| 17 | Send your partner a video of you in the mirror, showing off for them | Watch your partner show off their body for you | 3 | text: "Watch your partner show off their body for you" / partner: "Send your partner a video of you in the mirror, showing off for them" |
|
||||
| 18 | Describe what you're imagining while you touch yourself over text | Read your partner's thoughts while they get off to you | 4 | text: "Read your partner's thoughts while they get off to you" / partner: "Describe what you're imagining while you touch yourself over text" |
|
||||
| 19 | Send a voice note telling your partner to touch themselves right now | Follow your partner's command from afar | 4 | text: "Follow your partner's command from afar" / partner: "Send a voice note telling your partner to touch themselves right now" |
|
||||
| 20 | Watch the same video together over FaceTime and see who breaks first | Get turned on together from miles away | 4 | text: "Get turned on together from miles away" / partner: "Watch the same video together over FaceTime and see who breaks first" |
|
||||
| 21 | Describe what position you want them in the moment you're alone together | Hear how your partner wants to have you | 3 | text: "Hear how your partner wants to have you" / partner: "Describe what position you want them in the moment you're alone together" |
|
||||
| 22 | Sext your partner discreetly while you are both busy | Exchange discreet, sensual texts while you are both busy | 3 | text: "Exchange discreet, sensual texts while you are both busy" / partner: "Sext your partner discreetly while you are both busy" |
|
||||
| 23 | Send a voice note describing what you'd do to your partner | Hear your partner describe what they want to do to you | 3 | text: "Hear your partner describe what they want to do to you" / partner: "Send a voice note describing what you'd do to your partner" |
|
||||
| 24 | Send your partner a sensual morning text | Wake up to a sensual text from your partner | 2 | text: "Wake up to a sensual text from your partner" / partner: "Send your partner a sensual morning text" |
|
||||
| 25 | Send a teasing text to build anticipation for a later call | Receive a teasing text that builds anticipation for a call | 2 | text: "Receive a teasing text that builds anticipation for a call" / partner: "Send a teasing text to build anticipation for a later call" |
|
||||
| 26 | Send your partner a teasing, private photo | Receive a teasing, private photo from your partner | 3 | text: "Receive a teasing, private photo from your partner" / partner: "Send your partner a teasing, private photo" |
|
||||
| 27 | Tell your partner exactly where you want their mouth when you see them | Hear where your partner wants to taste you | 2 | text: "Hear where your partner wants to taste you" / partner: "Tell your partner exactly where you want their mouth when you see them" |
|
||||
|
||||
## ✨ Staying Close (19 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Challenge your partner to an online board game | Be challenged to an online board game by your partner | 1 | text: "Be challenged to an online board game by your partner" / partner: "Challenge your partner to an online board game" |
|
||||
| 2 | Send your partner a voice note on your commute | Get a voice note from your partner during their day | 1 | text: "Get a voice note from your partner during their day" / partner: "Send your partner a voice note on your commute" |
|
||||
| 3 | Send a random photo of your current view | Receive a candid snapshot of your partner's day | 1 | text: "Receive a candid snapshot of your partner's day" / partner: "Send a random photo of your current view" |
|
||||
| 4 | Send your partner a care package of their favorite snacks | Receive a snack-filled care package from your partner | 1 | text: "Receive a snack-filled care package from your partner" / partner: "Send your partner a care package of their favorite snacks" |
|
||||
| 5 | Text your partner updates from a social event they'd hate | Live vicariously through your partner's obligations | 1 | text: "Live vicariously through your partner's obligations" / partner: "Text your partner updates from a social event they'd hate" |
|
||||
| 6 | Make a shared note of restaurants to try when you're together | Build a list for your next visit | 1 | text: "Build a list for your next visit" / partner: "Make a shared note of restaurants to try when you're together" |
|
||||
| 7 | Share your coffee order for your next visit | Learn your partner's go-to coffee order | 1 | text: "Learn your partner's go-to coffee order" / partner: "Share your coffee order for your next visit" |
|
||||
| 8 | Send your partner a meme that only they would understand | Receive an inside joke from your partner | 1 | text: "Receive an inside joke from your partner" / partner: "Send your partner a meme that only they would understand" |
|
||||
| 9 | Watch a live sports game or event together while texting reactions | React to something live with your partner | 1 | text: "React to something live with your partner" / partner: "Watch a live sports game or event together while texting reactions" |
|
||||
| 10 | Do the same Wordle and compare results | Compete on a daily puzzle with your partner | 1 | text: "Compete on a daily puzzle with your partner" / partner: "Do the same Wordle and compare results" |
|
||||
| 11 | Send your partner a song you heard that made you think of them | Get a track that reminded your partner of you | 1 | text: "Get a track that reminded your partner of you" / partner: "Send your partner a song you heard that made you think of them" |
|
||||
| 12 | Screenshot something funny from your day and send it with no context | Get a random screenshot from your partner | 1 | text: "Get a random screenshot from your partner" / partner: "Screenshot something funny from your day and send it with no context" |
|
||||
| 13 | Surprise your partner by ordering delivery to their house | Be surprised with a delivery meal from your partner | 1 | text: "Be surprised with a delivery meal from your partner" / partner: "Surprise your partner by ordering delivery to their house" |
|
||||
| 14 | Share your location with your partner for safety | See your partner's location to know they're safe | 1 | text: "See your partner's location to know they're safe" / partner: "Share your location with your partner for safety" |
|
||||
| 15 | Send a voice note venting about your day | Listen to your partner vent about their day | 1 | text: "Listen to your partner vent about their day" / partner: "Send a voice note venting about your day" |
|
||||
| 16 | Video call your partner while you grocery shop | Go grocery shopping virtually with your partner | 1 | text: "Go grocery shopping virtually with your partner" / partner: "Video call your partner while you grocery shop" |
|
||||
| 17 | Call your partner while you're on a walk just to chat | Take a walk with your partner over the phone | 1 | text: "Take a walk with your partner over the phone" / partner: "Call your partner while you're on a walk just to chat" |
|
||||
| 18 | Send a photo of something that reminded you of an inside joke | Get a reference only you two would understand | 1 | text: "Get a reference only you two would understand" / partner: "Send a photo of something that reminded you of an inside joke" |
|
||||
| 19 | Video call your partner during your lunch break | Enjoy a quick lunch date over video call | 1 | text: "Enjoy a quick lunch date over video call" / partner: "Video call your partner during your lunch break" |
|
||||
|
||||
## 🔞 Total Control (30 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Set rules your partner must follow until you see each other again | Follow your partner's rules until your next reunion | 5 | text: "Follow your partner's rules until your next reunion" / partner: "Set rules your partner must follow until you see each other again" |
|
||||
| 2 | Make your partner edge for you on video every night for a week | Edge for your partner on camera every night until they let you finish | 5 | text: "Edge for your partner on camera every night until they let you finish" / partner: "Make your partner edge for you on video every night for a week" |
|
||||
| 3 | Build a reunion list of everything you'll do when you're finally together | Add to the list of what your partner will do to you when you meet | 4 | text: "Add to the list of what your partner will do to you when you meet" / partner: "Build a reunion list of everything you'll do when you're finally together" |
|
||||
| 4 | Control your partner's toy on maximum until they use your safe word | Take as much as you can until you have to tap out | 5 | text: "Take as much as you can until you have to tap out" / partner: "Control your partner's toy on maximum until they use your safe word" |
|
||||
| 5 | Make your partner sleep with a plug in and send proof in the morning | Sleep with a plug and send your partner a photo when you wake up | 5 | text: "Sleep with a plug and send your partner a photo when you wake up" / partner: "Make your partner sleep with a plug in and send proof in the morning" |
|
||||
| 6 | Have a session where your partner can't make a sound no matter what you do | Stay completely silent while your partner controls your toy | 5 | text: "Stay completely silent while your partner controls your toy" / partner: "Have a session where your partner can't make a sound no matter what you do" |
|
||||
| 7 | Tell your partner exactly when they're allowed to finish - down to the minute | Wait until the exact moment your partner gives you permission | 5 | text: "Wait until the exact moment your partner gives you permission" / partner: "Tell your partner exactly when they're allowed to finish - down to the minute" |
|
||||
| 8 | Make your partner keep a toy in while doing something mundane on video | Do a normal activity on camera while wearing a toy inside you | 4 | text: "Do a normal activity on camera while wearing a toy inside you" / partner: "Make your partner keep a toy in while doing something mundane on video" |
|
||||
| 9 | Give your partner an assignment to edge a specific number of times before bed | Edge the exact number of times your partner told you to | 5 | text: "Edge the exact number of times your partner told you to" / partner: "Give your partner an assignment to edge a specific number of times before bed" |
|
||||
| 10 | Control your partner's toy while they try to have a normal phone conversation with you | Try to talk normally while your partner controls your toy | 4 | text: "Try to talk normally while your partner controls your toy" / partner: "Control your partner's toy while they try to have a normal phone conversation with you" |
|
||||
| 11 | Make your partner beg over text before you'll let them touch themselves | Beg your partner for permission to touch yourself | 5 | text: "Beg your partner for permission to touch yourself" / partner: "Make your partner beg over text before you'll let them touch themselves" |
|
||||
| 12 | Set a time your partner must be ready and waiting on video | Be ready and waiting at the exact time your partner specified | 4 | text: "Be ready and waiting at the exact time your partner specified" / partner: "Set a time your partner must be ready and waiting on video" |
|
||||
| 13 | Make your partner wear a remote toy during dinner with friends | Wear a toy at dinner while your partner teases you from afar | 5 | text: "Wear a toy at dinner while your partner teases you from afar" / partner: "Make your partner wear a remote toy during dinner with friends" |
|
||||
| 14 | Have your partner send a video proving they followed your instructions | Send video evidence that you did what your partner asked | 5 | text: "Send video evidence that you did what your partner asked" / partner: "Have your partner send a video proving they followed your instructions" |
|
||||
| 15 | Control your partner's toy and stop right before they finish - repeatedly | Be brought to the edge over and over without release | 5 | text: "Be brought to the edge over and over without release" / partner: "Control your partner's toy and stop right before they finish - repeatedly" |
|
||||
| 16 | Make your partner use a toy on themselves while you watch and give commands | Use a toy on yourself while following your partner's instructions | 4 | text: "Use a toy on yourself while following your partner's instructions" / partner: "Make your partner use a toy on themselves while you watch and give commands" |
|
||||
| 17 | Surprise your partner with a remote toy buzz during a work call | Try to keep composure while your partner surprises you mid-meeting | 4 | text: "Try to keep composure while your partner surprises you mid-meeting" / partner: "Surprise your partner with a remote toy buzz during a work call" |
|
||||
| 18 | Make your partner edge themselves daily and report back to you | Edge yourself each day and send proof to your partner | 5 | text: "Edge yourself each day and send proof to your partner" / partner: "Make your partner edge themselves daily and report back to you" |
|
||||
| 19 | Deny your partner for an entire day, building anticipation for the call | Go a full day without touching yourself before your partner lets you | 5 | text: "Go a full day without touching yourself before your partner lets you" / partner: "Deny your partner for an entire day, building anticipation for the call" |
|
||||
| 20 | Make your partner describe every sensation while you control their toy | Narrate what you're feeling while your partner controls your toy | 4 | text: "Narrate what you're feeling while your partner controls your toy" / partner: "Make your partner describe every sensation while you control their toy" |
|
||||
| 21 | Control all of your partner's pleasure for an entire video session | Give up all control and let your partner run the session | 5 | text: "Give up all control and let your partner run the session" / partner: "Control all of your partner's pleasure for an entire video session" |
|
||||
| 22 | Tell your partner what to wear under their clothes for the day | Wear what your partner chose for you under your clothes | 4 | text: "Wear what your partner chose for you under your clothes" / partner: "Tell your partner what to wear under their clothes for the day" |
|
||||
| 23 | Make your partner record themselves following your explicit instructions | Record yourself doing exactly what your partner tells you | 5 | text: "Record yourself doing exactly what your partner tells you" / partner: "Make your partner record themselves following your explicit instructions" |
|
||||
| 24 | Count your partner's edges on video and decide when they've earned release | Count your edges out loud while your partner decides when you're done | 5 | text: "Count your edges out loud while your partner decides when you're done" / partner: "Count your partner's edges on video and decide when they've earned release" |
|
||||
| 25 | Make your partner wear a plug they chose for you throughout the day | Wear a plug all day that your partner picked out | 5 | text: "Wear a plug all day that your partner picked out" / partner: "Make your partner wear a plug they chose for you throughout the day" |
|
||||
| 26 | Give your partner a list of tasks to complete before your next video call | Complete tasks your partner assigns before you can call them | 5 | text: "Complete tasks your partner assigns before you can call them" / partner: "Give your partner a list of tasks to complete before your next video call" |
|
||||
| 27 | Make your partner ask permission via text before they're allowed to orgasm | Ask your partner for permission before you're allowed to come | 5 | text: "Ask your partner for permission before you're allowed to come" / partner: "Make your partner ask permission via text before they're allowed to orgasm" |
|
||||
| 28 | Edge your partner with a remote toy three times before letting them finish | Be edged remotely three times before your partner allows release | 5 | text: "Be edged remotely three times before your partner allows release" / partner: "Edge your partner with a remote toy three times before letting them finish" |
|
||||
| 29 | Make your partner wear a remote toy while they run errands | Wear a toy in public while your partner teases you randomly | 4 | text: "Wear a toy in public while your partner teases you randomly" / partner: "Make your partner wear a remote toy while they run errands" |
|
||||
| 30 | Control your partner's remote vibrator while on a video call | Wear a remote toy while your partner watches and controls it | 4 | text: "Wear a remote toy while your partner watches and controls it" / partner: "Control your partner's remote vibrator while on a video call" |
|
||||
|
||||
# Public Thrills (88 missing)
|
||||
|
||||
## 🔞 All Eyes On Us (28 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Have sex with your partner in the ocean or a lake | Have sex in the water with your partner | 5 | text: "Have sex in the water with your partner" / partner: "Have sex with your partner in the ocean or a lake" |
|
||||
| 2 | Have sex on your apartment balcony | Get taken on a balcony in view of other buildings | 5 | text: "Get taken on a balcony in view of other buildings" / partner: "Have sex on your apartment balcony" |
|
||||
| 3 | Have sex in a private room at a club or lounge | Sneak off to a back room and fuck at a venue | 4 | text: "Sneak off to a back room and fuck at a venue" / partner: "Have sex in a private room at a club or lounge" |
|
||||
| 4 | Make your partner finish while other people are in the next room | Try not to make a sound while finishing with people nearby | 4 | text: "Try not to make a sound while finishing with people nearby" / partner: "Make your partner finish while other people are in the next room" |
|
||||
| 5 | Make your partner beg for it quietly while in a public place | Beg your partner to fuck you while trying to stay quiet | 5 | text: "Beg your partner to fuck you while trying to stay quiet" / partner: "Make your partner beg for it quietly while in a public place" |
|
||||
| 6 | Have sex at a beach at night when no one is around | Have sex on the sand under the stars | 5 | text: "Have sex on the sand under the stars" / partner: "Have sex at a beach at night when no one is around" |
|
||||
| 7 | Have sex with your partner in front of a mirror in a semi-public place | Watch yourself have sex in a risky location | 5 | text: "Watch yourself have sex in a risky location" / partner: "Have sex with your partner in front of a mirror in a semi-public place" |
|
||||
| 8 | Have sex with your partner in an Airbnb with thin walls | Have loud sex knowing the neighbors can hear | 4 | text: "Have loud sex knowing the neighbors can hear" / partner: "Have sex with your partner in an Airbnb with thin walls" |
|
||||
| 9 | Have sex with your partner in a secluded part of a hiking trail | Have sex on a trail where a hiker could walk by | 5 | text: "Have sex on a trail where a hiker could walk by" / partner: "Have sex with your partner in a secluded part of a hiking trail" |
|
||||
| 10 | Give your partner a handjob under a blanket on a flight | Get jerked off on a plane under a blanket | 5 | text: "Get jerked off on a plane under a blanket" / partner: "Give your partner a handjob under a blanket on a flight" |
|
||||
| 11 | Have your partner go down on you in a parked car | Give road head while parked somewhere semi-private | 5 | text: "Give road head while parked somewhere semi-private" / partner: "Have your partner go down on you in a parked car" |
|
||||
| 12 | Have your partner finish while sitting at a bar with a remote toy | Cum at a bar while your partner controls your toy | 5 | text: "Cum at a bar while your partner controls your toy" / partner: "Have your partner finish while sitting at a bar with a remote toy" |
|
||||
| 13 | Have your partner wear a vibrator while meeting your friends | Wear a vibrator around your partner's friends while they control it | 5 | text: "Wear a vibrator around your partner's friends while they control it" / partner: "Have your partner wear a vibrator while meeting your friends" |
|
||||
| 14 | Have sex in a hotel room with the curtains open at night | Get fucked with the curtains open for anyone to see | 5 | text: "Get fucked with the curtains open for anyone to see" / partner: "Have sex in a hotel room with the curtains open at night" |
|
||||
| 15 | Have sex with your partner in a bathroom at a house party | Disappear at a party and come back looking suspicious | 4 | text: "Disappear at a party and come back looking suspicious" / partner: "Have sex with your partner in a bathroom at a house party" |
|
||||
| 16 | Have sex with your partner in a parked car in a secluded spot | Have car sex somewhere you might get caught | 4 | text: "Have car sex somewhere you might get caught" / partner: "Have sex with your partner in a parked car in a secluded spot" |
|
||||
| 17 | Make your partner moan loudly in a hotel knowing the walls are thin | Let the neighbors hear you while your partner makes you moan | 5 | text: "Let the neighbors hear you while your partner makes you moan" / partner: "Make your partner moan loudly in a hotel knowing the walls are thin" |
|
||||
| 18 | Go down on your partner on a balcony at night | Receive oral on a balcony where someone could look up | 5 | text: "Receive oral on a balcony where someone could look up" / partner: "Go down on your partner on a balcony at night" |
|
||||
| 19 | Make your partner finish in a semi-public bathroom | Get off in a public restroom with your partner | 4 | text: "Get off in a public restroom with your partner" / partner: "Make your partner finish in a semi-public bathroom" |
|
||||
| 20 | Have sex with your partner in a hotel pool or hot tub late at night | Fool around in a hotel pool when no one else is around | 5 | text: "Fool around in a hotel pool when no one else is around" / partner: "Have sex with your partner in a hotel pool or hot tub late at night" |
|
||||
| 21 | Bend your partner over in a dressing room | Get bent over in a fitting room by your partner | 5 | text: "Get bent over in a fitting room by your partner" / partner: "Bend your partner over in a dressing room" |
|
||||
| 22 | Have sex with your partner against a window at night | Be taken against a window where someone could see | 5 | text: "Be taken against a window where someone could see" / partner: "Have sex with your partner against a window at night" |
|
||||
| 23 | Get your partner off in the back row of an empty theater | Finish in a movie theater while your partner helps | 4 | text: "Finish in a movie theater while your partner helps" / partner: "Get your partner off in the back row of an empty theater" |
|
||||
| 24 | Have your partner ride you in the driver's seat in a parking garage | Ride your partner in a parked car | 5 | text: "Ride your partner in a parked car" / partner: "Have your partner ride you in the driver's seat in a parking garage" |
|
||||
| 25 | Have sex on a rooftop at night | Get fucked on a rooftop under the sky | 5 | text: "Get fucked on a rooftop under the sky" / partner: "Have sex on a rooftop at night" |
|
||||
| 26 | Go down on your partner while they're on a phone call | Take a call while your partner goes down on you | 4 | text: "Take a call while your partner goes down on you" / partner: "Go down on your partner while they're on a phone call" |
|
||||
| 27 | Have sex in a tent at a campsite where others might hear | Try to stay quiet while your partner fucks you in a tent | 5 | text: "Try to stay quiet while your partner fucks you in a tent" / partner: "Have sex in a tent at a campsite where others might hear" |
|
||||
| 28 | Have sex in your childhood bedroom while visiting family | Fool around in your partner's family home | 4 | text: "Fool around in your partner's family home" / partner: "Have sex in your childhood bedroom while visiting family" |
|
||||
|
||||
## 🔞 Pushing Limits (30 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Unbutton one too many buttons on your shirt for your partner at an event | Catch your partner showing extra skin just for you | 3 | text: "Catch your partner showing extra skin just for you" / partner: "Unbutton one too many buttons on your shirt for your partner at an event" |
|
||||
| 2 | Control your partner's remote toy while they order at a counter | Try to order food while your partner buzzes you | 4 | text: "Try to order food while your partner buzzes you" / partner: "Control your partner's remote toy while they order at a counter" |
|
||||
| 3 | Have a quickie makeout in a hotel hallway | Make out with your partner before you even get to the room | 2 | text: "Make out with your partner before you even get to the room" / partner: "Have a quickie makeout in a hotel hallway" |
|
||||
| 4 | Give your partner oral in a locked bathroom at a party | Receive oral while trying to stay quiet at an event | 5 | text: "Receive oral while trying to stay quiet at an event" / partner: "Give your partner oral in a locked bathroom at a party" |
|
||||
| 5 | Make your partner wear a remote vibrator while out to dinner | Wear a vibrator at a restaurant while your partner controls it | 4 | text: "Wear a vibrator at a restaurant while your partner controls it" / partner: "Make your partner wear a remote vibrator while out to dinner" |
|
||||
| 6 | Have your partner sit across from you at a bar while you describe what you'd do if you were alone | Listen to your partner describe exactly what's about to happen | 3 | text: "Listen to your partner describe exactly what's about to happen" / partner: "Have your partner sit across from you at a bar while you describe what you'd do if you were alone" |
|
||||
| 7 | Press your partner against a car and kiss them before getting in | Get kissed against the car before you even leave | 3 | text: "Get kissed against the car before you even leave" / partner: "Press your partner against a car and kiss them before getting in" |
|
||||
| 8 | Take your partner to a secluded spot at a park and make out | Find a hidden spot with your partner for some privacy | 3 | text: "Find a hidden spot with your partner for some privacy" / partner: "Take your partner to a secluded spot at a park and make out" |
|
||||
| 9 | Kiss your partner's neck in the back of a dark venue | Feel your partner's mouth on your neck in public | 3 | text: "Feel your partner's mouth on your neck in public" / partner: "Kiss your partner's neck in the back of a dark venue" |
|
||||
| 10 | Get your partner worked up in a booth at a dimly lit restaurant | Get teased in a booth where no one can see | 4 | text: "Get teased in a booth where no one can see" / partner: "Get your partner worked up in a booth at a dimly lit restaurant" |
|
||||
| 11 | Make your partner sit on your lap at a bar and feel what they do to you | Sit on your partner's lap and feel their reaction | 3 | text: "Sit on your partner's lap and feel their reaction" / partner: "Make your partner sit on your lap at a bar and feel what they do to you" |
|
||||
| 12 | Pull your partner into a coat closet at a party | Get pulled into a closet by your partner at an event | 4 | text: "Get pulled into a closet by your partner at an event" / partner: "Pull your partner into a coat closet at a party" |
|
||||
| 13 | Touch your partner while you're both in a pool or hot tub with others nearby | Feel your partner's hands on you under the water | 4 | text: "Feel your partner's hands on you under the water" / partner: "Touch your partner while you're both in a pool or hot tub with others nearby" |
|
||||
| 14 | Sneak off with your partner at a wedding for a private moment | Disappear with your partner during an event | 3 | text: "Disappear with your partner during an event" / partner: "Sneak off with your partner at a wedding for a private moment" |
|
||||
| 15 | Whisper to your partner exactly what you want them to do to you once you leave | Hear in detail what's coming the moment you get home | 3 | text: "Hear in detail what's coming the moment you get home" / partner: "Whisper to your partner exactly what you want them to do to you once you leave" |
|
||||
| 16 | Touch your partner through their clothes in a parked car | Get felt up in your car before going inside | 4 | text: "Get felt up in your car before going inside" / partner: "Touch your partner through their clothes in a parked car" |
|
||||
| 17 | Touch your partner under the table at a dinner party until they squirm | Try not to react while your partner's hand is between your legs | 4 | text: "Try not to react while your partner's hand is between your legs" / partner: "Touch your partner under the table at a dinner party until they squirm" |
|
||||
| 18 | Grind on your partner on a crowded dance floor | Feel your partner grind against you surrounded by people | 3 | text: "Feel your partner grind against you surrounded by people" / partner: "Grind on your partner on a crowded dance floor" |
|
||||
| 19 | Tease your partner in the back of a cab until you get home | Get worked up in a cab knowing you can't do anything yet | 3 | text: "Get worked up in a cab knowing you can't do anything yet" / partner: "Tease your partner in the back of a cab until you get home" |
|
||||
| 20 | Make out with your partner in a stairwell | Hook up with your partner in a quiet stairwell | 3 | text: "Hook up with your partner in a quiet stairwell" / partner: "Make out with your partner in a stairwell" |
|
||||
| 21 | Touch your partner under a blanket at a friend's movie night | Get touched under a blanket while others are in the room | 4 | text: "Get touched under a blanket while others are in the room" / partner: "Touch your partner under a blanket at a friend's movie night" |
|
||||
| 22 | Give your partner a hickey somewhere that will be hidden by clothes | Get marked by your partner in a semi-public place | 3 | text: "Get marked by your partner in a semi-public place" / partner: "Give your partner a hickey somewhere that will be hidden by clothes" |
|
||||
| 23 | Slip your hand into your partner's pants briefly in a dark corner | Feel your partner's hand slide into your pants somewhere risky | 4 | text: "Feel your partner's hand slide into your pants somewhere risky" / partner: "Slip your hand into your partner's pants briefly in a dark corner" |
|
||||
| 24 | Kiss your partner against a wall in an alley | Get pressed against a wall and kissed outside | 3 | text: "Get pressed against a wall and kissed outside" / partner: "Kiss your partner against a wall in an alley" |
|
||||
| 25 | Touch your partner over their clothes at a house party when no one's looking | Get touched at a party while trying to act normal | 3 | text: "Get touched at a party while trying to act normal" / partner: "Touch your partner over their clothes at a house party when no one's looking" |
|
||||
| 26 | Pull your partner into a dressing room for a heated makeout | Get dragged into a fitting room by your partner | 4 | text: "Get dragged into a fitting room by your partner" / partner: "Pull your partner into a dressing room for a heated makeout" |
|
||||
| 27 | Get handsy with your partner in the back seat of an Uber | Fool around with your partner while someone else drives | 4 | text: "Fool around with your partner while someone else drives" / partner: "Get handsy with your partner in the back seat of an Uber" |
|
||||
| 28 | Have your partner go commando on a date and tease them about it all night | Go without underwear while your partner reminds you constantly | 3 | text: "Go without underwear while your partner reminds you constantly" / partner: "Have your partner go commando on a date and tease them about it all night" |
|
||||
| 29 | Touch your partner under their clothes in a dark movie theater | Feel your partner's hands under your clothes at the movies | 4 | text: "Feel your partner's hands under your clothes at the movies" / partner: "Touch your partner under their clothes in a dark movie theater" |
|
||||
| 30 | Make out with your partner in a bar or club bathroom | Get pulled into a bathroom for a makeout session | 3 | text: "Get pulled into a bathroom for a makeout session" / partner: "Make out with your partner in a bar or club bathroom" |
|
||||
|
||||
## 🔞 Testing the Waters (30 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Graze your partner's back under their shirt while walking together | Feel your partner's fingers on your bare skin in public | 2 | text: "Feel your partner's fingers on your bare skin in public" / partner: "Graze your partner's back under their shirt while walking together" |
|
||||
| 2 | Brush against your partner suggestively while passing them in a crowd | Feel your partner brush against you on purpose | 2 | text: "Feel your partner brush against you on purpose" / partner: "Brush against your partner suggestively while passing them in a crowd" |
|
||||
| 3 | Leave your hand on your partner's inner thigh during a movie | Feel your partner's hand resting dangerously high during a film | 3 | text: "Feel your partner's hand resting dangerously high during a film" / partner: "Leave your hand on your partner's inner thigh during a movie" |
|
||||
| 4 | Tell your partner exactly what you're going to do to them later while at a party | Hear your partner's plans for you while surrounded by people | 2 | text: "Hear your partner's plans for you while surrounded by people" / partner: "Tell your partner exactly what you're going to do to them later while at a party" |
|
||||
| 5 | Make prolonged eye contact with your partner across a room until one of you breaks | Hold eye contact with your partner from across the room | 2 | text: "Hold eye contact with your partner from across the room" / partner: "Make prolonged eye contact with your partner across a room until one of you breaks" |
|
||||
| 6 | Give your partner a look that tells them exactly what you're thinking | Catch your partner giving you that look in public | 2 | text: "Catch your partner giving you that look in public" / partner: "Give your partner a look that tells them exactly what you're thinking" |
|
||||
| 7 | Whisper something sensual to your partner in a crowded place | Hear something sensual whispered in your ear in public | 2 | text: "Hear something sensual whispered in your ear in public" / partner: "Whisper something sensual to your partner in a crowded place" |
|
||||
| 8 | Steal a long kiss with your partner in an elevator before the doors open | Make out in an elevator and stop just before it opens | 3 | text: "Make out in an elevator and stop just before it opens" / partner: "Steal a long kiss with your partner in an elevator before the doors open" |
|
||||
| 9 | Run your fingers along your partner's neck while standing behind them | Feel your partner's touch on your neck in public | 2 | text: "Feel your partner's touch on your neck in public" / partner: "Run your fingers along your partner's neck while standing behind them" |
|
||||
| 10 | Press against your partner from behind while waiting in line | Feel your partner pressed against you in a queue | 3 | text: "Feel your partner pressed against you in a queue" / partner: "Press against your partner from behind while waiting in line" |
|
||||
| 11 | Kiss your partner's neck briefly while hugging them goodbye in front of others | Feel your partner's lips on your neck in a public goodbye | 2 | text: "Feel your partner's lips on your neck in a public goodbye" / partner: "Kiss your partner's neck briefly while hugging them goodbye in front of others" |
|
||||
| 12 | Flirt with your partner like you just met them at a bar | Play along while your partner hits on you like a stranger | 2 | text: "Play along while your partner hits on you like a stranger" / partner: "Flirt with your partner like you just met them at a bar" |
|
||||
| 13 | Bite your lip while looking at your partner across a table full of people | Catch your partner biting their lip at you in front of others | 2 | text: "Catch your partner biting their lip at you in front of others" / partner: "Bite your lip while looking at your partner across a table full of people" |
|
||||
| 14 | Slip your hand into your partner's back pocket while walking | Feel your partner's hand slide into your pocket | 2 | text: "Feel your partner's hand slide into your pocket" / partner: "Slip your hand into your partner's back pocket while walking" |
|
||||
| 15 | Tell your partner to meet you somewhere private in five minutes | Get a message to meet your partner somewhere right now | 3 | text: "Get a message to meet your partner somewhere right now" / partner: "Tell your partner to meet you somewhere private in five minutes" |
|
||||
| 16 | Drag your nails lightly down your partner's arm while talking to friends | Feel your partner's nails on your skin while acting normal | 2 | text: "Feel your partner's nails on your skin while acting normal" / partner: "Drag your nails lightly down your partner's arm while talking to friends" |
|
||||
| 17 | Lean in and smell your partner while complimenting them in public | Have your partner lean in close and breathe you in | 2 | text: "Have your partner lean in close and breathe you in" / partner: "Lean in and smell your partner while complimenting them in public" |
|
||||
| 18 | Dance closely with your partner at a club like no one else exists | Get lost in your partner on the dance floor | 3 | text: "Get lost in your partner on the dance floor" / partner: "Dance closely with your partner at a club like no one else exists" |
|
||||
| 19 | Pin your partner against a wall for a quick kiss when no one's watching | Get pinned and kissed somewhere semi-public | 3 | text: "Get pinned and kissed somewhere semi-public" / partner: "Pin your partner against a wall for a quick kiss when no one's watching" |
|
||||
| 20 | Trace shapes on your partner's palm under the table | Feel your partner's fingers drawing on your hand secretly | 2 | text: "Feel your partner's fingers drawing on your hand secretly" / partner: "Trace shapes on your partner's palm under the table" |
|
||||
| 21 | Whisper to your partner that you're skipping underwear today | Discover your partner is skipping underwear while you're out | 3 | text: "Discover your partner is skipping underwear while you're out" / partner: "Whisper to your partner that you're skipping underwear today" |
|
||||
| 22 | Send your partner a suggestive photo while at an event | Receive a suggestive photo from your partner in public | 3 | text: "Receive a suggestive photo from your partner in public" / partner: "Send your partner a suggestive photo while at an event" |
|
||||
| 23 | Give your partner's backside a playful squeeze | Have your partner playfully squeeze your backside | 2 | text: "Have your partner playfully squeeze your backside" / partner: "Give your partner's backside a playful squeeze" |
|
||||
| 24 | Whisper a romantic fantasy to your partner at a gathering | Hear a romantic fantasy whispered to you in a crowd | 2 | text: "Hear a romantic fantasy whispered to you in a crowd" / partner: "Whisper a romantic fantasy to your partner at a gathering" |
|
||||
| 25 | Play footsie with your partner under the table | Feel your partner's foot teasing yours under the table | 2 | text: "Feel your partner's foot teasing yours under the table" / partner: "Play footsie with your partner under the table" |
|
||||
| 26 | Text your partner your desires while sitting across from them | Receive a suggestive text from your partner while looking at them | 2 | text: "Receive a suggestive text from your partner while looking at them" / partner: "Text your partner your desires while sitting across from them" |
|
||||
| 27 | Kiss your partner somewhere unexpected while out running errands | Get kissed by your partner when you least expect it in public | 2 | text: "Get kissed by your partner when you least expect it in public" / partner: "Kiss your partner somewhere unexpected while out running errands" |
|
||||
| 28 | Rest your hand on your partner's upper thigh at dinner | Feel your partner's hand creep up your thigh at a restaurant | 2 | text: "Feel your partner's hand creep up your thigh at a restaurant" / partner: "Rest your hand on your partner's upper thigh at dinner" |
|
||||
| 29 | Hold your partner's hand under the table while making intense eye contact | Lock eyes with your partner while they hold your hand secretly | 2 | text: "Lock eyes with your partner while they hold your hand secretly" / partner: "Hold your partner's hand under the table while making intense eye contact" |
|
||||
| 30 | Pull your partner into a quick hidden kiss in a store aisle | Get pulled into a secret kiss while shopping | 2 | text: "Get pulled into a secret kiss while shopping" / partner: "Pull your partner into a quick hidden kiss in a store aisle" |
|
||||
|
||||
# Romance & Sensuality (103 missing)
|
||||
|
||||
## 🔞 Date Night Sparks (7 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Give your partner a sensual massage that leads to more | Receive a sensual massage that leads to more | 3 | text: "Receive a sensual massage that leads to more" / partner: "Give your partner a sensual massage that leads to more" |
|
||||
| 2 | Give your partner a lap dance | Receive a lap dance from your partner | 3 | text: "Receive a lap dance from your partner" / partner: "Give your partner a lap dance" |
|
||||
| 3 | Go to dinner and whisper what you want to do later | Hear what your partner wants to do to you later | 3 | text: "Hear what your partner wants to do to you later" / partner: "Go to dinner and whisper what you want to do later" |
|
||||
| 4 | Give your partner a foot massage that travels up their leg | Receive a foot massage that travels up your leg | 3 | text: "Receive a foot massage that travels up your leg" / partner: "Give your partner a foot massage that travels up their leg" |
|
||||
| 5 | Wear something your partner picks out for date night | Pick out what your partner wears for date night | 2 | text: "Pick out what your partner wears for date night" / partner: "Wear something your partner picks out for date night" |
|
||||
| 6 | Give your partner a striptease | Watch your partner do a striptease for you | 3 | text: "Watch your partner do a striptease for you" / partner: "Give your partner a striptease" |
|
||||
| 7 | Plan a surprise date night ending with intimacy | Be surprised with a date night ending in intimacy | 3 | text: "Be surprised with a date night ending in intimacy" / partner: "Plan a surprise date night ending with intimacy" |
|
||||
|
||||
## 🔞 Love Notes (34 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Send a 'thinking about you' text describing exactly what you're thinking | Receive a detailed 'thinking about you' text | 3 | text: "Receive a detailed 'thinking about you' text" / partner: "Send a 'thinking about you' text describing exactly what you're thinking" |
|
||||
| 2 | Write your partner a note about the first time you wanted them | Read about when your partner first wanted you | 2 | text: "Read about when your partner first wanted you" / partner: "Write your partner a note about the first time you wanted them" |
|
||||
| 3 | Send a flirty emoji-only message and have them guess what it means | Decode a flirty emoji message from your partner | 2 | text: "Decode a flirty emoji message from your partner" / partner: "Send a flirty emoji-only message and have them guess what it means" |
|
||||
| 4 | Create a scavenger hunt of notes leading to the bedroom | Follow a scavenger hunt of notes to the bedroom | 3 | text: "Follow a scavenger hunt of notes to the bedroom" / partner: "Create a scavenger hunt of notes leading to the bedroom" |
|
||||
| 5 | Text your partner during work about the lingerie you're wearing | Receive a text about what your partner is wearing underneath | 3 | text: "Receive a text about what your partner is wearing underneath" / partner: "Text your partner during work about the lingerie you're wearing" |
|
||||
| 6 | Write down what you want your partner to do to you and leave it on their pillow | Find instructions on your pillow from your partner | 4 | text: "Find instructions on your pillow from your partner" / partner: "Write down what you want your partner to do to you and leave it on their pillow" |
|
||||
| 7 | Send your partner a playlist of songs that make you think of them sexually | Listen to a sexy playlist your partner made for you | 2 | text: "Listen to a sexy playlist your partner made for you" / partner: "Send your partner a playlist of songs that make you think of them sexually" |
|
||||
| 8 | Leave a note in your partner's pocket or bag with tonight's plans | Find a surprise note about tonight's plans | 2 | text: "Find a surprise note about tonight's plans" / partner: "Leave a note in your partner's pocket or bag with tonight's plans" |
|
||||
| 9 | Write a poem about your physical attraction to your partner | Receive a poem about your partner's attraction to you | 2 | text: "Receive a poem about your partner's attraction to you" / partner: "Write a poem about your physical attraction to your partner" |
|
||||
| 10 | Send dirty talk messages throughout the day to build anticipation | Receive dirty talk messages all day | 4 | text: "Receive dirty talk messages all day" / partner: "Send dirty talk messages throughout the day to build anticipation" |
|
||||
| 11 | Create a 'sexy coupon book' for your partner | Receive a sexy coupon book from your partner | 3 | text: "Receive a sexy coupon book from your partner" / partner: "Create a 'sexy coupon book' for your partner" |
|
||||
| 12 | Write down a secret fantasy and give it to your partner | Receive and read your partner's secret fantasy | 3 | text: "Receive and read your partner's secret fantasy" / partner: "Write down a secret fantasy and give it to your partner" |
|
||||
| 13 | Send your partner voice messages describing your desires | Receive voice messages describing your partner's desires | 3 | text: "Receive voice messages describing your partner's desires" / partner: "Send your partner voice messages describing your desires" |
|
||||
| 14 | Write a list of places you want to have sex and share it | Receive a list of places your partner wants to have sex | 4 | text: "Receive a list of places your partner wants to have sex" / partner: "Write a list of places you want to have sex and share it" |
|
||||
| 15 | Send your partner a countdown of sexy things you'll do when you see them | Receive a sexy countdown from your partner | 3 | text: "Receive a sexy countdown from your partner" / partner: "Send your partner a countdown of sexy things you'll do when you see them" |
|
||||
| 16 | Leave a note on the bathroom mirror describing your favorite intimate moment | Find a note about your partner's favorite intimate moment | 2 | text: "Find a note about your partner's favorite intimate moment" / partner: "Leave a note on the bathroom mirror describing your favorite intimate moment" |
|
||||
| 17 | Write a steamy story starring you and your partner | Read a steamy story your partner wrote about you both | 3 | text: "Read a steamy story your partner wrote about you both" / partner: "Write a steamy story starring you and your partner" |
|
||||
| 18 | Send your partner a suggestive photo during the day | Receive a suggestive photo from your partner | 3 | text: "Receive a suggestive photo from your partner" / partner: "Send your partner a suggestive photo during the day" |
|
||||
| 19 | Write down three things you love about your partner's body | Read three things your partner loves about your body | 2 | text: "Read three things your partner loves about your body" / partner: "Write down three things you love about your partner's body" |
|
||||
| 20 | Text your partner a fantasy you want to try | Receive a fantasy text from your partner | 3 | text: "Receive a fantasy text from your partner" / partner: "Text your partner a fantasy you want to try" |
|
||||
| 21 | Leave a flirty note hidden for your partner to find | Find a flirty hidden note from your partner | 2 | text: "Find a flirty hidden note from your partner" / partner: "Leave a flirty note hidden for your partner to find" |
|
||||
| 22 | Send your partner a sexy text describing what you want to do tonight | Receive a sexy text about tonight | 3 | text: "Receive a sexy text about tonight" / partner: "Send your partner a sexy text describing what you want to do tonight" |
|
||||
| 23 | Write your partner a love letter describing what turns you on about them | Receive a love letter about what turns your partner on | 2 | text: "Receive a love letter about what turns your partner on" / partner: "Write your partner a love letter describing what turns you on about them" |
|
||||
| 24 | Write down a new position you want to try and slip it to your partner | Receive a suggestion for a new position to try | 4 | text: "Receive a suggestion for a new position to try" / partner: "Write down a new position you want to try and slip it to your partner" |
|
||||
| 25 | Send your partner a teasing text and make them wait for the follow-up | Receive a teasing text with anticipation | 3 | text: "Receive a teasing text with anticipation" / partner: "Send your partner a teasing text and make them wait for the follow-up" |
|
||||
| 26 | Write your partner a note about what you'd do if they walked in right now | Find a note about what would happen if you walked in | 3 | text: "Find a note about what would happen if you walked in" / partner: "Write your partner a note about what you'd do if they walked in right now" |
|
||||
| 27 | Send a 'can't wait to see you' text with explicit details | Receive an explicit 'can't wait' text | 4 | text: "Receive an explicit 'can't wait' text" / partner: "Send a 'can't wait to see you' text with explicit details" |
|
||||
| 28 | Write down your top 5 favorite things your partner does in bed | Read your partner's top 5 favorite things you do in bed | 3 | text: "Read your partner's top 5 favorite things you do in bed" / partner: "Write down your top 5 favorite things your partner does in bed" |
|
||||
| 29 | Send your partner a gif or meme that expresses how much you want them | Receive a flirty gif from your partner | 2 | text: "Receive a flirty gif from your partner" / partner: "Send your partner a gif or meme that expresses how much you want them" |
|
||||
| 30 | Write a note thanking your partner for an amazing sexual experience | Receive a thank-you note for an amazing experience | 2 | text: "Receive a thank-you note for an amazing experience" / partner: "Write a note thanking your partner for an amazing sexual experience" |
|
||||
| 31 | Send your partner a 'remember when we...' text about a hot memory | Reminisce about a hot memory via text | 3 | text: "Reminisce about a hot memory via text" / partner: "Send your partner a 'remember when we...' text about a hot memory" |
|
||||
| 32 | Leave a note with one word describing what you want tonight | Find a one-word hint about tonight | 3 | text: "Find a one-word hint about tonight" / partner: "Leave a note with one word describing what you want tonight" |
|
||||
| 33 | Write a letter about how your partner makes you feel desired | Receive a letter about how you make your partner feel desired | 2 | text: "Receive a letter about how you make your partner feel desired" / partner: "Write a letter about how your partner makes you feel desired" |
|
||||
| 34 | Send your partner a 'this made me think of you' message with something sexy | Receive a sexy 'this made me think of you' message | 2 | text: "Receive a sexy 'this made me think of you' message" / partner: "Send your partner a 'this made me think of you' message with something sexy" |
|
||||
|
||||
## ✨ Romantic Gestures (28 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Wash your partner in a warm bath | Be washed by your partner in a warm bath | 2 | text: "Be washed by your partner in a warm bath" / partner: "Wash your partner in a warm bath" |
|
||||
| 2 | Greet your partner wearing only their favorite outfit or nothing | Be greeted by your partner wearing only your favorite outfit or nothing | 3 | text: "Be greeted by your partner wearing only your favorite outfit or nothing" / partner: "Greet your partner wearing only their favorite outfit or nothing" |
|
||||
| 3 | Trace your fingers along your partner's body | Let your partner trace their fingers along your body | 2 | text: "Let your partner trace their fingers along your body" / partner: "Trace your fingers along your partner's body" |
|
||||
| 4 | Surprise your partner with a passionate kiss | Be surprised with a passionate kiss | 2 | text: "Be surprised with a passionate kiss" / partner: "Surprise your partner with a passionate kiss" |
|
||||
| 5 | Carry your partner to bed and worship their body | Be carried to bed and have your body worshipped | 3 | text: "Be carried to bed and have your body worshipped" / partner: "Carry your partner to bed and worship their body" |
|
||||
| 6 | Slowly kiss from your partner's lips down to their collarbone | Be kissed from your lips down to your collarbone | 3 | text: "Be kissed from your lips down to your collarbone" / partner: "Slowly kiss from your partner's lips down to their collarbone" |
|
||||
| 7 | Kiss your partner's forehead and tell them they're safe with you | Be kissed on the forehead and told you're safe | 2 | text: "Be kissed on the forehead and told you're safe" / partner: "Kiss your partner's forehead and tell them they're safe with you" |
|
||||
| 8 | Pull your partner onto your lap and kiss them | Be pulled onto your partner's lap | 3 | text: "Be pulled onto your partner's lap" / partner: "Pull your partner onto your lap and kiss them" |
|
||||
| 9 | Tell your partner you find them sexy while doing something mundane | Be told you're sexy during something mundane | 2 | text: "Be told you're sexy during something mundane" / partner: "Tell your partner you find them sexy while doing something mundane" |
|
||||
| 10 | Cup your partner's face and kiss them deeply before saying goodbye | Be kissed deeply before your partner leaves | 2 | text: "Be kissed deeply before your partner leaves" / partner: "Cup your partner's face and kiss them deeply before saying goodbye" |
|
||||
| 11 | Take your partner's hand and lead them to bed wordlessly | Be led to bed wordlessly | 3 | text: "Be led to bed wordlessly" / partner: "Take your partner's hand and lead them to bed wordlessly" |
|
||||
| 12 | Hold your partner from behind and kiss their neck while they do something | Be held from behind and kissed on the neck | 3 | text: "Be held from behind and kissed on the neck" / partner: "Hold your partner from behind and kiss their neck while they do something" |
|
||||
| 13 | Watch your partner get dressed and tell them how attractive they are | Be watched getting dressed and complimented | 2 | text: "Be watched getting dressed and complimented" / partner: "Watch your partner get dressed and tell them how attractive they are" |
|
||||
| 14 | Give your partner a full body massage ending wherever they want | Receive a full body massage that ends wherever you want | 3 | text: "Receive a full body massage that ends wherever you want" / partner: "Give your partner a full body massage ending wherever they want" |
|
||||
| 15 | Brush your partner's hair and tell them how beautiful they are | Have your partner brush your hair and compliment you | 2 | text: "Have your partner brush your hair and compliment you" / partner: "Brush your partner's hair and tell them how beautiful they are" |
|
||||
| 16 | Slowly apply lotion to your partner's whole body | Have lotion applied slowly to your whole body | 3 | text: "Have lotion applied slowly to your whole body" / partner: "Slowly apply lotion to your partner's whole body" |
|
||||
| 17 | Run a warm washcloth over your partner after intimacy | Be cared for after intimacy with a warm washcloth | 2 | text: "Be cared for after intimacy with a warm washcloth" / partner: "Run a warm washcloth over your partner after intimacy" |
|
||||
| 18 | Seduce your partner in a dimly lit room | Be seduced in a dimly lit room | 3 | text: "Be seduced in a dimly lit room" / partner: "Seduce your partner in a dimly lit room" |
|
||||
| 19 | Serve your partner breakfast in bed while naked | Be served breakfast in bed by your naked partner | 3 | text: "Be served breakfast in bed by your naked partner" / partner: "Serve your partner breakfast in bed while naked" |
|
||||
| 20 | Lead your partner to the bedroom for a surprise | Be led to the bedroom for a surprise | 2 | text: "Be led to the bedroom for a surprise" / partner: "Lead your partner to the bedroom for a surprise" |
|
||||
| 21 | Pull your partner close unexpectedly and tell them you want them | Be pulled close and told you're wanted | 3 | text: "Be pulled close and told you're wanted" / partner: "Pull your partner close unexpectedly and tell them you want them" |
|
||||
| 22 | Give your partner a lingering kiss on the neck | Receive a lingering kiss on your neck | 3 | text: "Receive a lingering kiss on your neck" / partner: "Give your partner a lingering kiss on the neck" |
|
||||
| 23 | Run your fingers through your partner's hair while they rest on you | Have your hair played with while resting on your partner | 2 | text: "Have your hair played with while resting on your partner" / partner: "Run your fingers through your partner's hair while they rest on you" |
|
||||
| 24 | Hold your partner's face and tell them what you love about them before kissing | Have your partner hold your face and shower you with love | 2 | text: "Have your partner hold your face and shower you with love" / partner: "Hold your partner's face and tell them what you love about them before kissing" |
|
||||
| 25 | Wake your partner up with kisses all over their body | Be woken up with kisses all over your body | 3 | text: "Be woken up with kisses all over your body" / partner: "Wake your partner up with kisses all over their body" |
|
||||
| 26 | Kiss every part of your partner's body | Have every part of your body kissed | 3 | text: "Have every part of your body kissed" / partner: "Kiss every part of your partner's body" |
|
||||
| 27 | Undress your partner slowly while maintaining eye contact | Be slowly undressed while looking into your partner's eyes | 3 | text: "Be slowly undressed while looking into your partner's eyes" / partner: "Undress your partner slowly while maintaining eye contact" |
|
||||
| 28 | Whisper something sexy in your partner's ear in public | Have your partner whisper something sexy in your ear | 3 | text: "Have your partner whisper something sexy in your ear" / partner: "Whisper something sexy in your partner's ear in public" |
|
||||
|
||||
## 🔞 Sensual Touch (34 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Breathe warmly on your partner's neck before kissing it | Feel warm breath on your neck before being kissed | 3 | text: "Feel warm breath on your neck before being kissed" / partner: "Breathe warmly on your partner's neck before kissing it" |
|
||||
| 2 | Use silk or satin fabric to touch your partner's skin | Be touched with silk or satin on your skin | 3 | text: "Be touched with silk or satin on your skin" / partner: "Use silk or satin fabric to touch your partner's skin" |
|
||||
| 3 | Give your partner a foot massage with intention | Receive an intentional foot massage | 2 | text: "Receive an intentional foot massage" / partner: "Give your partner a foot massage with intention" |
|
||||
| 4 | Lightly scratch your partner's back during intimacy | Have your back scratched during intimacy | 3 | text: "Have your back scratched during intimacy" / partner: "Lightly scratch your partner's back during intimacy" |
|
||||
| 5 | Use your lips to explore your partner's body without using hands | Be explored with only your partner's lips | 3 | text: "Be explored with only your partner's lips" / partner: "Use your lips to explore your partner's body without using hands" |
|
||||
| 6 | Trace your fingernails lightly down your partner's back | Have fingernails traced lightly down your back | 3 | text: "Have fingernails traced lightly down your back" / partner: "Trace your fingernails lightly down your partner's back" |
|
||||
| 7 | Tease your partner everywhere except where they want it most | Be teased everywhere except where you want it most | 4 | text: "Be teased everywhere except where you want it most" / partner: "Tease your partner everywhere except where they want it most" |
|
||||
| 8 | Massage your partner's inner thighs without going further | Have your inner thighs massaged without going further | 3 | text: "Have your inner thighs massaged without going further" / partner: "Massage your partner's inner thighs without going further" |
|
||||
| 9 | Suck and lick your partner's fingers | Have your fingers sucked and licked | 3 | text: "Have your fingers sucked and licked" / partner: "Suck and lick your partner's fingers" |
|
||||
| 10 | Touch your partner and have them rate the sensation | Let your partner touch you and rate the sensation | 3 | text: "Let your partner touch you and rate the sensation" / partner: "Touch your partner and have them rate the sensation" |
|
||||
| 11 | Dry hump your partner slowly | Be dry humped slowly by your partner | 3 | text: "Be dry humped slowly by your partner" / partner: "Dry hump your partner slowly" |
|
||||
| 12 | Kiss and nibble your partner's ears | Have your ears kissed and nibbled | 3 | text: "Have your ears kissed and nibbled" / partner: "Kiss and nibble your partner's ears" |
|
||||
| 13 | Use warming or cooling massage oil on your partner | Experience warming or cooling massage oil | 3 | text: "Experience warming or cooling massage oil" / partner: "Use warming or cooling massage oil on your partner" |
|
||||
| 14 | Give your partner a full body massage with warm oil | Receive a warm oil full body massage | 3 | text: "Receive a warm oil full body massage" / partner: "Give your partner a full body massage with warm oil" |
|
||||
| 15 | Blindfold your partner and use different textures on their body | Be blindfolded and touched with different textures | 3 | text: "Be blindfolded and touched with different textures" / partner: "Blindfold your partner and use different textures on their body" |
|
||||
| 16 | Run ice cubes along your partner's body | Have ice cubes run along your body | 3 | text: "Have ice cubes run along your body" / partner: "Run ice cubes along your partner's body" |
|
||||
| 17 | Use light touch to give your partner shivers | Let your partner give you shivers with light touch | 3 | text: "Let your partner give you shivers with light touch" / partner: "Use light touch to give your partner shivers" |
|
||||
| 18 | Explore your partner's body and discover new sensitive spots | Have your body explored for new sensitive spots | 3 | text: "Have your body explored for new sensitive spots" / partner: "Explore your partner's body and discover new sensitive spots" |
|
||||
| 19 | Blindfold your partner and make them guess what you're touching them with | Guess what you're being touched with while blindfolded | 3 | text: "Guess what you're being touched with while blindfolded" / partner: "Blindfold your partner and make them guess what you're touching them with" |
|
||||
| 20 | Use a feather to tease your partner all over | Be teased with a feather all over | 3 | text: "Be teased with a feather all over" / partner: "Use a feather to tease your partner all over" |
|
||||
| 21 | Use your breath to stimulate your partner without touching | Be stimulated by your partner's breath without touch | 3 | text: "Be stimulated by your partner's breath without touch" / partner: "Use your breath to stimulate your partner without touching" |
|
||||
| 22 | Caress your partner's face while looking into their eyes | Have your face caressed while looking into your partner's eyes | 2 | text: "Have your face caressed while looking into your partner's eyes" / partner: "Caress your partner's face while looking into their eyes" |
|
||||
| 23 | Massage your partner's shoulders while they sit between your legs | Sit between your partner's legs while they massage you | 2 | text: "Sit between your partner's legs while they massage you" / partner: "Massage your partner's shoulders while they sit between your legs" |
|
||||
| 24 | Use edible body paint on your partner | Have edible body paint used on you | 4 | text: "Have edible body paint used on you" / partner: "Use edible body paint on your partner" |
|
||||
| 25 | Give your partner goosebumps by whispering and touching | Get goosebumps from whispers and touch | 3 | text: "Get goosebumps from whispers and touch" / partner: "Give your partner goosebumps by whispering and touching" |
|
||||
| 26 | Alternate between firm and gentle touches on your partner's body | Experience alternating firm and gentle touches | 3 | text: "Experience alternating firm and gentle touches" / partner: "Alternate between firm and gentle touches on your partner's body" |
|
||||
| 27 | Touch your partner through their clothes before undressing them | Be touched through your clothes before being undressed | 3 | text: "Be touched through your clothes before being undressed" / partner: "Touch your partner through their clothes before undressing them" |
|
||||
| 28 | Run your hands through your partner's hair and grip gently | Have your hair played with and gripped gently | 3 | text: "Have your hair played with and gripped gently" / partner: "Run your hands through your partner's hair and grip gently" |
|
||||
| 29 | Kiss the inside of your partner's wrist | Have the inside of your wrist kissed | 2 | text: "Have the inside of your wrist kissed" / partner: "Kiss the inside of your partner's wrist" |
|
||||
| 30 | Use a massage candle on your partner's body | Have warm massage candle wax used on your body | 3 | text: "Have warm massage candle wax used on your body" / partner: "Use a massage candle on your partner's body" |
|
||||
| 31 | Apply lotion to your partner slowly and sensually | Have lotion applied to you slowly | 3 | text: "Have lotion applied to you slowly" / partner: "Apply lotion to your partner slowly and sensually" |
|
||||
| 32 | Use your tongue to trace patterns on your partner's skin | Have patterns traced on your skin with a tongue | 4 | text: "Have patterns traced on your skin with a tongue" / partner: "Use your tongue to trace patterns on your partner's skin" |
|
||||
| 33 | Give your partner a scalp massage that turns sensual | Receive a scalp massage that turns sensual | 2 | text: "Receive a scalp massage that turns sensual" / partner: "Give your partner a scalp massage that turns sensual" |
|
||||
| 34 | Hold your partner's hands above their head while kissing them | Have your hands held above your head while being kissed | 3 | text: "Have your hands held above your head while being kissed" / partner: "Hold your partner's hands above their head while kissing them" |
|
||||
|
||||
# Spicy Challenges (56 missing)
|
||||
|
||||
## 🔞 Dare to Play (22 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Perform a striptease for your partner | Watch your partner do a striptease | 3 | text: "Watch your partner do a striptease" / partner: "Perform a striptease for your partner" |
|
||||
| 2 | Use only your mouth to pleasure your partner | Be pleasured using only your partner's mouth | 4 | text: "Be pleasured using only your partner's mouth" / partner: "Use only your mouth to pleasure your partner" |
|
||||
| 3 | Make your partner orgasm twice in one session | Have your partner make you orgasm twice | 5 | text: "Have your partner make you orgasm twice" / partner: "Make your partner orgasm twice in one session" |
|
||||
| 4 | Initiate sex at an unexpected time | Be surprised by your partner initiating sex | 4 | text: "Be surprised by your partner initiating sex" / partner: "Initiate sex at an unexpected time" |
|
||||
| 5 | Make your partner beg for what they want | Let your partner make you beg for what you want | 4 | text: "Let your partner make you beg for what you want" / partner: "Make your partner beg for what they want" |
|
||||
| 6 | Tell your partner your dirtiest thought | Hear your partner's dirtiest thought | 3 | text: "Hear your partner's dirtiest thought" / partner: "Tell your partner your dirtiest thought" |
|
||||
| 7 | Send your partner a nude photo unexpectedly | Receive a nude photo from your partner | 4 | text: "Receive a nude photo from your partner" / partner: "Send your partner a nude photo unexpectedly " |
|
||||
| 8 | Surprise your partner with spontaneous oral during a mundane activity | Be surprised with spontaneous oral | 4 | text: "Be surprised with spontaneous oral" / partner: "Surprise your partner with spontaneous oral during a mundane activity" |
|
||||
| 9 | Blindfold your partner and pleasure them | Be blindfolded and pleasured | 4 | text: "Be blindfolded and pleasured" / partner: "Blindfold your partner and pleasure them" |
|
||||
| 10 | Let your partner do whatever they want to you for 15 minutes | Do whatever you want to your partner for 15 minutes | 5 | text: "Do whatever you want to your partner for 15 minutes" / partner: "Let your partner do whatever they want to you for 15 minutes" |
|
||||
| 11 | Describe exactly what you're going to do to your partner, then do it | Listen to what your partner will do, then experience it | 4 | text: "Listen to what your partner will do, then experience it" / partner: "Describe exactly what you're going to do to your partner, then do it" |
|
||||
| 12 | Make your partner cum without using penetration | Have your partner make you cum without penetration | 4 | text: "Have your partner make you cum without penetration" / partner: "Make your partner cum without using penetration" |
|
||||
| 13 | Wake your partner up with oral sex | Be woken up with oral sex | 4 | text: "Be woken up with oral sex" / partner: "Wake your partner up with oral sex" |
|
||||
| 14 | Take control and tell your partner exactly what to do | Let your partner take full control | 4 | text: "Let your partner take full control" / partner: "Take control and tell your partner exactly what to do" |
|
||||
| 15 | Give your partner a slow, teasing handjob or fingering session | Receive a slow, teasing handjob or fingering | 4 | text: "Receive a slow, teasing handjob or fingering" / partner: "Give your partner a slow, teasing handjob or fingering session" |
|
||||
| 16 | Edge your partner three times before letting them finish | Be edged three times before finishing | 4 | text: "Be edged three times before finishing" / partner: "Edge your partner three times before letting them finish" |
|
||||
| 17 | Go down on your partner for at least 10 minutes | Receive oral for at least 10 minutes | 4 | text: "Receive oral for at least 10 minutes" / partner: "Go down on your partner for at least 10 minutes" |
|
||||
| 18 | Give your partner a lap dance in lingerie or underwear | Receive a lap dance | 3 | text: "Receive a lap dance" / partner: "Give your partner a lap dance in lingerie or underwear" |
|
||||
| 19 | Masturbate in front of your partner | Watch your partner masturbate | 4 | text: "Watch your partner masturbate" / partner: "Masturbate in front of your partner" |
|
||||
| 20 | Tease your partner with texts all day, then deliver | Be teased with texts then get the payoff | 4 | text: "Be teased with texts then get the payoff" / partner: "Tease your partner with texts all day, then deliver" |
|
||||
| 21 | Make your partner come using only external stimulation | Be brought to orgasm without any penetration | 3 | text: "Be brought to orgasm without any penetration" / partner: "Make your partner come using only external stimulation" |
|
||||
| 22 | Be brought to orgasm without any penetration | Make your partner cum using only external stimulation | 3 | text: "Make your partner cum using only external stimulation" / partner: "Be brought to orgasm without any penetration" |
|
||||
|
||||
## 🔞 Pleasure Experiments (1 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Slowly run an ice cube down your partner's body | Let your partner trace an ice cube down your body | 2 | text: "Let your partner trace an ice cube down your body" / partner: "Slowly run an ice cube down your partner's body" |
|
||||
|
||||
## 🔞 Tease & Tension (33 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Strip slowly in front of your partner but don't let them touch | Watch your partner strip without being able to touch | 3 | text: "Watch your partner strip without being able to touch" / partner: "Strip slowly in front of your partner but don't let them touch" |
|
||||
| 2 | Whisper what you want to do later in explicit detail | Listen to explicit details of what's coming later | 3 | text: "Listen to explicit details of what's coming later" / partner: "Whisper what you want to do later in explicit detail" |
|
||||
| 3 | Restrain your partner and take your time with them | Be restrained while your partner takes their time | 4 | text: "Be restrained while your partner takes their time" / partner: "Restrain your partner and take your time with them" |
|
||||
| 4 | Use ice on your partner's body while they're restrained | Have ice used on you while restrained | 5 | text: "Have ice used on you while restrained" / partner: "Use ice on your partner's body while they're restrained" |
|
||||
| 5 | Use a vibrator on your partner on the lowest setting | Be teased with a vibrator on the lowest setting | 4 | text: "Be teased with a vibrator on the lowest setting" / partner: "Use a vibrator on your partner on the lowest setting" |
|
||||
| 6 | Blindfold your partner and make them guess what comes next | Be blindfolded and guess what's coming | 4 | text: "Be blindfolded and guess what's coming" / partner: "Blindfold your partner and make them guess what comes next" |
|
||||
| 7 | Maintain eye contact while touching your partner slowly | Maintain eye contact while being touched slowly | 3 | text: "Maintain eye contact while being touched slowly" / partner: "Maintain eye contact while touching your partner slowly" |
|
||||
| 8 | Make your partner beg before you touch them where they want | Let your partner make you beg before being touched | 4 | text: "Let your partner make you beg before being touched" / partner: "Make your partner beg before you touch them where they want" |
|
||||
| 9 | Deny your partner an orgasm until they can't take it anymore | Be denied orgasm until you can't take it anymore | 5 | text: "Be denied orgasm until you can't take it anymore" / partner: "Deny your partner an orgasm until they can't take it anymore" |
|
||||
| 10 | Give your partner a massage that 'accidentally' gets too close | Receive a massage that keeps getting close | 3 | text: "Receive a massage that keeps getting close" / partner: "Give your partner a massage that 'accidentally' gets too close" |
|
||||
| 11 | Whisper dirty things in your partner's ear without touching them | Hear dirty things whispered without being touched | 3 | text: "Hear dirty things whispered without being touched" / partner: "Whisper dirty things in your partner's ear without touching them" |
|
||||
| 12 | Bring your partner to the edge and stop - repeat three times | Be brought to the edge and stopped three times | 4 | text: "Be brought to the edge and stopped three times" / partner: "Bring your partner to the edge and stop - repeat three times" |
|
||||
| 13 | Give your partner a lap dance without letting them touch | Receive a lap dance without being allowed to touch | 3 | text: "Receive a lap dance without being allowed to touch" / partner: "Give your partner a lap dance without letting them touch" |
|
||||
| 14 | Send your partner flirty texts all day building anticipation | Receive flirty texts all day building anticipation | 2 | text: "Receive flirty texts all day building anticipation" / partner: "Send your partner flirty texts all day building anticipation" |
|
||||
| 15 | Touch your partner everywhere except where they want most | Be touched everywhere except where you want most | 4 | text: "Be touched everywhere except where you want most" / partner: "Touch your partner everywhere except where they want most" |
|
||||
| 16 | Use feathers or soft textures to tease all over | Be teased all over with feathers or soft textures | 3 | text: "Be teased all over with feathers or soft textures" / partner: "Use feathers or soft textures to tease all over" |
|
||||
| 17 | Send a countdown of hours until you'll have sex, with teasers | Receive a countdown with teasing updates | 2 | text: "Receive a countdown with teasing updates" / partner: "Send a countdown of hours until you'll have sex, with teasers" |
|
||||
| 18 | Undress your partner one piece at a time over an hour | Be undressed one piece at a time over an hour | 3 | text: "Be undressed one piece at a time over an hour" / partner: "Undress your partner one piece at a time over an hour" |
|
||||
| 19 | Get your partner worked up then leave the room for 5 minutes | Be worked up then left waiting for 5 minutes | 4 | text: "Be worked up then left waiting for 5 minutes" / partner: "Get your partner worked up then leave the room for 5 minutes" |
|
||||
| 20 | Tease your partner during an entire movie and only finish after | Be teased during a whole movie | 4 | text: "Be teased during a whole movie" / partner: "Tease your partner during an entire movie and only finish after" |
|
||||
| 21 | Tell your partner exactly what you want but make them wait | Hear what's coming but have to wait | 3 | text: "Hear what's coming but have to wait" / partner: "Tell your partner exactly what you want but make them wait" |
|
||||
| 22 | Touch yourself in front of your partner but don't let them join | Watch your partner but not be allowed to join | 4 | text: "Watch your partner but not be allowed to join" / partner: "Touch yourself in front of your partner but don't let them join" |
|
||||
| 23 | Hover your body over your partner without making contact | Have your partner hover without making contact | 3 | text: "Have your partner hover without making contact" / partner: "Hover your body over your partner without making contact" |
|
||||
| 24 | Kiss every part of your partner except their lips | Be kissed everywhere except your lips | 3 | text: "Be kissed everywhere except your lips" / partner: "Kiss every part of your partner except their lips" |
|
||||
| 25 | Give oral for 30 seconds, stop, and make them wait | Receive 30 seconds of oral then wait | 4 | text: "Receive 30 seconds of oral then wait" / partner: "Give oral for 30 seconds, stop, and make them wait" |
|
||||
| 26 | Describe everything you're going to do without doing it yet | Listen to everything your partner will do | 3 | text: "Listen to everything your partner will do" / partner: "Describe everything you're going to do without doing it yet" |
|
||||
| 27 | Give your partner oral but stop right before they finish | Receive oral that stops right before you finish | 4 | text: "Receive oral that stops right before you finish" / partner: "Give your partner oral but stop right before they finish" |
|
||||
| 28 | Touch your partner through their clothes for 10 minutes | Be touched through your clothes for 10 minutes | 3 | text: "Be touched through your clothes for 10 minutes" / partner: "Touch your partner through their clothes for 10 minutes" |
|
||||
| 29 | Send your partner a suggestive photo during dinner | Receive a suggestive photo during dinner | 3 | text: "Receive a suggestive photo during dinner" / partner: "Send your partner a suggestive photo during dinner" |
|
||||
| 30 | Kiss your partner's neck and stop when they want more | Have your neck kissed and then teased | 3 | text: "Have your neck kissed and then teased" / partner: "Kiss your partner's neck and stop when they want more" |
|
||||
| 31 | Make your partner ask permission before you give them more | Ask permission before receiving more | 4 | text: "Ask permission before receiving more" / partner: "Make your partner ask permission before you give them more" |
|
||||
| 32 | Use your mouth close to your partner's skin without touching | Feel your partner's mouth hover without touching | 3 | text: "Feel your partner's mouth hover without touching" / partner: "Use your mouth close to your partner's skin without touching" |
|
||||
| 33 | Trace your fingers over your partner's body feather-light | Have feather-light touches traced on your body | 3 | text: "Have feather-light touches traced on your body" / partner: "Trace your fingers over your partner's body feather-light" |
|
||||
|
||||
# The Kink Lab (34 missing)
|
||||
|
||||
## 🔞 Forbidden Fantasy Dates (9 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Use nipple clamps on your partner | Let your partner use nipple clamps on you | 4 | text: "Let your partner use nipple clamps on you" / partner: "Use nipple clamps on your partner" |
|
||||
| 2 | Let your partner use a clitoris clamp on you | Use a clitoris clamp on your partner | 5 | text: "Use a clitoris clamp on your partner" / partner: "Let your partner use a clitoris clamp on you" |
|
||||
| 3 | Finger your partner while wearing black latex gloves | Be fingered by your partner wearing black latex gloves | 3 | text: "Be fingered by your partner wearing black latex gloves" / partner: "Finger your partner while wearing black latex gloves" |
|
||||
| 4 | Edge your partner and deny their orgasm | Let your partner edge you and deny your orgasm | 5 | text: "Let your partner edge you and deny your orgasm" / partner: "Edge your partner and deny their orgasm" |
|
||||
| 5 | Make your partner cum through domination | Let your partner dominate you until you cum | 5 | text: "Let your partner dominate you until you cum" / partner: "Make your partner cum through domination" |
|
||||
| 6 | Blindfold your partner and plug their ears to take control | Be blindfolded with ears plugged while your partner controls you | 5 | text: "Be blindfolded with ears plugged while your partner controls you" / partner: "Blindfold your partner and plug their ears to take control" |
|
||||
| 7 | Talk dirty about breeding your partner | Listen to your partner talk dirty about breeding you | 5 | text: "Listen to your partner talk dirty about breeding you" / partner: "Talk dirty about breeding your partner" |
|
||||
| 8 | Let your partner take total control | Take total control of your partner | 3 | text: "Take total control of your partner" / partner: "Let your partner take total control" |
|
||||
| 9 | Spank your partner firmly | Receive a firm spanking from your partner | 4 | text: "Receive a firm spanking from your partner" / partner: "Spank your partner firmly" |
|
||||
|
||||
## ✨ Kink Discovery (25 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Gag your partner during sex | Let your partner gag you during sex | 5 | text: "Let your partner gag you during sex" / partner: "Gag your partner during sex" |
|
||||
| 2 | Hold your partner down during sex | Be held down by your partner during sex | 4 | text: "Be held down by your partner during sex" / partner: "Hold your partner down during sex" |
|
||||
| 3 | Put a collar on your partner | Wear a collar for your partner | 4 | text: "Wear a collar for your partner" / partner: "Put a collar on your partner" |
|
||||
| 4 | Blindfold your partner during sex | Be blindfolded by your partner during sex | 3 | text: "Be blindfolded by your partner during sex" / partner: "Blindfold your partner during sex" |
|
||||
| 5 | Pull your partner's hair during sex | Let your partner pull your hair during sex | 4 | text: "Let your partner pull your hair during sex" / partner: "Pull your partner's hair during sex" |
|
||||
| 6 | Make your partner ask permission to finish | Ask your partner for permission to finish | 4 | text: "Ask your partner for permission to finish" / partner: "Make your partner ask permission to finish" |
|
||||
| 7 | Place your hands on your partner's throat | Let your partner's hands rest on your throat | 3 | text: "Let your partner's hands rest on your throat" / partner: "Place your hands on your partner's throat" |
|
||||
| 8 | Worship your partner's body with kisses | Have your body worshipped with kisses | 2 | text: "Have your body worshipped with kisses" / partner: "Worship your partner's body with kisses" |
|
||||
| 9 | Make your partner beg for it | Let your partner make you beg for it | 4 | text: "Let your partner make you beg for it" / partner: "Make your partner beg for it" |
|
||||
| 10 | Use playful, naughty names for your partner | Be called playful, naughty names by your partner | 3 | text: "Be called playful, naughty names by your partner" / partner: "Use playful, naughty names for your partner" |
|
||||
| 11 | Swallow your partner's release | Have your partner swallow your release | 3 | text: "Have your partner swallow your release" / partner: "Swallow your partner's release" |
|
||||
| 12 | Finish on your partner's face | Let your partner finish on your face | 4 | text: "Let your partner finish on your face" / partner: "Finish on your partner's face" |
|
||||
| 13 | Pull your partner's hair during sex | Have your hair pulled during sex | 4 | text: "Have your hair pulled during sex" / partner: "Pull your partner's hair during sex" |
|
||||
| 14 | Put a collar on your partner | Wear a collar for your partner | 5 | text: "Wear a collar for your partner" / partner: "Put a collar on your partner" |
|
||||
| 15 | Gag your partner during sex | Be gagged during sex | 5 | text: "Be gagged during sex" / partner: "Gag your partner during sex" |
|
||||
| 16 | Blindfold your partner during sex | Be blindfolded during sex | 3 | text: "Be blindfolded during sex" / partner: "Blindfold your partner during sex" |
|
||||
| 17 | Make your partner ask permission before they can cum | Ask permission before you're allowed to cum | 4 | text: "Ask permission before you're allowed to cum" / partner: "Make your partner ask permission before they can cum" |
|
||||
| 18 | Choke your partner lightly during sex | Be choked lightly during sex | 5 | text: "Be choked lightly during sex" / partner: "Choke your partner lightly during sex" |
|
||||
| 19 | Hold your partner down and fuck them | Be held down and fucked | 5 | text: "Be held down and fucked" / partner: "Hold your partner down and fuck them" |
|
||||
| 20 | Worship your partner's body completely | Have your body worshipped and kissed everywhere | 3 | text: "Have your body worshipped and kissed everywhere" / partner: "Worship your partner's body completely" |
|
||||
| 21 | Make your partner beg for it | Be made to beg for it | 4 | text: "Be made to beg for it" / partner: "Make your partner beg for it" |
|
||||
| 22 | Call your partner a slut or whore during sex | Be called a slut or whore during sex | 5 | text: "Be called a slut or whore during sex" / partner: "Call your partner a slut or whore during sex" |
|
||||
| 23 | Have your partner swallow | Swallow your partner's cum | 4 | text: "Swallow your partner's cum" / partner: "Have your partner swallow" |
|
||||
| 24 | Fuck your partner's face | Have your face fucked | 5 | text: "Have your face fucked" / partner: "Fuck your partner's face" |
|
||||
| 25 | Finish on your partner's face | Have your partner finish on your face | 5 | text: "Have your partner finish on your face" / partner: "Finish on your partner's face" |
|
||||
|
||||
# Toy Time (128 missing)
|
||||
|
||||
## 🔞 Leveling Up Together (45 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Attach a vibrator to your partner's inner thigh while you tease them elsewhere | Have a vibrator strapped to your thigh while your partner explores your body | 3 | text: "Have a vibrator strapped to your thigh while your partner explores your body" / partner: "Attach a vibrator to your partner's inner thigh while you tease them elsewhere" |
|
||||
| 2 | Use a vibrating butt plug on your partner while giving them a slow handjob | Wear a vibrating plug while receiving a teasing handjob | 4 | text: "Wear a vibrating plug while receiving a teasing handjob" / partner: "Use a vibrating butt plug on your partner while giving them a slow handjob" |
|
||||
| 3 | Blindfold your partner and make them guess which toy you're using | Guess the toy while blindfolded as your partner teases you | 3 | text: "Guess the toy while blindfolded as your partner teases you" / partner: "Blindfold your partner and make them guess which toy you're using" |
|
||||
| 4 | Record your partner's reactions while you use a powerful toy on them | Let your partner film your pleasure as they use a toy on you | 4 | text: "Let your partner film your pleasure as they use a toy on you" / partner: "Record your partner's reactions while you use a powerful toy on them" |
|
||||
| 5 | Tie your partner's wrists to the headboard and tease them with a vibrator | Be tied to the headboard and teased with a vibrator | 4 | text: "Be tied to the headboard and teased with a vibrator" / partner: "Tie your partner's wrists to the headboard and tease them with a vibrator" |
|
||||
| 6 | Control the speed of your partner's toy while they try not to make noise | Use a toy while trying to stay quiet as your partner controls it | 4 | text: "Use a toy while trying to stay quiet as your partner controls it" / partner: "Control the speed of your partner's toy while they try not to make noise" |
|
||||
| 7 | Wear a strap-on and let your partner control a remote vibrator in the base | Use a strap-on with a vibrator that your partner controls | 4 | text: "Use a strap-on with a vibrator that your partner controls" / partner: "Wear a strap-on and let your partner control a remote vibrator in the base" |
|
||||
| 8 | Have your partner hold a vibrator on themselves while you penetrate them | Hold a vibrator on yourself while your partner penetrates you | 4 | text: "Hold a vibrator on yourself while your partner penetrates you" / partner: "Have your partner hold a vibrator on themselves while you penetrate them" |
|
||||
| 9 | Use a textured sleeve on your partner while they watch porn together with you | Watch porn together while your partner uses a textured sleeve on you | 4 | text: "Watch porn together while your partner uses a textured sleeve on you" / partner: "Use a textured sleeve on your partner while they watch porn together with you" |
|
||||
| 10 | Alternate between your mouth and a vibrator on your partner without warning | Be teased with unpredictable switches between oral and vibration | 4 | text: "Be teased with unpredictable switches between oral and vibration" / partner: "Alternate between your mouth and a vibrator on your partner without warning" |
|
||||
| 11 | Perform a striptease and make your partner cum with a toy | Watch your partner strip before they make you cum with a toy | 4 | text: "Watch your partner strip before they make you cum with a toy" / partner: "Perform a striptease and make your partner cum with a toy" |
|
||||
| 12 | Use a vibrating prostate massager on your partner during a handjob | Receive a handjob while wearing a vibrating prostate massager | 4 | text: "Receive a handjob while wearing a vibrating prostate massager" / partner: "Use a vibrating prostate massager on your partner during a handjob" |
|
||||
| 13 | Perform oral while using nipple suction toys on your partner | Receive oral while your partner uses nipple suction toys on you | 4 | text: "Receive oral while your partner uses nipple suction toys on you" / partner: "Perform oral while using nipple suction toys on your partner" |
|
||||
| 14 | Use a dildo on your partner to the beat of the music | Be penetrated by a dildo to the beat of the music | 4 | text: "Be penetrated by a dildo to the beat of the music" / partner: "Use a dildo on your partner to the beat of the music" |
|
||||
| 15 | Use a warming lubricant and a glass dildo on your partner | Feel the contrast of warming lube with a cool glass toy inside you | 4 | text: "Feel the contrast of warming lube with a cool glass toy inside you" / partner: "Use a warming lubricant and a glass dildo on your partner" |
|
||||
| 16 | Warm massage oil and a vibrating wand on your partner's entire body | Receive a full-body massage with warm oil and vibrations | 3 | text: "Receive a full-body massage with warm oil and vibrations" / partner: "Warm massage oil and a vibrating wand on your partner's entire body" |
|
||||
| 17 | Insert anal beads into your partner slowly during foreplay | Have anal beads inserted slowly while your partner builds your arousal | 4 | text: "Have anal beads inserted slowly while your partner builds your arousal" / partner: "Insert anal beads into your partner slowly during foreplay" |
|
||||
| 18 | Use a prostate or G-spot vibrator on your partner during oral | Receive oral while your partner stimulates your G-spot or P-spot | 4 | text: "Receive oral while your partner stimulates your G-spot or P-spot" / partner: "Use a prostate or G-spot vibrator on your partner during oral" |
|
||||
| 19 | Blindfold your partner and tease them with ice and a warming vibrator | Be blindfolded and teased with ice and a warming vibrator | 4 | text: "Be blindfolded and teased with ice and a warming vibrator" / partner: "Blindfold your partner and tease them with ice and a warming vibrator" |
|
||||
| 20 | Edge your partner with a wand, bringing them close twice before letting them finish | Be edged with a wand vibrator, brought close twice before release | 4 | text: "Be edged with a wand vibrator, brought close twice before release" / partner: "Edge your partner with a wand, bringing them close twice before letting them finish" |
|
||||
| 21 | Control a remote vibrator inside your partner while out at dinner | Wear a remote-controlled vibrator while your partner teases you at dinner | 4 | text: "Wear a remote-controlled vibrator while your partner teases you at dinner" / partner: "Control a remote vibrator inside your partner while out at dinner" |
|
||||
| 22 | Wear a vibrating cock ring while your partner rides you | Ride your partner while they wear a vibrating cock ring | 4 | text: "Ride your partner while they wear a vibrating cock ring" / partner: "Wear a vibrating cock ring while your partner rides you" |
|
||||
| 23 | Have your partner wear vibrating panties while running errands together | Wear vibrating panties in public while your partner controls the intensity | 4 | text: "Wear vibrating panties in public while your partner controls the intensity" / partner: "Have your partner wear vibrating panties while running errands together" |
|
||||
| 24 | Let your partner control a toy inside you during a video call with friends | Wear a hidden vibrator during a video call while your partner controls it | 4 | text: "Wear a hidden vibrator during a video call while your partner controls it" / partner: "Let your partner control a toy inside you during a video call with friends" |
|
||||
| 25 | Drip massage candle wax on your partner while using a vibrator on them | Feel warm wax drip on your skin while your partner vibrates you | 4 | text: "Feel warm wax drip on your skin while your partner vibrates you" / partner: "Drip massage candle wax on your partner while using a vibrator on them" |
|
||||
| 26 | Let your partner choose the toy and the setting while you lie back | Take full control of which toy and intensity to use on your partner | 4 | text: "Take full control of which toy and intensity to use on your partner" / partner: "Let your partner choose the toy and the setting while you lie back" |
|
||||
| 27 | Take complete control of your partner's pleasure for an entire session | Surrender all control and let your partner decide everything | 5 | text: "Surrender all control and let your partner decide everything" / partner: "Take complete control of your partner's pleasure for an entire session" |
|
||||
| 28 | Use a toy on your partner while lightly pulling their hair | Feel your hair pulled while your partner uses a toy on you | 5 | text: "Feel your hair pulled while your partner uses a toy on you" / partner: "Use a toy on your partner while lightly pulling their hair" |
|
||||
| 29 | Blindfold your partner and make them hold completely still while you tease them | Stay perfectly still while blindfolded as your partner teases you with toys | 5 | text: "Stay perfectly still while blindfolded as your partner teases you with toys" / partner: "Blindfold your partner and make them hold completely still while you tease them" |
|
||||
| 30 | Edge your partner until they're shaking, then finally let them finish | Be edged until you're trembling before your partner lets you come | 5 | text: "Be edged until you're trembling before your partner lets you come" / partner: "Edge your partner until they're shaking, then finally let them finish" |
|
||||
| 31 | Use nipple clamps on your partner while penetrating them with a toy | Wear nipple clamps while your partner penetrates you with a toy | 5 | text: "Wear nipple clamps while your partner penetrates you with a toy" / partner: "Use nipple clamps on your partner while penetrating them with a toy" |
|
||||
| 32 | Put a collar on your partner while pleasuring them with a vibrator | Wear a collar while your partner uses a vibrator on you | 5 | text: "Wear a collar while your partner uses a vibrator on you" / partner: "Put a collar on your partner while pleasuring them with a vibrator" |
|
||||
| 33 | Restrain your partner's hands behind their back while using toys on them | Have your hands bound behind you while your partner pleasures you with toys | 5 | text: "Have your hands bound behind you while your partner pleasures you with toys" / partner: "Restrain your partner's hands behind their back while using toys on them" |
|
||||
| 34 | Make your partner ask permission before they're allowed to orgasm | Ask your partner for permission before you're allowed to come | 5 | text: "Ask your partner for permission before you're allowed to come" / partner: "Make your partner ask permission before they're allowed to orgasm" |
|
||||
| 35 | Blindfold and cuff your partner while using a wand on them | Be blindfolded and cuffed while your partner uses a wand on you | 5 | text: "Be blindfolded and cuffed while your partner uses a wand on you" / partner: "Blindfold and cuff your partner while using a wand on them" |
|
||||
| 36 | Tie your partner's ankles to the bedposts and tease them with toys | Have your ankles tied while your partner teases you with toys | 5 | text: "Have your ankles tied while your partner teases you with toys" / partner: "Tie your partner's ankles to the bedposts and tease them with toys" |
|
||||
| 37 | Edge your partner three times before finally letting them finish | Be edged three times before your partner allows you release | 4 | text: "Be edged three times before your partner allows you release" / partner: "Edge your partner three times before finally letting them finish" |
|
||||
| 38 | Use a toy on your partner from behind while they grip the sheets | Grip the sheets while your partner uses a toy on you from behind | 4 | text: "Grip the sheets while your partner uses a toy on you from behind" / partner: "Use a toy on your partner from behind while they grip the sheets" |
|
||||
| 39 | Use a thrusting toy on your partner while kissing their neck | Feel a thrusting toy while your partner kisses your neck | 4 | text: "Feel a thrusting toy while your partner kisses your neck" / partner: "Use a thrusting toy on your partner while kissing their neck" |
|
||||
| 40 | Make your partner describe what they're feeling while you use a toy on them | Describe every sensation out loud while your partner toys you | 4 | text: "Describe every sensation out loud while your partner toys you" / partner: "Make your partner describe what they're feeling while you use a toy on them" |
|
||||
| 41 | Use a suction toy on your partner while penetrating them | Feel suction stimulation while your partner penetrates you | 4 | text: "Feel suction stimulation while your partner penetrates you" / partner: "Use a suction toy on your partner while penetrating them" |
|
||||
| 42 | Tease your partner with a wand through their underwear until they beg | Be teased through your underwear until you're begging for more | 4 | text: "Be teased through your underwear until you're begging for more" / partner: "Tease your partner with a wand through their underwear until they beg" |
|
||||
| 43 | Use two different vibrators on your partner at the same time | Feel two vibrators stimulating different areas simultaneously | 4 | text: "Feel two vibrators stimulating different areas simultaneously" / partner: "Use two different vibrators on your partner at the same time" |
|
||||
| 44 | Make your partner hold a vibrator on themselves while you watch | Hold a vibrator on yourself while your partner watches | 4 | text: "Hold a vibrator on yourself while your partner watches" / partner: "Make your partner hold a vibrator on themselves while you watch" |
|
||||
| 45 | Use a vibrating plug on your partner while they go down on you | Wear a vibrating plug while giving your partner oral | 4 | text: "Wear a vibrating plug while giving your partner oral" / partner: "Use a vibrating plug on your partner while they go down on you" |
|
||||
|
||||
## 🔞 Starting Out (36 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Give your partner a backrub using a small vibrator | Receive a backrub with a small vibrator | 3 | text: "Receive a backrub with a small vibrator" / partner: "Give your partner a backrub using a small vibrator" |
|
||||
| 2 | Tease your partner with a bullet vibrator over their clothes | Be teased with a bullet vibrator through your clothes | 3 | text: "Be teased with a bullet vibrator through your clothes" / partner: "Tease your partner with a bullet vibrator over their clothes" |
|
||||
| 3 | Use a feather tickler on your blindfolded partner | Be blindfolded and teased with a feather tickler | 3 | text: "Be blindfolded and teased with a feather tickler" / partner: "Use a feather tickler on your blindfolded partner" |
|
||||
| 4 | Have sex using a small dildo on your partner | Be penetrated with a small dildo during sex | 4 | text: "Be penetrated with a small dildo during sex" / partner: "Have sex using a small dildo on your partner" |
|
||||
| 5 | Tease your partner with feathers and a heating massager | Be teased with feathers and a heating massager | 3 | text: "Be teased with feathers and a heating massager" / partner: "Tease your partner with feathers and a heating massager" |
|
||||
| 6 | Press a wand vibrator against your partner during missionary | Have a wand vibrator pressed against you during missionary | 4 | text: "Have a wand vibrator pressed against you during missionary" / partner: "Press a wand vibrator against your partner during missionary" |
|
||||
| 7 | Insert a small butt plug into your partner before you have sex | Have a small butt plug inserted before you have sex | 4 | text: "Have a small butt plug inserted before you have sex" / partner: "Insert a small butt plug into your partner before you have sex" |
|
||||
| 8 | Wear a vibrating egg while you have sex | Have sex while your partner wears a vibrating egg | 4 | text: "Have sex while your partner wears a vibrating egg" / partner: "Wear a vibrating egg while you have sex" |
|
||||
| 9 | Use a clitoral suction toy and a dildo on your partner simultaneously | Be penetrated while your partner uses a suction toy on your clit | 4 | text: "Be penetrated while your partner uses a suction toy on your clit" / partner: "Use a clitoral suction toy and a dildo on your partner simultaneously" |
|
||||
| 10 | Use a textured sleeve on your partner's cock | Have a textured sleeve used on your cock | 4 | text: "Have a textured sleeve used on your cock" / partner: "Use a textured sleeve on your partner's cock" |
|
||||
| 11 | Perform oral while using a small vibrator on your partner | Receive oral while your partner uses a small vibrator on you | 3 | text: "Receive oral while your partner uses a small vibrator on you" / partner: "Perform oral while using a small vibrator on your partner" |
|
||||
| 12 | Blindfold your partner and penetrate them with a textured dildo | Be blindfolded and penetrated with a textured dildo | 4 | text: "Be blindfolded and penetrated with a textured dildo" / partner: "Blindfold your partner and penetrate them with a textured dildo" |
|
||||
| 13 | Blindfold your partner and use a vibrating sleeve on their G-spot or P-spot | Be blindfolded while your partner stimulates your G-spot or P-spot with a vibrating sleeve | 4 | text: "Be blindfolded while your partner stimulates your G-spot or P-spot with a vibrating sleeve" / partner: "Blindfold your partner and use a vibrating sleeve on their G-spot or P-spot" |
|
||||
| 14 | Blindfold your partner and tease their nipples with weighted clamps | Be blindfolded and have weighted clamps placed on your nipples | 5 | text: "Be blindfolded and have weighted clamps placed on your nipples" / partner: "Blindfold your partner and tease their nipples with weighted clamps" |
|
||||
| 15 | Tie your partner's hands with a scarf and kiss their body | Have your hands tied with a scarf while your partner kisses you | 3 | text: "Have your hands tied with a scarf while your partner kisses you" / partner: "Tie your partner's hands with a scarf and kiss their body" |
|
||||
| 16 | Record your partner using a toy on themselves | Let your partner record you using a toy on yourself | 5 | text: "Let your partner record you using a toy on yourself" / partner: "Record your partner using a toy on themselves" |
|
||||
| 17 | Tease your partner with a toy under the covers in bed | Be teased with a toy under the blankets | 4 | text: "Be teased with a toy under the blankets" / partner: "Tease your partner with a toy under the covers in bed" |
|
||||
| 18 | Shop for a new toy together online | Browse and pick out a new toy with your partner | 3 | text: "Browse and pick out a new toy with your partner" / partner: "Shop for a new toy together online" |
|
||||
| 19 | Unbox a new toy together and explore how it feels | Discover a new toy together with your partner | 3 | text: "Discover a new toy together with your partner" / partner: "Unbox a new toy together and explore how it feels" |
|
||||
| 20 | Use a warming massager on your partner's lower back | Feel warmth and vibration on your lower back | 3 | text: "Feel warmth and vibration on your lower back" / partner: "Use a warming massager on your partner's lower back" |
|
||||
| 21 | Use a massage wand on your partner's back and shoulders | Receive a relaxing massage with a vibrating wand | 3 | text: "Receive a relaxing massage with a vibrating wand" / partner: "Use a massage wand on your partner's back and shoulders" |
|
||||
| 22 | Trace a small vibrator slowly along your partner's arms and legs | Feel a vibrator traced gently across your limbs | 3 | text: "Feel a vibrator traced gently across your limbs" / partner: "Trace a small vibrator slowly along your partner's arms and legs" |
|
||||
| 23 | Tease your partner's inner thighs with a bullet vibrator | Feel a bullet vibrator teasing your inner thighs | 3 | text: "Feel a bullet vibrator teasing your inner thighs" / partner: "Tease your partner's inner thighs with a bullet vibrator" |
|
||||
| 24 | Use a vibrator on your partner while giving them a hand massage | Receive a hand massage while feeling vibrations elsewhere | 3 | text: "Receive a hand massage while feeling vibrations elsewhere" / partner: "Use a vibrator on your partner while giving them a hand massage" |
|
||||
| 25 | Give your partner a foot massage using a small vibrator | Receive a foot massage with vibrating sensations | 3 | text: "Receive a foot massage with vibrating sensations" / partner: "Give your partner a foot massage using a small vibrator" |
|
||||
| 26 | Hold a vibrator against your partner while cuddling on the couch | Feel vibrations while cuddling with your partner | 3 | text: "Feel vibrations while cuddling with your partner" / partner: "Hold a vibrator against your partner while cuddling on the couch" |
|
||||
| 27 | Use a vibrating ring during foreplay | Feel the vibrating ring while making out with your partner | 3 | text: "Feel the vibrating ring while making out with your partner" / partner: "Use a vibrating ring during foreplay" |
|
||||
| 28 | Tease your partner with a vibrator while they're still fully clothed | Feel vibrations through your clothes as your partner teases you | 3 | text: "Feel vibrations through your clothes as your partner teases you" / partner: "Tease your partner with a vibrator while they're still fully clothed" |
|
||||
| 29 | Use a bullet vibrator on your partner while kissing them deeply | Be kissed while your partner uses a bullet vibrator on you | 3 | text: "Be kissed while your partner uses a bullet vibrator on you" / partner: "Use a bullet vibrator on your partner while kissing them deeply" |
|
||||
| 30 | Let your partner choose which toy to use on you | Pick a toy from the collection to use on your partner | 3 | text: "Pick a toy from the collection to use on your partner" / partner: "Let your partner choose which toy to use on you" |
|
||||
| 31 | Use a soft vibrator on your partner's neck and collarbone | Feel gentle vibrations on your neck and collarbone | 3 | text: "Feel gentle vibrations on your neck and collarbone" / partner: "Use a soft vibrator on your partner's neck and collarbone" |
|
||||
| 32 | Use a small dildo on your partner while whispering what you want to do to them | Be penetrated with a toy while your partner whispers in your ear | 4 | text: "Be penetrated with a toy while your partner whispers in your ear" / partner: "Use a small dildo on your partner while whispering what you want to do to them" |
|
||||
| 33 | Let your partner guide your hand while you hold a toy on them | Guide your partner's hand to show them where the toy feels best | 3 | text: "Guide your partner's hand to show them where the toy feels best" / partner: "Let your partner guide your hand while you hold a toy on them" |
|
||||
| 34 | Let your partner use a toy on you while you touch yourself | Use a toy on your partner while they pleasure themselves | 4 | text: "Use a toy on your partner while they pleasure themselves" / partner: "Let your partner use a toy on you while you touch yourself" |
|
||||
| 35 | Use a vibrator on your partner while they watch themselves in a mirror | Watch yourself in a mirror while your partner uses a toy on you | 4 | text: "Watch yourself in a mirror while your partner uses a toy on you" / partner: "Use a vibrator on your partner while they watch themselves in a mirror" |
|
||||
| 36 | Use cooling lube with a small toy on your partner | Feel the cool sensation as your partner uses a toy with cooling lube | 4 | text: "Feel the cool sensation as your partner uses a toy with cooling lube" / partner: "Use cooling lube with a small toy on your partner" |
|
||||
|
||||
## ✨ The Deep End (47 missing)
|
||||
|
||||
| # | Current Text | Current Partner Text | Intensity | Inverse to Create |
|
||||
|---|--------------|---------------------|-----------|-------------------|
|
||||
| 1 | Lock your partner in a collar and leash, using toys on them while they kneel | Wear a collar and kneel while your partner uses toys on you | 5 | text: "Wear a collar and kneel while your partner uses toys on you" / partner: "Lock your partner in a collar and leash, using toys on them while they kneel" |
|
||||
| 2 | Restrain your partner in a sex swing and use multiple toys on them | Be suspended in a swing while your partner pleasures you with toys | 5 | text: "Be suspended in a swing while your partner pleasures you with toys" / partner: "Restrain your partner in a sex swing and use multiple toys on them" |
|
||||
| 3 | Use a toy and a vibrating plug on your partner | Be filled by a toy and a vibrating plug | 5 | text: "Be filled by a toy and a vibrating plug" / partner: "Use a toy and a vibrating plug on your partner" |
|
||||
| 4 | Control a remote toy for your partner all day | Wear a remote toy for your partner to control all day | 4 | text: "Wear a remote toy for your partner to control all day" / partner: "Control a remote toy for your partner all day" |
|
||||
| 5 | Suspend your partner's wrists above them and tease their body with a pinwheel and wand | Have your arms suspended while your partner torments you with sensation toys | 5 | text: "Have your arms suspended while your partner torments you with sensation toys" / partner: "Suspend your partner's wrists above them and tease their body with a pinwheel and wand" |
|
||||
| 6 | Use two vibrating toys on your partner at once | Be filled by two vibrating toys at once | 5 | text: "Be filled by two vibrating toys at once" / partner: "Use two vibrating toys on your partner at once" |
|
||||
| 7 | Restrain your partner's legs and use a suction toy | Be restrained while your partner uses a suction toy | 4 | text: "Be restrained while your partner uses a suction toy" / partner: "Restrain your partner's legs and use a suction toy" |
|
||||
| 8 | Use a powerful wand on your partner during penetration | Feel a powerful wand while your partner penetrates you | 4 | text: "Feel a powerful wand while your partner penetrates you" / partner: "Use a powerful wand on your partner during penetration" |
|
||||
| 9 | Slowly expand an inflatable toy inside your partner | Let your partner slowly expand an inflatable toy inside you | 5 | text: "Let your partner slowly expand an inflatable toy inside you" / partner: "Slowly expand an inflatable toy inside your partner" |
|
||||
| 10 | Use sensation pads and a vibrator on your partner | Feel sensation pads and a vibrator on your thighs | 5 | text: "Feel sensation pads and a vibrator on your thighs" / partner: "Use sensation pads and a vibrator on your partner" |
|
||||
| 11 | Use a rhythmic thrusting machine on your partner | Be pleasured by a thrusting machine while your partner controls it | 5 | text: "Be pleasured by a thrusting machine while your partner controls it" / partner: "Use a rhythmic thrusting machine on your partner" |
|
||||
| 12 | Edge your partner five times before allowing them to peak | Let your partner edge you five times before you reach a peak | 4 | text: "Let your partner edge you five times before you reach a peak" / partner: "Edge your partner five times before allowing them to peak" |
|
||||
| 13 | Use a TENS unit on your partner while simultaneously penetrating them with a toy | Feel electrical stimulation while your partner uses a toy inside you | 5 | text: "Feel electrical stimulation while your partner uses a toy inside you" / partner: "Use a TENS unit on your partner while simultaneously penetrating them with a toy" |
|
||||
| 14 | Blindfold your partner and use a sensation wheel | Be blindfolded while your partner uses a sensation wheel | 5 | text: "Be blindfolded while your partner uses a sensation wheel" / partner: "Blindfold your partner and use a sensation wheel" |
|
||||
| 15 | Make your partner ride a mounted dildo while you control a vibrator on them | Ride a dildo while your partner controls additional stimulation | 4 | text: "Ride a dildo while your partner controls additional stimulation" / partner: "Make your partner ride a mounted dildo while you control a vibrator on them" |
|
||||
| 16 | Use a thrusting dildo on your partner while they're restrained and blindfolded | Be bound and blindfolded while a thrusting toy penetrates you | 5 | text: "Be bound and blindfolded while a thrusting toy penetrates you" / partner: "Use a thrusting dildo on your partner while they're restrained and blindfolded" |
|
||||
| 17 | Bring your partner to a peak using only internal toys | Reach a peak using only internal stimulation | 4 | text: "Reach a peak using only internal stimulation" / partner: "Bring your partner to a peak using only internal toys" |
|
||||
| 18 | Use a thrusting machine for anal play while using a wand on your partner | Experience a thrusting machine and wand stimulation simultaneously | 5 | text: "Experience a thrusting machine and wand stimulation simultaneously" / partner: "Use a thrusting machine for anal play while using a wand on your partner" |
|
||||
| 19 | Tie your partner in a hogtie and edge them repeatedly | Be bound in a hogtie while your partner edges you | 5 | text: "Be bound in a hogtie while your partner edges you" / partner: "Tie your partner in a hogtie and edge them repeatedly" |
|
||||
| 20 | Use a sensation wand on your partner's sensitive spots | Feel the tingle of a sensation wand on your body | 5 | text: "Feel the tingle of a sensation wand on your body" / partner: "Use a sensation wand on your partner's sensitive spots" |
|
||||
| 21 | Attach weighted nipple clamps to your partner and use a powerful wand on them | Wear weighted clamps while your partner overwhelms you with a wand | 5 | text: "Wear weighted clamps while your partner overwhelms you with a wand" / partner: "Attach weighted nipple clamps to your partner and use a powerful wand on them" |
|
||||
| 22 | Use an internal massager to tease your partner | Let your partner tease you with an internal massager | 4 | text: "Let your partner tease you with an internal massager" / partner: "Use an internal massager to tease your partner" |
|
||||
| 23 | Restrain your partner and use a vibrator on them | Be restrained while your partner uses a vibrator on you | 5 | text: "Be restrained while your partner uses a vibrator on you" / partner: "Restrain your partner and use a vibrator on them" |
|
||||
| 24 | Tease your partner while they wear a locked toy | Be teased by your partner while wearing a locked toy | 5 | text: "Be teased by your partner while wearing a locked toy" / partner: "Tease your partner while they wear a locked toy" |
|
||||
| 25 | Restrain your partner with a spreader bar and tease them with a wand | Be restrained with a spreader bar while your partner teases you | 5 | text: "Be restrained with a spreader bar while your partner teases you" / partner: "Restrain your partner with a spreader bar and tease them with a wand" |
|
||||
| 26 | Use a large plug and a powerful wand on your partner until they have multiple orgasms | Be filled and vibrated until you orgasm again and again | 5 | text: "Be filled and vibrated until you orgasm again and again" / partner: "Use a large plug and a powerful wand on your partner until they have multiple orgasms" |
|
||||
| 27 | Make your partner count their edges out loud while you control the toy | Count each edge out loud while your partner decides when you've had enough | 5 | text: "Count each edge out loud while your partner decides when you've had enough" / partner: "Make your partner count their edges out loud while you control the toy" |
|
||||
| 28 | Put your partner in predicament bondage where struggling increases toy intensity | Be bound so that every movement makes the vibrations stronger | 5 | text: "Be bound so that every movement makes the vibrations stronger" / partner: "Put your partner in predicament bondage where struggling increases toy intensity" |
|
||||
| 29 | Restrain your partner in a strict position and methodically overstimulate every erogenous zone with toys | Be bound strictly while your partner systematically overwhelms every sensitive part of your body | 5 | text: "Be bound strictly while your partner systematically overwhelms every sensitive part of your body" / partner: "Restrain your partner in a strict position and methodically overstimulate every erogenous zone with toys" |
|
||||
| 30 | Use a hollow plug on your partner so you can penetrate them while they're already filled | Wear a hollow plug while your partner penetrates you through it | 5 | text: "Wear a hollow plug while your partner penetrates you through it" / partner: "Use a hollow plug on your partner so you can penetrate them while they're already filled" |
|
||||
| 31 | Edge your partner for an hour using various toys before allowing release | Be edged with different toys for an extended session before finally coming | 5 | text: "Be edged with different toys for an extended session before finally coming" / partner: "Edge your partner for an hour using various toys before allowing release" |
|
||||
| 32 | Use a large toy on your partner with plenty of lube | Be filled by a large toy with plenty of lube | 4 | text: "Be filled by a large toy with plenty of lube" / partner: "Use a large toy on your partner with plenty of lube" |
|
||||
| 33 | Use a rotating and thrusting toy on your partner while restraining their hips | Have your hips pinned while a toy rotates and thrusts inside you | 5 | text: "Have your hips pinned while a toy rotates and thrusts inside you" / partner: "Use a rotating and thrusting toy on your partner while restraining their hips" |
|
||||
| 34 | Bring your partner to a peak while they are gagged | Let your partner bring you to a peak while gagged | 5 | text: "Let your partner bring you to a peak while gagged" / partner: "Bring your partner to a peak while they are gagged" |
|
||||
| 35 | Use clover clamps on your partner that tighten when pulled while you penetrate them with a toy | Feel clamps tighten with each thrust as your partner toys you | 5 | text: "Feel clamps tighten with each thrust as your partner toys you" / partner: "Use clover clamps on your partner that tighten when pulled while you penetrate them with a toy" |
|
||||
| 36 | Connect nipple clamps with a chain to your partner's collar while using a toy on them | Wear connected clamps and collar while your partner pleasures you with a toy | 5 | text: "Wear connected clamps and collar while your partner pleasures you with a toy" / partner: "Connect nipple clamps with a chain to your partner's collar while using a toy on them" |
|
||||
| 37 | Use a large textured dildo on your partner while they wear a ball gag | Be gagged and stretched by a large textured toy | 5 | text: "Be gagged and stretched by a large textured toy" / partner: "Use a large textured dildo on your partner while they wear a ball gag" |
|
||||
| 38 | Use a thrusting toy on your partner while spanking them | Be penetrated with a thrusting toy while your partner spanks you | 5 | text: "Be penetrated with a thrusting toy while your partner spanks you" / partner: "Use a thrusting toy on your partner while spanking them" |
|
||||
| 39 | Use a pump on your partner to increase sensitivity before using other toys | Have your sensitivity heightened by a pump before your partner uses toys on you | 5 | text: "Have your sensitivity heightened by a pump before your partner uses toys on you" / partner: "Use a pump on your partner to increase sensitivity before using other toys" |
|
||||
| 40 | Tie your partner face down and use a thrusting toy on them from behind | Be bound face down while a toy thrusts into you | 5 | text: "Be bound face down while a toy thrusts into you" / partner: "Tie your partner face down and use a thrusting toy on them from behind" |
|
||||
| 41 | Bind your partner completely immobile and use toys on them until you decide they're done | Be completely restrained while your partner controls all stimulation | 5 | text: "Be completely restrained while your partner controls all stimulation" / partner: "Bind your partner completely immobile and use toys on them until you decide they're done" |
|
||||
| 42 | Strap vibrators to multiple parts of your partner's body and turn them all on at once | Have vibrators attached to several erogenous zones buzzing simultaneously | 5 | text: "Have vibrators attached to several erogenous zones buzzing simultaneously" / partner: "Strap vibrators to multiple parts of your partner's body and turn them all on at once" |
|
||||
| 43 | Strap a wand to your partner and leave it on medium while you do other things to them | Have a wand strapped to you as constant stimulation while your partner plays | 5 | text: "Have a wand strapped to you as constant stimulation while your partner plays" / partner: "Strap a wand to your partner and leave it on medium while you do other things to them" |
|
||||
| 44 | Use electro-stimulation on your partner while they edge themselves with a toy | Shock yourself lightly while edging with a toy at your partner's command | 5 | text: "Shock yourself lightly while edging with a toy at your partner's command" / partner: "Use electro-stimulation on your partner while they edge themselves with a toy" |
|
||||
| 45 | Make your partner edge themselves while you watch and give instructions | Edge yourself under your partner's commands while they watch | 5 | text: "Edge yourself under your partner's commands while they watch" / partner: "Make your partner edge themselves while you watch and give instructions" |
|
||||
| 46 | Blindfold your partner and make them guess how many toys are inside them | Guess how many toys your partner has put inside you while blindfolded | 4 | text: "Guess how many toys your partner has put inside you while blindfolded" / partner: "Blindfold your partner and make them guess how many toys are inside them" |
|
||||
| 47 | Tease your partner right to the edge before stopping | Be teased to the edge before your partner stops | 5 | text: "Be teased to the edge before your partner stops" / partner: "Tease your partner right to the edge before stopping" |
|
||||
@@ -1,256 +0,0 @@
|
||||
# New Question Types Draft - Non-Explicit Packs
|
||||
|
||||
New questions using **audio**, **photo**, and **who_likely** types for all non-explicit question packs. Questions match the tone, intensity range, and theme of each pack.
|
||||
|
||||
**Question types:**
|
||||
- **audio** - User records a voice response (max 60s)
|
||||
- **photo** - User uploads/captures a photo
|
||||
- **who_likely** - "Who is more likely to..." (each partner picks who)
|
||||
|
||||
None of these types use partner_text.
|
||||
|
||||
---
|
||||
|
||||
## 1. Staying Close (Long Distance, intensity 1)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | audio | Record a voice note saying good morning to your partner | 1 |
|
||||
| 2 | audio | Record yourself humming or singing a song that reminds you of your partner | 1 |
|
||||
| 3 | audio | Leave a voice note describing the best part of your day | 1 |
|
||||
| 4 | audio | Record a bedtime voice note for your partner to fall asleep to | 1 |
|
||||
| 5 | photo | Send a photo of your current view right now | 1 |
|
||||
| 6 | photo | Share a photo of something that made you smile today | 1 |
|
||||
| 7 | photo | Take a photo of your pillow and empty bed | 1 |
|
||||
| 8 | photo | Send a photo of your morning coffee or tea | 1 |
|
||||
| 9 | who_likely | Who is more likely to call the other first? | 1 |
|
||||
| 10 | who_likely | Who is more likely to fall asleep on a video call? | 1 |
|
||||
| 11 | who_likely | Who is more likely to send a random "I miss you" text? | 1 |
|
||||
| 12 | who_likely | Who is more likely to plan a surprise visit? | 1 |
|
||||
| 13 | who_likely | Who is more likely to cry at the airport? | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Finding Your Vibe (Social Life, intensity 1)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | who_likely | Who is more likely to be the last one on the dance floor? | 1 |
|
||||
| 2 | who_likely | Who is more likely to make friends with a stranger at a party? | 1 |
|
||||
| 3 | who_likely | Who is more likely to suggest leaving early? | 1 |
|
||||
| 4 | who_likely | Who is more likely to be the designated driver? | 1 |
|
||||
| 5 | who_likely | Who is more likely to start a karaoke session? | 1 |
|
||||
| 6 | who_likely | Who is more likely to cancel plans to stay in? | 1 |
|
||||
| 7 | audio | Describe your perfect night out in a voice note | 1 |
|
||||
| 8 | photo | Share a photo of your go-to going-out outfit | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Weekend Warriors (Adventure & Travel, intensity 1-2)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | who_likely | Who is more likely to suggest an unplanned road trip? | 1 |
|
||||
| 2 | who_likely | Who is more likely to get competitive at mini golf? | 1 |
|
||||
| 3 | who_likely | Who is more likely to try a new restaurant without checking reviews? | 1 |
|
||||
| 4 | who_likely | Who is more likely to spend the entire weekend on the couch? | 1 |
|
||||
| 5 | who_likely | Who is more likely to drag the other to a flea market? | 1 |
|
||||
| 6 | photo | Share a photo of a place nearby you've been wanting to visit together | 1 |
|
||||
| 7 | photo | Take a photo of your ideal weekend morning setup | 1 |
|
||||
| 8 | audio | Describe a weekend adventure you'd love to go on together | 1 |
|
||||
| 9 | audio | Tell your partner about your favorite weekend memory together | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Outdoor Explorers (Adventure & Travel, intensity 1)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | who_likely | Who is more likely to suggest a spontaneous hike? | 1 |
|
||||
| 2 | who_likely | Who is more likely to get lost on a trail? | 1 |
|
||||
| 3 | who_likely | Who is more likely to complain about the weather? | 1 |
|
||||
| 4 | who_likely | Who is more likely to forget the sunscreen? | 1 |
|
||||
| 5 | who_likely | Who would survive longer in the wilderness? | 1 |
|
||||
| 6 | photo | Share a photo of your favorite outdoor spot | 1 |
|
||||
| 7 | photo | Take a photo of the view from your window right now | 1 |
|
||||
| 8 | audio | Describe your dream outdoor adventure in a voice note | 1 |
|
||||
| 9 | audio | Tell your partner about a time in nature that felt magical | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Romantic Gestures (Romance & Sensuality, intensity 2-4)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | audio | Whisper something sweet to your partner | 2 |
|
||||
| 2 | audio | Record yourself saying what you love most about your partner | 2 |
|
||||
| 3 | audio | Tell your partner about a moment with them you'll never forget | 2 |
|
||||
| 4 | audio | In your most romantic voice, tell your partner what they mean to you | 3 |
|
||||
| 5 | audio | Describe the first time you knew you were falling for your partner | 2 |
|
||||
| 6 | photo | Set up a romantic scene at home and share a photo | 2 |
|
||||
| 7 | photo | Write a short love note by hand and send a photo of it | 2 |
|
||||
| 8 | photo | Share a photo from a moment together that makes your heart race | 2 |
|
||||
| 9 | who_likely | Who is more likely to plan a surprise romantic gesture? | 2 |
|
||||
| 10 | who_likely | Who is more likely to initiate a slow dance in the kitchen? | 2 |
|
||||
| 11 | who_likely | Who is more likely to write a love letter? | 2 |
|
||||
| 12 | who_likely | Who is more likely to pull the other in for a random kiss? | 2 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Sensual Massage (Sensual Discovery, intensity 2-3)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | who_likely | Who is more likely to ask for a massage after a long day? | 2 |
|
||||
| 2 | who_likely | Who is more likely to fall asleep during a massage? | 2 |
|
||||
| 3 | who_likely | Who gives the better back massage? | 2 |
|
||||
| 4 | audio | Describe how you would give your partner the perfect massage | 3 |
|
||||
| 5 | audio | Tell your partner which part of your body holds the most tension | 2 |
|
||||
|
||||
---
|
||||
|
||||
## 7. Missing You (Long Distance, intensity 1-3)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | audio | Record a whispered "I miss you" voice note | 2 |
|
||||
| 2 | audio | Describe what you'd do in the first five minutes of seeing your partner again | 2 |
|
||||
| 3 | audio | Read your partner a short goodnight message in your sleepiest voice | 1 |
|
||||
| 4 | audio | Tell your partner the thing you miss most about being together | 1 |
|
||||
| 5 | photo | Take a selfie right now, no filter, no prep | 1 |
|
||||
| 6 | photo | Share a photo of something you bought that reminded you of your partner | 1 |
|
||||
| 7 | photo | Send a photo of what you're wearing right now | 2 |
|
||||
| 8 | photo | Share a photo of your favorite thing your partner left at your place | 1 |
|
||||
| 9 | who_likely | Who is more likely to send a 3am "thinking of you" text? | 1 |
|
||||
| 10 | who_likely | Who is more likely to rewatch old photos and videos of you two? | 1 |
|
||||
| 11 | who_likely | Who is more likely to wear the other person's hoodie? | 1 |
|
||||
| 12 | who_likely | Who is more likely to get emotional hearing a song that reminds them of the other? | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 8. Everyday Moments (Quality Time, intensity 1)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | photo | Share a photo of your morning coffee or breakfast | 1 |
|
||||
| 2 | photo | Take a photo of your workspace or desk setup | 1 |
|
||||
| 3 | photo | Share a photo of something small that made your day better | 1 |
|
||||
| 4 | audio | Describe your favorite daily ritual you share together | 1 |
|
||||
| 5 | audio | Tell your partner what you were just thinking about | 1 |
|
||||
| 6 | who_likely | Who is more likely to wake up first? | 1 |
|
||||
| 7 | who_likely | Who is more likely to cook dinner on a weeknight? | 1 |
|
||||
| 8 | who_likely | Who is more likely to fall asleep on the couch? | 1 |
|
||||
| 9 | who_likely | Who is more likely to suggest ordering takeout? | 1 |
|
||||
| 10 | who_likely | Who is more likely to do the dishes without being asked? | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 9. Relationship Essentials (Quality Time, intensity 1)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | audio | Tell your partner one thing you appreciate about them that you don't say enough | 1 |
|
||||
| 2 | audio | Describe in your own words what makes your relationship work | 1 |
|
||||
| 3 | audio | Tell your partner about a time they made you feel truly safe | 1 |
|
||||
| 4 | who_likely | Who is more likely to apologize first after an argument? | 1 |
|
||||
| 5 | who_likely | Who is more likely to bring up something that's bothering them? | 1 |
|
||||
| 6 | who_likely | Who is more likely to remember an anniversary or special date? | 1 |
|
||||
| 7 | who_likely | Who is more likely to compromise during a disagreement? | 1 |
|
||||
| 8 | who_likely | Who is more likely to say "I love you" first each day? | 1 |
|
||||
| 9 | photo | Share a photo of something that reminds you of your relationship | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 10. Hosting (Social Life, intensity 1)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | who_likely | Who is more likely to overdo it on the snacks and decorations? | 1 |
|
||||
| 2 | who_likely | Who is more likely to still be cleaning five minutes before guests arrive? | 1 |
|
||||
| 3 | who_likely | Who is the better DJ at a house party? | 1 |
|
||||
| 4 | who_likely | Who is more likely to invite too many people? | 1 |
|
||||
| 5 | photo | Share a photo of your dream hosting setup or table setting | 1 |
|
||||
| 6 | audio | Describe your dream dinner party - who's invited and what's on the menu? | 1 |
|
||||
| 7 | audio | What's your signature dish you'd make for guests? Describe it | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 11. Body Mapping (Sensual Discovery, intensity 2-3)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | who_likely | Who is more ticklish? | 2 |
|
||||
| 2 | who_likely | Who has more sensitive ears? | 2 |
|
||||
| 3 | who_likely | Who reacts more to having their neck kissed? | 2 |
|
||||
| 4 | who_likely | Who is more likely to discover a new sensitive spot on the other? | 2 |
|
||||
| 5 | audio | Describe where you most love being touched by your partner | 3 |
|
||||
| 6 | audio | Tell your partner the kind of touch that instantly relaxes you | 2 |
|
||||
|
||||
---
|
||||
|
||||
## 12. Date Night Ideas (Quality Time, intensity 1-2)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | photo | Share a photo of a restaurant or cafe you'd love to take your partner to | 1 |
|
||||
| 2 | photo | Take a photo of your date night outfit | 1 |
|
||||
| 3 | photo | Share a photo of a dish you'd love to cook together | 1 |
|
||||
| 4 | audio | Pitch your dream date night from start to finish | 1 |
|
||||
| 5 | audio | Describe the best date you've ever been on together | 1 |
|
||||
| 6 | who_likely | Who is more likely to pick the restaurant? | 1 |
|
||||
| 7 | who_likely | Who is more likely to try something completely new on a date? | 1 |
|
||||
| 8 | who_likely | Who is more likely to plan a surprise date? | 1 |
|
||||
| 9 | who_likely | Who is more likely to lose track of time on a date because you're having too much fun? | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 13. First Impressions (Roleplay, intensity 2-3)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | audio | Introduce yourself to your partner as if you're meeting for the first time | 2 |
|
||||
| 2 | audio | Deliver your best pick-up line to your partner | 2 |
|
||||
| 3 | audio | In character, leave a voicemail asking your partner on a first date | 2 |
|
||||
| 4 | audio | Describe what you'd wear to impress your partner if you were meeting for the first time | 2 |
|
||||
| 5 | who_likely | Who is more likely to approach the other if you were strangers? | 2 |
|
||||
| 6 | who_likely | Who is more likely to break character and laugh? | 2 |
|
||||
| 7 | who_likely | Who is more likely to use a cheesy pick-up line? | 2 |
|
||||
| 8 | who_likely | Who would be more nervous on a real first date? | 2 |
|
||||
| 9 | photo | Snap a photo of the outfit you'd wear to make a killer first impression | 2 |
|
||||
|
||||
---
|
||||
|
||||
## 14. Food & Culture (Adventure & Travel, intensity 1)
|
||||
|
||||
| # | Type | Text | Intensity |
|
||||
|---|------|------|-----------|
|
||||
| 1 | photo | Share a photo of a dish you've been craving lately | 1 |
|
||||
| 2 | photo | Take a photo of something in your kitchen that tells a story | 1 |
|
||||
| 3 | audio | Describe a meal from your childhood that means a lot to you | 1 |
|
||||
| 4 | audio | Tell your partner about a cuisine you've always wanted to try together | 1 |
|
||||
| 5 | who_likely | Who is the pickier eater? | 1 |
|
||||
| 6 | who_likely | Who is more likely to try an unusual local delicacy while traveling? | 1 |
|
||||
| 7 | who_likely | Who is more likely to send back a dish at a restaurant? | 1 |
|
||||
| 8 | who_likely | Who is the better cook? | 1 |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Pack | Audio | Photo | Who Likely | Total New |
|
||||
|------|-------|-------|------------|-----------|
|
||||
| Staying Close | 4 | 4 | 5 | **13** |
|
||||
| Finding Your Vibe | 1 | 1 | 6 | **8** |
|
||||
| Weekend Warriors | 2 | 2 | 5 | **9** |
|
||||
| Outdoor Explorers | 2 | 2 | 5 | **9** |
|
||||
| Romantic Gestures | 5 | 3 | 4 | **12** |
|
||||
| Sensual Massage | 2 | 0 | 3 | **5** |
|
||||
| Missing You | 4 | 4 | 4 | **12** |
|
||||
| Everyday Moments | 2 | 3 | 5 | **10** |
|
||||
| Relationship Essentials | 3 | 1 | 5 | **9** |
|
||||
| Hosting | 2 | 1 | 4 | **7** |
|
||||
| Body Mapping | 2 | 0 | 4 | **6** |
|
||||
| Date Night Ideas | 2 | 3 | 4 | **9** |
|
||||
| First Impressions | 4 | 1 | 4 | **9** |
|
||||
| Food & Culture | 2 | 2 | 4 | **8** |
|
||||
| **Totals** | **37** | **27** | **62** | **126** |
|
||||
Generated
+3796
-1390
File diff suppressed because it is too large.
Load diff
+9
-8
@@ -7,14 +7,15 @@
|
||||
"apps/supabase",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "turbo dev",
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"test": "npm --workspace apps/mobile test",
|
||||
"clean": "turbo clean",
|
||||
"typecheck": "turbo typecheck"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node scripts/fix-hoisting.js",
|
||||
"dev": "turbo dev",
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"test": "npm --workspace apps/mobile test",
|
||||
"clean": "turbo clean",
|
||||
"typecheck": "turbo typecheck"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pg": "^8.17.2",
|
||||
"turbo": "^2.3.3",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Fixes npm workspace hoisting for React Native native modules.
|
||||
*
|
||||
* npm workspaces hoists all deps to the root node_modules by default.
|
||||
* Native modules like @react-native-firebase/* and @shopify/react-native-skia
|
||||
* break when hoisted because:
|
||||
* - Firebase podspecs use relative paths to sibling packages (../app/package.json)
|
||||
* - Skia native bindings must resolve from the app's node_modules
|
||||
*
|
||||
* This script moves hoisted native packages back into apps/mobile/node_modules
|
||||
* so CocoaPods autolinking and Metro resolve them correctly.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const MOBILE_NM = path.join(ROOT, 'apps', 'mobile', 'node_modules');
|
||||
const ROOT_NM = path.join(ROOT, 'node_modules');
|
||||
|
||||
// Packages that must live in apps/mobile/node_modules, not root
|
||||
const PACKAGES_TO_FIX = [
|
||||
'@react-native-firebase/analytics',
|
||||
'@shopify/react-native-skia',
|
||||
];
|
||||
|
||||
for (const pkg of PACKAGES_TO_FIX) {
|
||||
const rootPkg = path.join(ROOT_NM, pkg);
|
||||
const localPkg = path.join(MOBILE_NM, pkg);
|
||||
|
||||
if (!fs.existsSync(rootPkg)) continue;
|
||||
if (fs.existsSync(localPkg)) continue; // already local
|
||||
|
||||
// Ensure parent scope dir exists (e.g. @react-native-firebase)
|
||||
const localParent = path.dirname(localPkg);
|
||||
fs.mkdirSync(localParent, { recursive: true });
|
||||
|
||||
// Move from root to local
|
||||
fs.renameSync(rootPkg, localPkg);
|
||||
console.log(`fix-hoisting: moved ${pkg} → apps/mobile/node_modules/`);
|
||||
}
|
||||
Reference in new issue
Block a user