Skip to content
Merged
42 changes: 42 additions & 0 deletions apps/mobile/app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import {
StyleSheet,
ScrollView,
Modal,
Share,
type TextInput,
} from 'react-native'
import { File, Paths } from 'expo-file-system'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { useLocalSearchParams, useRouter, type Href } from 'expo-router'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import { useQueryClient } from '@tanstack/react-query'
import { parseISO } from 'date-fns'
import { API } from '@orbit/shared/api'
import type { UserDataExport } from '@orbit/shared'
import { profileKeys } from '@orbit/shared/query'
import {
buildFreshStartDeletedItems,
Expand Down Expand Up @@ -201,6 +204,34 @@ export default function ProfileScreen() {
}
}

const [isExporting, setIsExporting] = useState(false)
const [exportError, setExportError] = useState('')

async function handleExportData() {
if (isExporting) return
if (!isOnline) {
setExportError(t('calendarSync.notConnected'))
return
}
setIsExporting(true)
setExportError('')
try {
const data = await apiClient<UserDataExport>(API.profile.export)
const fileName = `orbit-data-export-${new Date().toISOString().slice(0, 10)}.json`
const file = new File(Paths.cache, fileName)
file.create({ overwrite: true })
file.write(JSON.stringify(data, null, 2))
await Share.share({
title: t('dataExport.shareTitle'),
url: file.uri,
})
} catch (err: unknown) {
setExportError(getErrorMessage(err, t('dataExport.error')))
} finally {
setIsExporting(false)
}
}

