diff --git a/apps/rebreak-native/app/devices.tsx b/apps/rebreak-native/app/devices.tsx index e43bd2f..0a26711 100644 --- a/apps/rebreak-native/app/devices.tsx +++ b/apps/rebreak-native/app/devices.tsx @@ -19,6 +19,7 @@ import { useProtectedDevicesStore, type ProtectedDevice } from '../stores/protec import { useUserPlan } from '../hooks/useUserPlan'; import { AppHeader } from '../components/AppHeader'; import { AddMacSheet } from '../components/devices/AddMacSheet'; +import { AddWindowsSheet } from '../components/devices/AddWindowsSheet'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -399,6 +400,7 @@ export default function DevicesScreen() { } = useProtectedDevicesStore(); const [addMacVisible, setAddMacVisible] = useState(false); + const [addWindowsVisible, setAddWindowsVisible] = useState(false); useEffect(() => { loadDevices(); @@ -535,8 +537,9 @@ export default function DevicesScreen() { - setAddWindowsVisible(true)} + activeOpacity={0.7} style={{ borderRadius: 14, paddingVertical: 14, @@ -544,7 +547,6 @@ export default function DevicesScreen() { flexDirection: 'row', justifyContent: 'center', gap: 8, - opacity: 0.4, backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, @@ -554,9 +556,9 @@ export default function DevicesScreen() { - {t('devices.add_windows')} + {t('devices.add_windows_enabled')} - + ) : ( + + { + setAddWindowsVisible(false); + loadProtected(); + }} + /> ); } diff --git a/apps/rebreak-native/components/devices/AddMacSheet.tsx b/apps/rebreak-native/components/devices/AddMacSheet.tsx index db63eaa..9867d6d 100644 --- a/apps/rebreak-native/components/devices/AddMacSheet.tsx +++ b/apps/rebreak-native/components/devices/AddMacSheet.tsx @@ -77,7 +77,7 @@ export function AddMacSheet({ } setLabelError(''); try { - const result = await enroll(trimmed); + const result = await enroll(trimmed, 'mac'); setEnrollResult(result); setStep(2); } catch { diff --git a/apps/rebreak-native/components/devices/AddWindowsSheet.tsx b/apps/rebreak-native/components/devices/AddWindowsSheet.tsx new file mode 100644 index 0000000..ced504f --- /dev/null +++ b/apps/rebreak-native/components/devices/AddWindowsSheet.tsx @@ -0,0 +1,464 @@ +import { + ActivityIndicator, + Alert, + Linking, + Pressable, + Text, + TextInput, + View, +} from 'react-native'; +import { useState } from 'react'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import * as Haptics from 'expo-haptics'; +import { useColors } from '../../lib/theme'; +import { KeyboardAwareSheet } from '../KeyboardAwareSheet'; +import { RiveAvatar } from '../RiveAvatar'; +import { useProtectedDevicesStore } from '../../stores/protectedDevices'; +import { useRouter } from 'expo-router'; + +// TODO lyra-persona: review windows_lyra_intro + windows_step_* body strings for coach tone + +type Step = 1 | 2 | 3; + +interface StepItem { + titleKey: string; + bodyKey: string; + icon: React.ComponentProps['name']; +} + +const STEPS: StepItem[] = [ + { titleKey: 'devices.windows_step_1_title', bodyKey: 'devices.windows_step_1_body', icon: 'download-outline' }, + { titleKey: 'devices.windows_step_2_title', bodyKey: 'devices.windows_step_2_body', icon: 'document-outline' }, + { titleKey: 'devices.windows_step_3_title', bodyKey: 'devices.windows_step_3_body', icon: 'shield-checkmark-outline' }, + { titleKey: 'devices.windows_step_4_title', bodyKey: 'devices.windows_step_4_body', icon: 'wifi-outline' }, + { titleKey: 'devices.windows_step_5_title', bodyKey: 'devices.windows_step_5_body', icon: 'checkmark-circle-outline' }, +]; + +export function AddWindowsSheet({ + visible, + onClose, +}: { + visible: boolean; + onClose: () => void; +}) { + const { t } = useTranslation(); + const colors = useColors(); + const router = useRouter(); + const { enroll, confirmInstalled, enrolling } = useProtectedDevicesStore(); + + const [step, setStep] = useState(1); + const [label, setLabel] = useState(t('devices.windows_label_default')); + const [labelError, setLabelError] = useState(''); + const [enrollResult, setEnrollResult] = useState<{ deviceId: string; downloadUrl: string } | null>(null); + const [confirming, setConfirming] = useState(false); + + function reset() { + setStep(1); + setLabel(t('devices.windows_label_default')); + setLabelError(''); + setEnrollResult(null); + setConfirming(false); + } + + function handleClose() { + reset(); + onClose(); + } + + async function handlePrepare() { + const trimmed = label.trim(); + if (!trimmed) { + setLabelError(t('devices.windows_label_question')); + return; + } + if (trimmed.length > 32) { + setLabelError(t('devices.windows_label_question')); + return; + } + setLabelError(''); + try { + const result = await enroll(trimmed, 'windows'); + setEnrollResult(result); + setStep(2); + } catch { + Alert.alert(t('common.error'), t('common.unknown_error')); + } + } + + async function handleConfirmInstalled() { + if (!enrollResult) return; + setConfirming(true); + try { + await confirmInstalled(enrollResult.deviceId); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); + setStep(3); + } catch { + Alert.alert(t('common.error'), t('common.unknown_error')); + } finally { + setConfirming(false); + } + } + + function handleDownload() { + if (!enrollResult?.downloadUrl) return; + Linking.openURL(enrollResult.downloadUrl).catch(() => {}); + } + + function handleNeedHelp() { + handleClose(); + router.push('/coach'); + } + + const collapsedHeight = step === 1 ? 300 : step === 2 ? 700 : 380; + + return ( + + + {step === 1 + ? t('devices.windows_label_question') + : step === 2 + ? t('devices.windows_download_button') + : t('devices.windows_success_title')} + + ({ opacity: pressed ? 0.5 : 1 })} + > + + + + } + > + {step === 1 && ( + + )} + {step === 2 && ( + + )} + {step === 3 && ( + + )} + + ); +} + +function WindowsStep1LabelContent({ + label, + setLabel, + labelError, + onPrepare, + enrolling, + colors, + t, +}: { + label: string; + setLabel: (v: string) => void; + labelError: string; + onPrepare: () => void; + enrolling: boolean; + colors: ReturnType; + t: (k: string) => string; +}) { + return ( + + + {labelError ? ( + + {labelError} + + ) : null} + + ({ + backgroundColor: colors.brandOrange, + borderRadius: 14, + paddingVertical: 16, + alignItems: 'center', + opacity: pressed || enrolling ? 0.7 : 1, + })} + > + {enrolling ? ( + + ) : ( + + {t('devices.prepare_profile')} + + )} + + + ); +} + +function WindowsStep2OnboardingContent({ + onDownload, + onConfirmInstalled, + onNeedHelp, + confirming, + colors, + t, +}: { + onDownload: () => void; + onConfirmInstalled: () => void; + onNeedHelp: () => void; + confirming: boolean; + colors: ReturnType; + t: (k: string) => string; +}) { + return ( + + {/* Lyra intro card */} + + + + {t('devices.windows_lyra_intro')} + + + + {/* 5-step list */} + + {STEPS.map((item, idx) => ( + + + + + + + {t(item.titleKey)} + + + {t(item.bodyKey)} + + + + ))} + + + {/* Download button */} + ({ + backgroundColor: colors.brandOrange, + borderRadius: 14, + paddingVertical: 16, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + gap: 8, + opacity: pressed ? 0.7 : 1, + })} + > + + + {t('devices.windows_download_button')} + + + + {/* Confirm installed */} + ({ + borderWidth: 1.5, + borderColor: colors.brandOrange, + borderRadius: 14, + paddingVertical: 14, + alignItems: 'center', + opacity: pressed || confirming ? 0.7 : 1, + })} + > + {confirming ? ( + + ) : ( + + {t('devices.confirm_installed')} + + )} + + + {/* Need help */} + ({ opacity: pressed ? 0.5 : 1, alignItems: 'center' })} + > + + {t('devices.need_help')} + + + + ); +} + +function WindowsStep3SuccessContent({ + onClose, + colors, + t, +}: { + onClose: () => void; + colors: ReturnType; + t: (k: string) => string; +}) { + return ( + + + + + + {t('devices.windows_success_title')} + + + {t('devices.windows_success_body')} + + + + ({ + backgroundColor: colors.brandOrange, + borderRadius: 14, + paddingVertical: 16, + paddingHorizontal: 40, + alignItems: 'center', + opacity: pressed ? 0.7 : 1, + alignSelf: 'stretch', + })} + > + + {t('common.ok')} + + + + ); +} diff --git a/apps/rebreak-native/stores/protectedDevices.ts b/apps/rebreak-native/stores/protectedDevices.ts index 59160b1..5baac65 100644 --- a/apps/rebreak-native/stores/protectedDevices.ts +++ b/apps/rebreak-native/stores/protectedDevices.ts @@ -23,7 +23,7 @@ type ProtectedDevicesState = { enrolling: boolean; load: () => Promise; - enroll: (label: string) => Promise; + enroll: (label: string, platform: 'mac' | 'windows') => Promise; confirmInstalled: (id: string) => Promise; remove: (id: string) => Promise<{ manualRemovalRequired: boolean }>; }; @@ -45,12 +45,12 @@ export const useProtectedDevicesStore = create((set, get) } }, - enroll: async (label: string) => { + enroll: async (label: string, platform: 'mac' | 'windows') => { set({ enrolling: true }); try { const result = await apiFetch('/api/devices/enroll', { method: 'POST', - body: { platform: 'mac', label }, + body: { platform, label }, }); await get().load(); return result;