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;