polished and finalised onboarding flow
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { getSetupStatus } from "@/lib/setupStatus";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import * as Notifications from 'expo-notifications';
|
||||
@@ -32,6 +33,8 @@ function UseNotificationObserver() {
|
||||
export default function TabLayout() {
|
||||
const [session, SetSession] = useState<Session | null>(null)
|
||||
const [loading, SetLoading] = useState(true);
|
||||
const [setupChecked, setSetupChecked] = useState(false);
|
||||
const [needsSetup, setNeedsSetup] = useState(false);
|
||||
|
||||
UseNotificationObserver();
|
||||
|
||||
@@ -51,7 +54,29 @@ export default function TabLayout() {
|
||||
return () => sub.subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
useEffect(() => {
|
||||
const checkSetupStatus = async () => {
|
||||
if (!session?.user.id) {
|
||||
setNeedsSetup(false);
|
||||
setSetupChecked(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const setupStatus = await getSetupStatus(session.user.id);
|
||||
setNeedsSetup(!setupStatus.isSetupComplete);
|
||||
} catch {
|
||||
setNeedsSetup(true);
|
||||
} finally {
|
||||
setSetupChecked(true);
|
||||
}
|
||||
};
|
||||
|
||||
setSetupChecked(false);
|
||||
void checkSetupStatus();
|
||||
}, [session?.user.id]);
|
||||
|
||||
if (loading || !setupChecked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -59,6 +84,10 @@ export default function TabLayout() {
|
||||
return <Redirect href="/login" />;
|
||||
}
|
||||
|
||||
if (needsSetup) {
|
||||
return <Redirect href="/setup" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
@@ -68,4 +97,4 @@ export default function TabLayout() {
|
||||
<Tabs.Screen name="subjects" options={{title: "Subjects"}} />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import { formatDate, formatDateTime } from '@/lib/date';
|
||||
import { RegisterForLocalNotificationsAsync } from '@/lib/notifications';
|
||||
import { CheckAssignmentCompletion } from '@/lib/progress';
|
||||
import { DEFAULT_FOCUS_DURATION_MINUTES } from '@/lib/sessionDefaults';
|
||||
import { getSetupStatus } from '@/lib/setupStatus';
|
||||
import { finalizeStoredSession } from '@/lib/sessionLifecycle';
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { router, Stack, useFocusEffect } from "expo-router";
|
||||
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
@@ -164,6 +165,7 @@ export default function HomeScreen() {
|
||||
const [isFlowInfoVisible, setIsFlowInfoVisible] = useState(false);
|
||||
const [completingTaskId, setCompletingTaskId] = useState<string | null>(null);
|
||||
const [subjectCount, setSubjectCount] = useState(0);
|
||||
const [needsSetup, setNeedsSetup] = useState<boolean | null>(null);
|
||||
|
||||
const loadActiveSprint = useCallback(async () => {
|
||||
const storedSprint = await GetActiveSession();
|
||||
@@ -479,6 +481,25 @@ export default function HomeScreen() {
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSetupGate = async () => {
|
||||
if (!session?.user.id) {
|
||||
setNeedsSetup(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const setupStatus = await getSetupStatus(session.user.id);
|
||||
setNeedsSetup(!setupStatus.isSetupComplete);
|
||||
} catch {
|
||||
setNeedsSetup(true);
|
||||
}
|
||||
};
|
||||
|
||||
setNeedsSetup(null);
|
||||
void loadSetupGate();
|
||||
}, [session?.user.id]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
void loadActiveSprint();
|
||||
@@ -611,6 +632,14 @@ export default function HomeScreen() {
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (session && needsSetup === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (needsSetup) {
|
||||
return <Redirect href="/setup" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-app-bg">
|
||||
<Stack.Screen
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SUBJECT_COLORS } from '@/lib/subjectColors';
|
||||
import { getSetupStatus } from '@/lib/setupStatus';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Subject } from '@/lib/types';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { router, Stack, useFocusEffect } from 'expo-router';
|
||||
import { Redirect, router, Stack, useFocusEffect } from 'expo-router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Alert, Modal, Pressable, ScrollView, Text, View } from 'react-native';
|
||||
|
||||
@@ -36,6 +37,7 @@ export default function Subjects() {
|
||||
const [subjects, SetSubjects] = useState<Subject[]>([]);
|
||||
const [session, SetSession] = useState<Session | null>(null);
|
||||
const [isFlowInfoVisible, setIsFlowInfoVisible] = useState(false);
|
||||
const [needsSetup, setNeedsSetup] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
@@ -51,6 +53,25 @@ export default function Subjects() {
|
||||
return () => sub.subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSetupGate = async () => {
|
||||
if (!session?.user.id) {
|
||||
setNeedsSetup(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const setupStatus = await getSetupStatus(session.user.id);
|
||||
setNeedsSetup(!setupStatus.isSetupComplete);
|
||||
} catch {
|
||||
setNeedsSetup(true);
|
||||
}
|
||||
};
|
||||
|
||||
setNeedsSetup(null);
|
||||
void loadSetupGate();
|
||||
}, [session?.user.id]);
|
||||
|
||||
const GetSubjects = useCallback(async () => {
|
||||
if (!session?.user.id) return;
|
||||
|
||||
@@ -76,6 +97,14 @@ export default function Subjects() {
|
||||
}, [GetSubjects, session])
|
||||
);
|
||||
|
||||
if (session && needsSetup === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (needsSetup) {
|
||||
return <Redirect href="/setup" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-app-bg">
|
||||
<Stack.Screen
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getSetupStatus } from "@/lib/setupStatus";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { router } from "expo-router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -49,7 +50,7 @@ export default function Login() {
|
||||
|
||||
SetIsLoading(true);
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
@@ -61,7 +62,17 @@ export default function Login() {
|
||||
return;
|
||||
}
|
||||
|
||||
router.replace("/");
|
||||
if (!data.user?.id) {
|
||||
Alert.alert("Login failed, missing user session after sign-in");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const setupStatus = await getSetupStatus(data.user.id);
|
||||
router.replace(setupStatus.isSetupComplete ? "/" : "/setup");
|
||||
} catch {
|
||||
router.replace("/setup");
|
||||
}
|
||||
}
|
||||
|
||||
const inputClassName =
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { GetActiveSession, type ActiveSession } from '@/lib/asyncStorage';
|
||||
import {
|
||||
GetActiveSession,
|
||||
GetSetupSprintDemoUsed,
|
||||
SaveSetupSprintDemoUsed,
|
||||
type ActiveSession,
|
||||
} from '@/lib/asyncStorage';
|
||||
import { DEFAULT_FOCUS_DURATION_MINUTES } from '@/lib/sessionDefaults';
|
||||
import { getSetupStatus, type SetupStepKey } from '@/lib/setupStatus';
|
||||
import { finalizeStoredSession } from '@/lib/sessionLifecycle';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { Redirect, Stack, router, useFocusEffect, useLocalSearchParams } from 'expo-router';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Pressable, ScrollView, Text, View } from 'react-native';
|
||||
|
||||
type SetupState = {
|
||||
@@ -40,8 +47,6 @@ const SETUP_STEPS = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
type SetupStepKey = (typeof SETUP_STEPS)[number]['key'];
|
||||
|
||||
export default function SetupScreen() {
|
||||
const {
|
||||
subjectId: subjectIdParam,
|
||||
@@ -89,37 +94,10 @@ export default function SetupScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
const [storedActiveSession, subjectResult, assignmentResult, taskResult, focusSessionResult] =
|
||||
await Promise.all([
|
||||
GetActiveSession(),
|
||||
supabase
|
||||
.from('subjects')
|
||||
.select('sId')
|
||||
.eq('uId', session.user.id)
|
||||
.order('lastChanged', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from('assignments')
|
||||
.select('aId')
|
||||
.eq('uId', session.user.id)
|
||||
.order('lastChanged', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from('tasks')
|
||||
.select('tId')
|
||||
.eq('uId', session.user.id)
|
||||
.order('lastChanged', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from('sprint_sessions')
|
||||
.select('sessionId', { count: 'exact', head: true })
|
||||
.eq('userId', session.user.id)
|
||||
.eq('sessionType', 'focus')
|
||||
.eq('status', 'completed'),
|
||||
]);
|
||||
const [storedActiveSession, status] = await Promise.all([
|
||||
GetActiveSession(),
|
||||
getSetupStatus(session.user.id),
|
||||
]);
|
||||
|
||||
if (storedActiveSession && storedActiveSession.endTime <= Date.now()) {
|
||||
await finalizeStoredSession('expired', storedActiveSession);
|
||||
@@ -129,10 +107,10 @@ export default function SetupScreen() {
|
||||
}
|
||||
|
||||
setSetupState({
|
||||
subjectId: subjectIdParam ?? subjectResult.data?.sId ?? null,
|
||||
assignmentId: assignmentIdParam ?? assignmentResult.data?.aId ?? null,
|
||||
taskId: taskIdParam ?? taskResult.data?.tId ?? null,
|
||||
completedFocusSessions: focusSessionResult.count ?? 0,
|
||||
subjectId: subjectIdParam ?? status.subjectId,
|
||||
assignmentId: assignmentIdParam ?? status.assignmentId,
|
||||
taskId: taskIdParam ?? status.taskId,
|
||||
completedFocusSessions: status.completedFocusSessions,
|
||||
});
|
||||
}, [assignmentIdParam, session?.user.id, subjectIdParam, taskIdParam]);
|
||||
|
||||
@@ -142,7 +120,7 @@ export default function SetupScreen() {
|
||||
}, [loadSetupState])
|
||||
);
|
||||
|
||||
const currentStep = useMemo<SetupStepKey>(() => {
|
||||
const currentStep: SetupStepKey = (() => {
|
||||
if (!setupState.subjectId) {
|
||||
return 'subject';
|
||||
}
|
||||
@@ -156,7 +134,7 @@ export default function SetupScreen() {
|
||||
}
|
||||
|
||||
return 'sprint';
|
||||
}, [setupState]);
|
||||
})();
|
||||
|
||||
const isSetupComplete =
|
||||
setupState.taskId !== null && setupState.completedFocusSessions > 0;
|
||||
@@ -223,14 +201,28 @@ export default function SetupScreen() {
|
||||
setActiveSession(null);
|
||||
}
|
||||
|
||||
const shouldUseDemoSprint = session?.user.id
|
||||
? !(await GetSetupSprintDemoUsed(session.user.id))
|
||||
: false;
|
||||
|
||||
if (shouldUseDemoSprint && session?.user.id) {
|
||||
await SaveSetupSprintDemoUsed(session.user.id);
|
||||
}
|
||||
|
||||
router.push({
|
||||
pathname: '/task/timer',
|
||||
params: {
|
||||
tId: setupState.taskId,
|
||||
durationSeconds: '5',
|
||||
},
|
||||
params: shouldUseDemoSprint
|
||||
? {
|
||||
tId: setupState.taskId,
|
||||
durationSeconds: '5',
|
||||
onboardingDemo: 'true',
|
||||
}
|
||||
: {
|
||||
tId: setupState.taskId,
|
||||
durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES),
|
||||
},
|
||||
});
|
||||
}, [currentStep, isSetupComplete, setupState]);
|
||||
}, [currentStep, isSetupComplete, session?.user.id, setupState]);
|
||||
|
||||
const primaryLabel = isSetupComplete
|
||||
? 'Go to dashboard'
|
||||
|
||||
@@ -139,14 +139,16 @@ export default function TimerScreen() {
|
||||
const cancelHoldIdRef = React.useRef(0);
|
||||
const cancelHoldStartedAtRef = React.useRef(0);
|
||||
|
||||
const { tId, sessionType: sessionTypeParam, durationMinutes, durationSeconds, returnTaskId, chooseDuration } = useLocalSearchParams<{
|
||||
const { tId, sessionType: sessionTypeParam, durationMinutes, durationSeconds, returnTaskId, chooseDuration, onboardingDemo } = useLocalSearchParams<{
|
||||
tId?: string;
|
||||
sessionType?: SessionType;
|
||||
durationMinutes?: string;
|
||||
durationSeconds?: string;
|
||||
returnTaskId?: string;
|
||||
chooseDuration?: string;
|
||||
onboardingDemo?: string;
|
||||
}>();
|
||||
const isOnboardingDemo = onboardingDemo === 'true';
|
||||
const timerOverlayHeight = Math.max(containerHeight, 1);
|
||||
const timerOverlayOffscreenY = timerOverlayHeight + 1000;
|
||||
const selectedSessionType: SessionType = sessionTypeParam ?? 'focus';
|
||||
@@ -484,13 +486,18 @@ export default function TimerScreen() {
|
||||
|
||||
setIsRunning(false);
|
||||
resetSessionValues();
|
||||
await finalizeSprintSession('completed', completedSession);
|
||||
|
||||
if (isOnboardingDemo && completedSessionType === 'focus') {
|
||||
router.replace('/');
|
||||
return;
|
||||
}
|
||||
|
||||
setPostSessionPrompt({
|
||||
completedSessionType,
|
||||
returnTaskId: completedReturnTaskId,
|
||||
nextBreakType,
|
||||
});
|
||||
|
||||
await finalizeSprintSession('completed', completedSession);
|
||||
})();
|
||||
});
|
||||
});
|
||||
@@ -502,6 +509,7 @@ export default function TimerScreen() {
|
||||
currentSessionType,
|
||||
finalizeSprintSession,
|
||||
focusModeAnimation,
|
||||
isOnboardingDemo,
|
||||
resetSessionValues,
|
||||
syncStudyCycleAfterCompletion,
|
||||
taskDetailsAnimation,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defaultStyles } from '@/constants/defaultStyles';
|
||||
import { SaveSetupSprintDemoUsed } from '@/lib/asyncStorage';
|
||||
import { CheckAssignmentCompletion } from '@/lib/progress';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import type { Task } from '@/lib/types';
|
||||
@@ -125,9 +126,14 @@ export default function UpsertTask() {
|
||||
SetIsSaving(false);
|
||||
|
||||
if (!isEditMode && isSetupFlow && result.data?.tId) {
|
||||
await SaveSetupSprintDemoUsed(data.user.id);
|
||||
router.replace({
|
||||
pathname: '/task/timer',
|
||||
params: { tId: result.data.tId },
|
||||
params: {
|
||||
tId: result.data.tId,
|
||||
durationSeconds: '5',
|
||||
onboardingDemo: 'true',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user