const [showTourReplay, setShowTourReplay] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [deleteStep, setDeleteStep] = useState<'confirm' | 'code' | 'deactivated'>('confirm')
Expand Down Expand Up @@ -502,6 +533,17 @@ export default function ProfileScreen() {
</View>

<SectionLabel>{t('profile.sections.accountActions')}</SectionLabel>
<ProfileActionButton
onPress={() => {
void handleExportData()
}}
label={isExporting ? t('dataExport.preparing') : t('dataExport.button')}
/>
{exportError ? (
<Text style={[styles.errorText, { color: tokens.statusBad }]}>
{exportError}
</Text>
) : null}
<ProfileActionButton
onPress={() => logout()}
label={t('profile.logout')}
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ function RootLayoutNav() {
options={{ animation: 'slide_from_right' }}
/>
<Stack.Screen name="privacy" options={{ animation: 'fade' }} />
<Stack.Screen name="terms" options={{ animation: 'fade' }} />
</Stack>
</View>

Expand Down
8 changes: 8 additions & 0 deletions apps/mobile/app/chat.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export function createStyles(tokens: Tokens) {
letterSpacing: -0.2,
textAlign: "center",
},
aiDisclaimer: {
fontFamily: "Geist",
fontSize: 11,
lineHeight: 15,
fontStyle: "italic",
textAlign: "center",
maxWidth: 300,
},
messageList: {
paddingVertical: 16,
},
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/app/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export default function LoginScreen() {
}

function openTerms() {
Linking.openURL('https://app.useorbit.org/about')
Linking.openURL('https://app.useorbit.org/terms')
}

const canSubmitEmail = Boolean(email.trim()) && !isSubmitting && isOnline
Expand Down
66 changes: 66 additions & 0 deletions apps/mobile/app/privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export default function PrivacyScreen() {
'openai',
'resend',
] as const
const retentionKeys = [
'account',
'sessions',
'ai',
'afterDeletion',
] as const
const googleScopesKeys = ['auth', 'calendar', 'control'] as const

return (
<SafeAreaView
Expand All @@ -53,6 +60,13 @@ export default function PrivacyScreen() {
</Text>
</View>

<SectionLabel>{t('privacy.controller.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('privacy.controller.body')}
</Text>
</View>

<SectionLabel>{t('privacy.dataCollected.title')}</SectionLabel>
<View style={styles.bodyBlock}>
{dataCollectedKeys.map((key) => (
Expand Down Expand Up @@ -92,6 +106,58 @@ export default function PrivacyScreen() {
))}
</View>

<SectionLabel>{t('privacy.retention.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('privacy.retention.intro')}
</Text>
{retentionKeys.map((key) => (
<Text key={key} style={[styles.bodyText, { color: tokens.fg2 }]}>
{`• ${t(`privacy.retention.${key}`)}`}
</Text>
))}
</View>

<SectionLabel>{t('privacy.googleScopes.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('privacy.googleScopes.intro')}
</Text>
{googleScopesKeys.map((key) => (
<Text key={key} style={[styles.bodyText, { color: tokens.fg2 }]}>
{`• ${t(`privacy.googleScopes.${key}`)}`}
</Text>
))}
</View>

<SectionLabel>{t('privacy.dataResidency.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('privacy.dataResidency.body')}
</Text>
</View>

<SectionLabel>{t('privacy.automatedProcessing.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('privacy.automatedProcessing.body')}
</Text>
</View>

<SectionLabel>{t('privacy.minors.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('privacy.minors.body')}
</Text>
</View>

<SectionLabel>{t('privacy.export.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('privacy.export.body')}
</Text>
</View>

<SectionLabel>{t('privacy.noSell.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
Expand Down
11 changes: 11 additions & 0 deletions apps/mobile/app/retrospective.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,9 @@ export default function RetrospectiveScreen() {
text={displayedRetrospective}
tokens={tokens}
/>
<Text style={[styles.aiDisclaimer, { color: tokens.fg3 }]}>
{t('aiDisclosure.notMedicalAdvice')}
</Text>
{displayedFromCache ? (
<View style={styles.cachedRow}>
<Text
Expand Down Expand Up @@ -601,6 +604,14 @@ const styles = StyleSheet.create({
letterSpacing: 0.63,
textTransform: 'uppercase',
},
aiDisclaimer: {
fontFamily: 'Geist',
fontSize: 11,
lineHeight: 15,
fontStyle: 'italic',
marginTop: 12,
paddingHorizontal: 20,
},
regenLinkPress: { padding: 4 },
regenLink: {
fontFamily: 'Geist',
Expand Down
154 changes: 154 additions & 0 deletions apps/mobile/app/terms.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { useMemo } from 'react'
import { ScrollView, StyleSheet, Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import { createTokensV2 } from '@/lib/theme'
import { useGoBackOrFallback } from '@/hooks/use-go-back-or-fallback'
import { useAppTheme } from '@/lib/use-app-theme'
import { useAuthStore } from '@/stores/auth-store'
import { AppBar } from '@/components/ui/app-bar'
import { SectionLabel } from '@/components/ui/section-label'

export default function TermsScreen() {
const goBackOrFallback = useGoBackOrFallback()
const { t } = useTranslation()
const { currentScheme, currentTheme } = useAppTheme()
const tokens = useMemo(
() => createTokensV2(currentScheme, currentTheme),
[currentScheme, currentTheme],
)
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)

const subscriptionKeys = ['intro', 'autoRenew', 'cancel', 'refunds'] as const

return (
<SafeAreaView
style={[styles.safeArea, { backgroundColor: tokens.bg }]}
edges={['top']}
>
<AppBar
back
onBack={() => goBackOrFallback(isAuthenticated ? '/' : '/login')}
title={t('terms.title')}
subtitle={t('terms.lastUpdated')}
backLabel={t('common.goBack')}
/>
<ScrollView
style={styles.container}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<SectionLabel>{t('terms.intro.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.intro.body')}
</Text>
</View>

<SectionLabel>{t('terms.provider.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.provider.body')}
</Text>
</View>

<SectionLabel>{t('terms.eligibility.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.eligibility.body')}
</Text>
</View>

<SectionLabel>{t('terms.license.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.license.body')}
</Text>
</View>

<SectionLabel>{t('terms.subscription.title')}</SectionLabel>
<View style={styles.bodyBlock}>
{subscriptionKeys.map((key) => (
<Text key={key} style={[styles.bodyText, { color: tokens.fg2 }]}>
{t(`terms.subscription.${key}`)}
</Text>
))}
</View>

<SectionLabel>{t('terms.ai.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.ai.body')}
</Text>
</View>

<SectionLabel>{t('terms.noMedicalAdvice.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.noMedicalAdvice.body')}
</Text>
</View>

<SectionLabel>{t('terms.warranty.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.warranty.body')}
</Text>
</View>

<SectionLabel>{t('terms.liability.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.liability.body')}
</Text>
</View>

<SectionLabel>{t('terms.termination.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.termination.body')}
</Text>
</View>

<SectionLabel>{t('terms.governingLaw.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.governingLaw.body')}
</Text>
</View>

<SectionLabel>{t('terms.changes.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.changes.body')}
</Text>
</View>

<SectionLabel>{t('terms.contact.title')}</SectionLabel>
<View style={styles.bodyBlock}>
<Text style={[styles.bodyText, { color: tokens.fg2 }]}>
{t('terms.contact.body')}
</Text>
</View>

<View style={{ height: 24 }} />
</ScrollView>
</SafeAreaView>
)
}

const styles = StyleSheet.create({
safeArea: { flex: 1 },
container: { flex: 1 },
scrollContent: { paddingBottom: 40 },
bodyBlock: {
paddingHorizontal: 20,
paddingBottom: 18,
gap: 6,
},
bodyText: {
fontFamily: 'Geist',
fontSize: 14,
lineHeight: 22,
},
})
3 changes: 3 additions & 0 deletions apps/mobile/components/chat/chat-empty-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const ChatEmptyState = forwardRef<View, Readonly<ChatEmptyStateProps>>(
{t("chat.suggestion.prompt")}
</Text>
<SuggestionChips onSelect={onSelectSuggestion} />
<Text style={[styles.aiDisclaimer, { color: tokens.fg3 }]}>
{t("aiDisclosure.notMedicalAdvice")}
</Text>
</View>
);
},
Expand Down
Loading
Loading