Compare commits

...
2 Commits
Author SHA1 Message Date
jjbrunton e52430f0a8 Update 2026-01-05 15:21:56 +00:00
Jamie Brunton bc0b8abd17 Add content balance implementation plan 2026-01-05 12:30:51 +00:00
2 changed files with 237 additions and 0 deletions

No files matched your search

@@ -9,6 +9,7 @@ import {
ScrollView, ScrollView,
Platform, Platform,
Pressable, Pressable,
Linking,
} from "react-native"; } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
@@ -368,6 +369,19 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
Subscription automatically renews unless cancelled at least 24 hours Subscription automatically renews unless cancelled at least 24 hours
before the end of the current period. before the end of the current period.
</Text> </Text>
<View style={styles.legalLinks}>
<TouchableOpacity
onPress={() => Linking.openURL("https://sauci.app/terms")}
>
<Text style={styles.legalLinkText}>Terms of Use</Text>
</TouchableOpacity>
<Text style={styles.legalSeparator}>•</Text>
<TouchableOpacity
onPress={() => Linking.openURL("https://sauci.app/privacy")}
>
<Text style={styles.legalLinkText}>Privacy Policy</Text>
</TouchableOpacity>
</View>
</View> </View>
</ScrollView> </ScrollView>
</View> </View>
@@ -716,6 +730,22 @@ const styles = StyleSheet.create({
textAlign: "center", textAlign: "center",
lineHeight: 18, lineHeight: 18,
}, },
legalLinks: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
marginTop: spacing.sm,
gap: spacing.xs,
},
legalLinkText: {
...typography.caption1,
color: colors.premium.gold,
textDecorationLine: "underline",
},
legalSeparator: {
...typography.caption1,
color: colors.textTertiary,
},
}); });
export default Paywall; export default Paywall;
+207
View File
@@ -0,0 +1,207 @@
# Content Balance Implementation
> Transform Sauci from binary explicit/clean filtering to graduated intensity-based control.
## Overview
### Problem
The current binary "Spicy Content" toggle is too coarse. Users either see ALL explicit content or NONE, which:
- Makes the app appear like "smut" to App Store reviewers if explicit is on
- Provides no middle ground for users who want flirty but not explicit content
### Solution
Replace the binary toggle with a 5-level intensity slider:
| Level | Label | Content Type |
|-------|-------|--------------|
| 1 | Light | Conversations, compliments, emotional connection |
| 2 | Mild | Cuddling, gentle kisses, flirty texts |
| 3 | Moderate | Making out, massage, suggestive content |
| 4 | Spicy | Intimate activities, explicit content |
| 5 | Intense | Advanced intimacy, kinks, fantasies |
### Key Changes
- **Default:** New users see max intensity 2 (Mild)
- **Control:** Graduated 1-5 slider instead of on/off
- **Filtering:** Filter by question intensity, not just pack explicit flag
- **Display:** Show intensity range on pack cards
---
## Tasks
### Phase 1: Database Migration
**File:** `apps/supabase/migrations/YYYYMMDDHHMMSS_add_intensity_levels.sql`
- [ ] Add `max_intensity` column to `profiles` table (INTEGER, default 2, CHECK 1-5)
- [ ] Migrate existing users: `show_explicit_content=true` → 5, else → 2
- [ ] Add `min_intensity`, `max_intensity`, `avg_intensity` columns to `question_packs`
- [ ] Create trigger to auto-update pack intensity stats when questions change
- [ ] Backfill intensity stats for existing packs
```sql
-- Example migration
ALTER TABLE profiles
ADD COLUMN max_intensity INTEGER DEFAULT 2
CHECK (max_intensity >= 1 AND max_intensity <= 5);
UPDATE profiles SET max_intensity = CASE
WHEN show_explicit_content = true THEN 5
ELSE 2
END;
```
---
### Phase 2: Mobile Types
**File:** `apps/mobile/src/types/index.ts`
- [ ] Add `max_intensity: 1 | 2 | 3 | 4 | 5` to `Profile` interface
- [ ] Add `min_intensity?: number`, `max_intensity?: number`, `avg_intensity?: number` to `QuestionPack` interface
---
### Phase 3: IntensitySlider Component
**File:** `apps/mobile/src/components/ui/IntensitySlider.tsx` (NEW)
- [ ] Create slider component with 5 levels
- [ ] Add flame icons that fill based on selected level
- [ ] Display label and description for current level
- [ ] Apply glass-morphism styling matching app theme
- [ ] Add haptic feedback on level change
```typescript
interface IntensitySliderProps {
value: 1 | 2 | 3 | 4 | 5;
onValueChange: (value: 1 | 2 | 3 | 4 | 5) => void;
disabled?: boolean;
}
```
---
### Phase 4: Onboarding Update
**File:** `apps/mobile/app/(app)/onboarding.tsx`
- [ ] Replace Stage 4 binary choice with IntensitySlider
- [ ] Update heading: "Set Your Comfort Level"
- [ ] Change default state from `showExplicit: false` to `maxIntensity: 2`
- [ ] Update profile save to set both fields for backwards compatibility
**Current Stage 4:**
```
"Show me everything" vs "Keep it clean"
```
**New Stage 4:**
```
"Set Your Comfort Level"
[Slider: 1 ----*---- 5]
Light Intense
Current: "Mild - Cuddling, gentle kisses, flirty texts"
Note: "Start gentle - you can turn up the heat anytime"
```
---
### Phase 5: Settings Update
**File:** `apps/mobile/src/features/profile/components/PrivacySettings.tsx`
- [ ] Replace `SwitchItem` with `IntensitySlider`
**File:** `apps/mobile/src/features/profile/hooks/useProfileSettings.ts`
- [ ] Change state from `showExplicit: boolean` to `maxIntensity: number`
- [ ] Update handler to save both `max_intensity` and `show_explicit_content`
- [ ] Call `fetchPacks()` after intensity change to refresh filtered packs
---
### Phase 6: Pack Store Filtering
**File:** `apps/mobile/src/store/packsStore.ts`
- [ ] Update `fetchPacks()` to filter by intensity instead of explicit flag
```typescript
// Current
if (!showExplicit) {
query = query.eq("is_explicit", false);
}
// New
const maxIntensity = useAuthStore.getState().user?.max_intensity ?? 2;
query = query.or(`max_intensity.is.null,max_intensity.lte.${maxIntensity}`);
```
---
### Phase 7: Pack Display Enhancement
**File:** `apps/mobile/app/(app)/packs/questions.tsx`
- [ ] Add intensity badge to pack cards showing flame icons
- [ ] Display intensity label: "Light" or "Light - Moderate" range
---
### Phase 8: Default Packs for New Couples
**File:** `apps/supabase/functions/manage-couple/index.ts`
- [ ] When couple forms, auto-enable packs where `avg_intensity <= 2`
```typescript
const { data: safePacks } = await supabase
.from('question_packs')
.select('id')
.lte('avg_intensity', 2)
.eq('is_public', true);
await supabase.from('couple_packs').insert(
safePacks.map(p => ({ couple_id, pack_id: p.id, enabled: true }))
);
```
---
## Files Summary
| File | Action |
|------|--------|
| `apps/supabase/migrations/XXX_add_intensity_levels.sql` | CREATE |
| `apps/mobile/src/types/index.ts` | MODIFY |
| `apps/mobile/src/components/ui/IntensitySlider.tsx` | CREATE |
| `apps/mobile/app/(app)/onboarding.tsx` | MODIFY |
| `apps/mobile/src/features/profile/components/PrivacySettings.tsx` | MODIFY |
| `apps/mobile/src/features/profile/hooks/useProfileSettings.ts` | MODIFY |
| `apps/mobile/src/store/packsStore.ts` | MODIFY |
| `apps/mobile/app/(app)/packs/questions.tsx` | MODIFY |
| `apps/supabase/functions/manage-couple/index.ts` | MODIFY |
---
## Backwards Compatibility
1. **Keep `show_explicit_content` column** during transition period
2. **Sync both fields** when user changes intensity:
- `max_intensity >= 3` → `show_explicit_content = true`
- `max_intensity < 3` → `show_explicit_content = false`
3. **Deprecation:** Remove `show_explicit_content` after 2-3 app update cycles
---
## App Store Benefits
After implementation:
- Default experience shows only Light/Mild content (levels 1-2)
- App Store screenshots will feature connection-focused questions
- Users must consciously opt-in to see spicier content
- Categories already ordered with connection-focused content first