@@ -8,7 +8,7 @@ import { Text, Heading } from '@/components/ui/Text';
88import { Button } from '@/components/ui/Button' ;
99import { useThemedAlert } from '@/components/ui/ThemedAlert' ;
1010import { VolumeSetup } from '@/components/VolumeSetup' ;
11- import { MicrophoneTest } from '@/components/MicrophoneTest ' ;
11+ import { SensorCalibration } from '@/components/SensorCalibration ' ;
1212import { SleepStageGraph } from '@/components/SleepStageGraph' ;
1313import { SleepHistoryCard } from '@/components/SleepHistoryCard' ;
1414import { SleepSessionDetailModal } from '@/components/SleepSessionDetailModal' ;
@@ -22,8 +22,12 @@ import {
2222 onRemStart ,
2323 onRemEnd ,
2424 onStageHistoryChange ,
25+ onSleepStageChange ,
26+ startCalibrationTest ,
27+ stopCalibrationTest ,
2528 type StageHistoryEntry ,
2629} from '@/services/sleep' ;
30+ import type { SleepStage } from '@/types/database' ;
2731import { storage } from '@/lib/storage' ;
2832import { fadeOut , configureSleepAudioSession } from '@/services/volume' ;
2933import { colors , spacing , borderRadius } from '@/theme/tokens' ;
@@ -37,7 +41,7 @@ export default function DreamScreen() {
3741 const { queue, getNext, complete } = useLaunchQueue ( ) ;
3842
3943 const [ showVolumeSetup , setShowVolumeSetup ] = useState ( false ) ;
40- const [ showMicTest , setShowMicTest ] = useState ( false ) ;
44+ const [ showSensorCalibration , setShowSensorCalibration ] = useState ( false ) ;
4145 const [ selectedSession , setSelectedSession ] = useState < SleepSession | null > ( null ) ;
4246 const [ showSleepSummary , setShowSleepSummary ] = useState ( false ) ;
4347 const [ sleepSummaryData , setSleepSummaryData ] = useState < {
@@ -62,7 +66,9 @@ export default function DreamScreen() {
6266 const remUnsubscribeRef = useRef < ( ( ) => void ) | null > ( null ) ;
6367 const remEndUnsubscribeRef = useRef < ( ( ) => void ) | null > ( null ) ;
6468 const stageHistoryUnsubscribeRef = useRef < ( ( ) => void ) | null > ( null ) ;
69+ const meditationSleepUnsubscribeRef = useRef < ( ( ) => void ) | null > ( null ) ;
6570 const currentQueueIdRef = useRef < string | null > ( null ) ;
71+ const isMeditationFadingRef = useRef ( false ) ;
6672
6773 useEffect ( ( ) => {
6874 return ( ) => {
@@ -106,8 +112,53 @@ export default function DreamScreen() {
106112 } catch { }
107113 meditationSoundRef . current = null ;
108114 }
115+ if ( meditationSleepUnsubscribeRef . current ) {
116+ meditationSleepUnsubscribeRef . current ( ) ;
117+ meditationSleepUnsubscribeRef . current = null ;
118+ }
119+ stopCalibrationTest ( ) ;
109120 } ;
110121
122+ const handleSleepDetectedDuringMeditation = useCallback (
123+ async ( stage : SleepStage ) => {
124+ if ( stage === 'awake' || ! meditationSoundRef . current || isMeditationFadingRef . current ) {
125+ return ;
126+ }
127+
128+ console . log ( '[Dream] Sleep detected during meditation:' , stage ) ;
129+ isMeditationFadingRef . current = true ;
130+
131+ try {
132+ const status = await meditationSoundRef . current . getStatusAsync ( ) ;
133+ if ( ! status . isLoaded ) return ;
134+
135+ const currentVolume = status . volume ?? 1.0 ;
136+ await fadeOut ( meditationSoundRef . current , currentVolume , 10000 ) ;
137+
138+ await meditationSoundRef . current . stopAsync ( ) ;
139+ await meditationSoundRef . current . unloadAsync ( ) ;
140+ meditationSoundRef . current = null ;
141+
142+ if ( meditationSleepUnsubscribeRef . current ) {
143+ meditationSleepUnsubscribeRef . current ( ) ;
144+ meditationSleepUnsubscribeRef . current = null ;
145+ }
146+ stopCalibrationTest ( ) ;
147+
148+ setIsMeditating ( false ) ;
149+ setIsMeditationPaused ( false ) ;
150+ setMeditationProgress ( 0 ) ;
151+
152+ await start ( 'audio' ) ;
153+ } catch ( err ) {
154+ console . warn ( 'Failed to handle sleep transition:' , err ) ;
155+ } finally {
156+ isMeditationFadingRef . current = false ;
157+ }
158+ } ,
159+ [ start ]
160+ ) ;
161+
111162 const handleRemStart = async ( ) => {
112163 console . log ( '[Dream] REM detected - starting playback' ) ;
113164 if ( isPlaying ) return ;
@@ -218,6 +269,11 @@ export default function DreamScreen() {
218269 try {
219270 await configureSleepAudioSession ( ) ;
220271
272+ await startCalibrationTest ( ) ;
273+ meditationSleepUnsubscribeRef . current = onSleepStageChange (
274+ handleSleepDetectedDuringMeditation
275+ ) ;
276+
221277 const { sound, status } = await Audio . Sound . createAsync (
222278 { uri : getMeditationUrl ( ) } ,
223279 {
@@ -236,6 +292,11 @@ export default function DreamScreen() {
236292 }
237293
238294 if ( playbackStatus . didJustFinish ) {
295+ if ( meditationSleepUnsubscribeRef . current ) {
296+ meditationSleepUnsubscribeRef . current ( ) ;
297+ meditationSleepUnsubscribeRef . current = null ;
298+ }
299+ stopCalibrationTest ( ) ;
239300 setIsMeditating ( false ) ;
240301 setIsMeditationPaused ( false ) ;
241302 setMeditationProgress ( 0 ) ;
@@ -252,15 +313,25 @@ export default function DreamScreen() {
252313 setIsMeditationPaused ( false ) ;
253314 } catch ( err ) {
254315 console . warn ( 'Failed to play meditation:' , err ) ;
316+ if ( meditationSleepUnsubscribeRef . current ) {
317+ meditationSleepUnsubscribeRef . current ( ) ;
318+ meditationSleepUnsubscribeRef . current = null ;
319+ }
320+ stopCalibrationTest ( ) ;
255321 start ( 'audio' ) . catch ( ( ) => {
256322 showAlert ( 'Error' , 'Failed to start sleep tracking.' ) ;
257323 } ) ;
258324 }
259325 } ,
260- [ start ]
326+ [ start , handleSleepDetectedDuringMeditation ]
261327 ) ;
262328
263329 const skipMeditation = useCallback ( async ( ) => {
330+ if ( meditationSleepUnsubscribeRef . current ) {
331+ meditationSleepUnsubscribeRef . current ( ) ;
332+ meditationSleepUnsubscribeRef . current = null ;
333+ }
334+ stopCalibrationTest ( ) ;
264335 if ( meditationSoundRef . current ) {
265336 try {
266337 await meditationSoundRef . current . stopAsync ( ) ;
@@ -311,6 +382,11 @@ export default function DreamScreen() {
311382 } , [ playMeditation ] ) ;
312383
313384 const stopMeditation = useCallback ( async ( ) => {
385+ if ( meditationSleepUnsubscribeRef . current ) {
386+ meditationSleepUnsubscribeRef . current ( ) ;
387+ meditationSleepUnsubscribeRef . current = null ;
388+ }
389+ stopCalibrationTest ( ) ;
314390 if ( meditationSoundRef . current ) {
315391 try {
316392 await meditationSoundRef . current . stopAsync ( ) ;
@@ -325,11 +401,11 @@ export default function DreamScreen() {
325401
326402 const handleVolumeComplete = useCallback ( ( ) => {
327403 setShowVolumeSetup ( false ) ;
328- setShowMicTest ( true ) ;
404+ setShowSensorCalibration ( true ) ;
329405 } , [ ] ) ;
330406
331- const handleMicTestComplete = useCallback ( async ( ) => {
332- setShowMicTest ( false ) ;
407+ const handleCalibrationComplete = useCallback ( async ( ) => {
408+ setShowSensorCalibration ( false ) ;
333409
334410 if ( helpMeFallAsleep ) {
335411 await playMeditation ( ) ;
@@ -339,14 +415,14 @@ export default function DreamScreen() {
339415 } catch {
340416 showAlert (
341417 'Error' ,
342- 'Failed to start sleep tracking. Please ensure microphone access is enabled.'
418+ 'Failed to start sleep tracking. Please ensure sensor access is enabled.'
343419 ) ;
344420 }
345421 }
346422 } , [ start , helpMeFallAsleep , playMeditation ] ) ;
347423
348- const handleMicTestSkip = useCallback ( async ( ) => {
349- setShowMicTest ( false ) ;
424+ const handleCalibrationSkip = useCallback ( async ( ) => {
425+ setShowSensorCalibration ( false ) ;
350426
351427 if ( helpMeFallAsleep ) {
352428 await playMeditation ( ) ;
@@ -356,7 +432,7 @@ export default function DreamScreen() {
356432 } catch {
357433 showAlert (
358434 'Error' ,
359- 'Failed to start sleep tracking. Please ensure microphone access is enabled.'
435+ 'Failed to start sleep tracking. Please ensure sensor access is enabled.'
360436 ) ;
361437 }
362438 }
@@ -733,25 +809,28 @@ export default function DreamScreen() {
733809 onComplete = { handleVolumeComplete }
734810 onSkip = { ( ) => {
735811 setShowVolumeSetup ( false ) ;
736- setShowMicTest ( true ) ;
812+ setShowSensorCalibration ( true ) ;
737813 } }
738814 />
739815 </ SafeAreaView >
740816 </ Modal >
741817
742818 < Modal
743- visible = { showMicTest }
819+ visible = { showSensorCalibration }
744820 animationType = "slide"
745821 presentationStyle = "pageSheet"
746- onRequestClose = { ( ) => setShowMicTest ( false ) }
822+ onRequestClose = { ( ) => setShowSensorCalibration ( false ) }
747823 >
748824 < SafeAreaView style = { styles . modalContainer } edges = { [ 'top' , 'bottom' ] } >
749825 < View style = { styles . modalHeader } >
750- < Pressable onPress = { ( ) => setShowMicTest ( false ) } style = { styles . closeButton } >
826+ < Pressable onPress = { ( ) => setShowSensorCalibration ( false ) } style = { styles . closeButton } >
751827 < Ionicons name = "close" size = { 24 } color = { colors . gray [ 400 ] } />
752828 </ Pressable >
753829 </ View >
754- < MicrophoneTest onComplete = { handleMicTestComplete } onSkip = { handleMicTestSkip } />
830+ < SensorCalibration
831+ onComplete = { handleCalibrationComplete }
832+ onSkip = { handleCalibrationSkip }
833+ />
755834 </ SafeAreaView >
756835 </ Modal >
757836 </ SafeAreaView >
0 commit comments