Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mobile/app/preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const colors = {
// Language options
const LANGUAGE_OPTIONS: { value: 'en' | 'pt-BR'; label: string }[] = [
{ value: 'en', label: 'English' },
{ value: 'pt-BR', label: 'Portugues' },
{ value: 'pt-BR', label: 'Portugu锚s' },
]

// ---------------------------------------------------------------------------
Expand Down
59 changes: 18 additions & 41 deletions apps/mobile/app/privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ShieldCheck,
ExternalLink,
} from 'lucide-react-native'
import { useTranslation } from 'react-i18next'

// ---------------------------------------------------------------------------
// Colors
Expand All @@ -36,41 +37,18 @@ const colors = {

export default function PrivacyScreen() {
const router = useRouter()
const { t } = useTranslation()

const PRIVACY_URL = 'https://useorbit.org/privacy'

const sections = [
{
title: 'Data Collection',
content:
'Orbit collects only the data necessary to provide our service: your email, habit data, and optional AI conversation history. We never sell your data.',
},
{
title: 'Data Storage',
content:
'Your data is stored securely on encrypted servers. Habit logs, goals, and profile data are associated with your account and accessible only by you.',
},
{
title: 'AI & Privacy',
content:
'When AI features are enabled, your habit data is processed to generate summaries and insights. This data is never used to train AI models or shared with third parties.',
},
{
title: 'Third Parties',
content:
'We use essential third-party services for authentication (Google OAuth), payments (Stripe), and infrastructure. These services only receive the minimum data required.',
},
{
title: 'Data Deletion',
content:
'You can delete your account at any time from Profile > Account Actions. Your data will be permanently deleted within 30 days. You can also use "Fresh Start" to wipe your data while keeping your account.',
},
{
title: 'Your Rights',
content:
'You have the right to access, export, and delete your data. Contact support@useorbit.org for any data-related requests.',
},
]
const SECTION_KEYS = [
'dataCollection',
'dataStorage',
'aiPrivacy',
'thirdParties',
'dataDeletion',
'yourRights',
] as const

return (
<SafeAreaView style={styles.safeArea}>
Expand All @@ -88,24 +66,23 @@ export default function PrivacyScreen() {
>
<ArrowLeft size={20} color={colors.textMuted} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Privacy Policy</Text>
<Text style={styles.headerTitle}>{t('privacy.title')}</Text>
</View>

{/* Shield badge */}
<View style={styles.shieldCard}>
<ShieldCheck size={32} color={colors.green} />
<Text style={styles.shieldTitle}>Your privacy matters</Text>
<Text style={styles.shieldTitle}>{t('privacy.badge')}</Text>
<Text style={styles.shieldSubtitle}>
Orbit is designed with privacy first. We collect minimal data and
never sell it.
{t('privacy.badgeDescription')}
</Text>
</View>

{/* Sections */}
{sections.map((section) => (
<View key={section.title} style={styles.sectionCard}>
<Text style={styles.sectionTitle}>{section.title}</Text>
<Text style={styles.sectionContent}>{section.content}</Text>
{SECTION_KEYS.map((key) => (
<View key={key} style={styles.sectionCard}>
<Text style={styles.sectionTitle}>{t(`privacy.${key}.title`)}</Text>
<Text style={styles.sectionContent}>{t(`privacy.${key}.content`)}</Text>
</View>
))}

Expand All @@ -117,7 +94,7 @@ export default function PrivacyScreen() {
>
<ExternalLink size={16} color={colors.primary} />
<Text style={styles.linkButtonText}>
Read Full Privacy Policy
{t('privacy.fullPolicy')}
</Text>
</TouchableOpacity>
</ScrollView>
Expand Down
165 changes: 102 additions & 63 deletions apps/mobile/app/retrospective.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react'
import React from 'react'
import {
View,
Text,
Expand All @@ -9,12 +9,9 @@ import {
ActivityIndicator,
} from 'react-native'
import { useRouter } from 'expo-router'
import { ArrowLeft, Sparkles, ChevronLeft, ChevronRight } from 'lucide-react-native'
import { useQuery } from '@tanstack/react-query'
import { format, subWeeks, addWeeks, startOfWeek, endOfWeek } from 'date-fns'
import { habitKeys } from '@orbit/shared/query'
import { formatAPIDate } from '@orbit/shared/utils'
import { apiClient } from '@/lib/api-client'
import { ArrowLeft, Sparkles } from 'lucide-react-native'
import { useTranslation } from 'react-i18next'
import { useRetrospective, type RetrospectivePeriod } from '@/hooks/use-retrospective'

// ---------------------------------------------------------------------------
// Colors
Expand All @@ -35,22 +32,20 @@ const colors = {
// Retrospective Screen
// ---------------------------------------------------------------------------

const PERIODS: RetrospectivePeriod[] = ['week', 'month', 'quarter', 'semester', 'year']

export default function RetrospectiveScreen() {
const router = useRouter()
const [currentWeek, setCurrentWeek] = useState(() => startOfWeek(new Date()))

const weekStart = formatAPIDate(currentWeek)
const weekEnd = formatAPIDate(endOfWeek(currentWeek))
const periodLabel = `${format(currentWeek, 'MMM d')} - ${format(endOfWeek(currentWeek), 'MMM d, yyyy')}`

const { data, isLoading, error } = useQuery({
queryKey: habitKeys.retrospective(weekStart),
queryFn: () =>
apiClient<{ retrospective: string }>(
`/api/habits/retrospective?period=week&date=${weekStart}`,
),
staleTime: 10 * 60 * 1000,
})
const { t } = useTranslation()
const {
retrospective,
isLoading,
error,
fromCache,
period,
setPeriod,
generate,
} = useRetrospective()

return (
<SafeAreaView style={styles.safeArea}>
Expand All @@ -68,54 +63,71 @@ export default function RetrospectiveScreen() {
>
<ArrowLeft size={20} color={colors.textMuted} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Retrospective</Text>
<Text style={styles.headerTitle}>{t('retrospective.title')}</Text>
</View>

{/* Week navigation */}
<View style={styles.weekNav}>
<TouchableOpacity
style={styles.weekNavButton}
onPress={() => setCurrentWeek((w) => subWeeks(w, 1))}
activeOpacity={0.7}
>
<ChevronLeft size={16} color={colors.textMuted} />
</TouchableOpacity>
<Text style={styles.weekLabel}>{periodLabel}</Text>
<TouchableOpacity
style={styles.weekNavButton}
onPress={() => setCurrentWeek((w) => addWeeks(w, 1))}
activeOpacity={0.7}
>
<ChevronRight size={16} color={colors.textMuted} />
</TouchableOpacity>
</View>
{/* Period selector */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.periodScroll}
contentContainerStyle={styles.periodScrollContent}
>
{PERIODS.map((p) => (
<TouchableOpacity
key={p}
style={[styles.periodChip, period === p && styles.periodChipActive]}
onPress={() => setPeriod(p)}
activeOpacity={0.7}
>
<Text style={[styles.periodChipText, period === p && styles.periodChipTextActive]}>
{t(`retrospective.periods.${p}`)}
</Text>
</TouchableOpacity>
))}
</ScrollView>

{/* Generate button */}
<TouchableOpacity
style={[styles.generateButton, isLoading && styles.generateButtonDisabled]}
onPress={generate}
disabled={isLoading}
activeOpacity={0.8}
>
{isLoading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Sparkles size={16} color="#fff" />
)}
<Text style={styles.generateButtonText}>{t('retrospective.generate')}</Text>
</TouchableOpacity>

{/* Content */}
{isLoading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>
Generating your retrospective...
{t('retrospective.generating')}
</Text>
</View>
)}

{error && (
<View style={styles.errorCard}>
<Text style={styles.errorText}>
Failed to generate retrospective. This feature requires a Pro
subscription and completed habits for the selected week.
</Text>
<Text style={styles.errorText}>{error}</Text>
</View>
)}

{data?.retrospective && (
{retrospective && (
<View style={styles.retroCard}>
<View style={styles.retroHeader}>
<Sparkles size={18} color={colors.primary} />
<Text style={styles.retroLabel}>AI Retrospective</Text>
<Text style={styles.retroLabel}>
{t('retrospective.aiTitle')}
{fromCache && <Text style={styles.cacheIndicator}> ({t('retrospective.cached')})</Text>}
</Text>
</View>
<Text style={styles.retroText}>{data.retrospective}</Text>
<Text style={styles.retroText}>{retrospective}</Text>
</View>
)}
</ScrollView>
Expand Down Expand Up @@ -145,28 +157,55 @@ const styles = StyleSheet.create({
color: colors.textPrimary,
letterSpacing: -0.5,
},
weekNav: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
periodScroll: {
marginBottom: 12,
},
periodScrollContent: {
gap: 8,
paddingVertical: 2,
},
periodChip: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: colors.surface,
borderRadius: 14,
borderWidth: 1,
borderColor: colors.border,
padding: 4,
marginBottom: 16,
},
weekNavButton: {
width: 40,
height: 40,
borderRadius: 12,
periodChipActive: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
periodChipText: {
fontSize: 13,
fontWeight: '600',
color: colors.textMuted,
},
periodChipTextActive: {
color: '#fff',
},
generateButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
backgroundColor: colors.primary,
borderRadius: 16,
paddingVertical: 14,
marginBottom: 16,
},
weekLabel: {
fontSize: 14,
fontWeight: '600',
color: colors.textPrimary,
generateButtonDisabled: {
opacity: 0.5,
},
generateButtonText: {
fontSize: 15,
fontWeight: '700',
color: '#fff',
},
cacheIndicator: {
fontSize: 12,
fontWeight: '400',
color: colors.textMuted,
},
loadingContainer: {
alignItems: 'center',
Expand Down
Loading
Loading