Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement user onboarding process #105

Merged
merged 28 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6d94aa6
Add onboarding forms
michaeleii Oct 22, 2024
69371af
add email script for development environment
scottchen98 Oct 24, 2024
7c18e8f
rename actions file to invitations and add action for inviting user
scottchen98 Oct 24, 2024
b96fc2b
add LTUserInvitation email template
scottchen98 Oct 24, 2024
b611b88
Add emails for inviting user and edit email styling
michaeleii Oct 24, 2024
8732d55
refactor invitations module and add functionality for deleting existi…
scottchen98 Oct 24, 2024
d672427
Merge branch '99-implement-invitations' of https://github.com/Vero-Ve…
scottchen98 Oct 24, 2024
f572832
add functionality for deleting existing invitations adn resend user i…
scottchen98 Oct 25, 2024
5a6e6d2
refactor resend user invitations
scottchen98 Oct 25, 2024
c5d0a66
refactor LTUserInvitation component and add functionality for invitin…
scottchen98 Oct 25, 2024
eb2286d
relocate createUser, createOrganization,and createMember functions to…
scottchen98 Oct 25, 2024
4b56977
add getMemberByOrgIdAndRole function
scottchen98 Oct 25, 2024
9c38850
add getOrganizationById function
scottchen98 Oct 25, 2024
361d251
add getUserById function
scottchen98 Oct 25, 2024
06b4eaa
refactor LTUserInvitation component
scottchen98 Oct 25, 2024
42b9747
remove duplicate createMember function
scottchen98 Oct 25, 2024
a189a1b
add sendOwnerInvitation mutation
scottchen98 Oct 26, 2024
7cdb3a0
add updateMemberRole mutation
scottchen98 Oct 26, 2024
6324aa3
refactor createUser function to make the 'name' argument optional
scottchen98 Oct 26, 2024
cefbc61
create new user and member entries when the invitation is accepted
scottchen98 Oct 27, 2024
9d02001
refactor updateMemberRole mutation to check user's role before updati…
scottchen98 Oct 28, 2024
bae3c33
refactor deleteInvitation mutation to use ctx.db.delete instead of in…
scottchen98 Oct 28, 2024
888200a
include utils module
scottchen98 Oct 28, 2024
25b7c94
check user's role before performing actions
scottchen98 Oct 28, 2024
31d8218
Merge branch '99-implement-invitations' into 101-user-onboarding-process
michaeleii Oct 28, 2024
93d04fb
Implement user onboarding process
michaeleii Oct 28, 2024
9d6a36a
Update onboarding process
michaeleii Oct 28, 2024
4ceebc1
Remove email script
michaeleii Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/(tabs)/goals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import { Text } from "~/components/ui/text";
import { Button } from "~/components/ui/button";
import { Link, SplashScreen } from "expo-router";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useRef, useState } from "react";
import { fontFamily } from "~/lib/font";
import { Plus } from "lucide-react-native";
import { Separator } from "~/components/ui/separator";
Expand Down Expand Up @@ -235,7 +236,7 @@
return null;
}

const handleLogPress = (e: any) => {

Check warning on line 239 in app/(tabs)/goals.tsx

View workflow job for this annotation

GitHub Actions / npm run lint

Unexpected any. Specify a different type
e.stopPropagation(); // Prevent parent navigation

if (latestLog.isComplete) {
Expand All @@ -244,7 +245,11 @@
}

router.push({
pathname: `/goals/${goal._id}/${latestLog._id}/start/logProgress`,
pathname: `/goals/[goalId]/[goalLogId]/start/logProgress`,
params: {
goalId: goal._id,
goalLogId: latestLog._id,
},
});
};

Expand Down
14 changes: 14 additions & 0 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ export default function RootLayout() {
animation: "fade",
}}
/>
<Stack.Screen
name="onboarding/profile/index"
options={{
headerShown: false,
animation: "slide_from_right",
}}
/>
<Stack.Screen
name="onboarding/name/index"
options={{
headerShown: false,
animation: "slide_from_right",
}}
/>
<Stack.Screen
name="sign-in"
options={{
Expand Down
13 changes: 11 additions & 2 deletions app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Redirect } from "expo-router";
import { Authenticated, Unauthenticated } from "convex/react";
import { Authenticated, Unauthenticated, useQuery } from "convex/react";
import Onboarding from "./onboarding";
import { api } from "~/convex/_generated/api";

export default function SignInPage() {
return (
Expand All @@ -9,8 +10,16 @@ export default function SignInPage() {
<Onboarding />
</Unauthenticated>
<Authenticated>
<Redirect href="/goals" />
<CrossRoad />
</Authenticated>
</>
);
}

function CrossRoad() {
const user = useQuery(api.users.currentUser);
if (!user) {
return null;
}
return <Redirect href={user.hasOnboarded ? "/goals" : "/onboarding/name"} />;
}
89 changes: 89 additions & 0 deletions app/onboarding/name/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useMutation } from "convex/react";
import { router, SplashScreen } from "expo-router";
import { AlertCircle } from "lucide-react-native";
import { useEffect, useState } from "react";
import { View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { SafeAreaView } from "react-native-safe-area-context";
import FormSubmitButton from "~/components/form-submit-button";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { Input } from "~/components/ui/input";
import { Text } from "~/components/ui/text";
import { api } from "~/convex/_generated/api";

export default function OnboardingNamePage() {
useEffect(() => {
SplashScreen.hideAsync();
}, []);

return (
<SafeAreaView
style={{
flex: 1,
backgroundColor: "#082139",
justifyContent: "center",
}}
>
<KeyboardAwareScrollView
contentContainerStyle={{
flexGrow: 1,
justifyContent: "center",
}}
>
<Text className="mb-6 text-center text-2xl font-bold">
What is your name?
</Text>
<OnboardingProfileForm />
</KeyboardAwareScrollView>
</SafeAreaView>
);
}

function OnboardingProfileForm() {
const updateUserName = useMutation(api.users.updateUserName);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState("");
const [name, setName] = useState("");

const handleSaveName = async () => {
try {
setIsPending(true);
if (name.trim().length < 2) {
throw new Error("Your name is too short");
}
const nameRegex = /^[a-zA-Z]+$/;
if (nameRegex.test(name.trim())) {
throw new Error("Your name can only contain letters");
}
await updateUserName({ name });
router.replace("/onboarding/profile");
} catch (error) {
if (error instanceof Error) {
setError(error.message);
}
} finally {
setIsPending(false);
}
};
return (
<View className="gap-6 px-4">
{!!error && (
<Alert icon={AlertCircle} variant="destructive" className="max-w-xl">
<AlertTitle>Something went wrong!</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<View className="gap-2">
<Input
inputMode="text"
placeholder="John Doe"
value={name}
onChangeText={setName}
/>
</View>
<FormSubmitButton isPending={isPending} onPress={handleSaveName}>
Continue
</FormSubmitButton>
</View>
);
}
163 changes: 163 additions & 0 deletions app/onboarding/profile/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Link, router } from "expo-router";
import { AlertCircle, CalendarDays } from "lucide-react-native";
import { useState } from "react";
import { Pressable, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import DateTimePicker from "react-native-modal-datetime-picker";
import { SafeAreaView } from "react-native-safe-area-context";
import FormSubmitButton from "~/components/form-submit-button";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Text } from "~/components/ui/text";
import { formatDate } from "~/lib/date";
import { fontFamily } from "~/lib/font";
import { useProfileFormStore } from "~/app/profile/edit/profile-form-store";
import { useShallow } from "zustand/react/shallow";
import { Button } from "~/components/ui/button";
import { useMutation } from "convex/react";
import { api } from "~/convex/_generated/api";

export default function OnboardingProfilePage() {
return (
<SafeAreaView
style={{
flex: 1,
backgroundColor: "#082139",
justifyContent: "center",
}}
>
<KeyboardAwareScrollView
contentContainerStyle={{
flexGrow: 1,
justifyContent: "center",
}}
>
<Text className="mb-6 text-center text-2xl font-bold">
Tell us more about you!
</Text>
<OnboardingProfileForm />
</KeyboardAwareScrollView>
</SafeAreaView>
);
}

function OnboardingProfileForm() {
const updateProfile = useMutation(api.users.updatePartialProfile);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState("");
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [dob, setDob, weight, setWeight, height, setHeight] =
useProfileFormStore(
useShallow((s) => [
s.dob,
s.setDob,
s.weight,
s.setWeight,
s.height,
s.setHeight,
])
);
const showDatePicker = () => {
setDatePickerVisibility(true);
};

const hideDatePicker = () => {
setDatePickerVisibility(false);
};

const handleConfirm = (date: Date) => {
setDob(date);
hideDatePicker();
};

const handleSaveProfile = async () => {
try {
setIsPending(true);
updateProfile({
dob: dob.getTime(),
weight: Number(weight),
height: Number(height),
});
router.replace("/goals");
} catch (error) {
if (error instanceof Error) {
setError(error.message);
}
} finally {
setIsPending(false);
}
};
return (
<View className="gap-6 px-4">
{!!error && (
<Alert icon={AlertCircle} variant="destructive" className="max-w-xl">
<AlertTitle>Something went wrong!</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Pressable onPress={showDatePicker}>
<View className="rounded-xl border border-input">
<View className="flex flex-row items-center gap-4 p-5">
<View className="rounded-xl bg-[#1E00FE] p-2">
<CalendarDays color="#fff" />
</View>
<View>
<Text
className="text-xs text-muted-foreground"
style={{
fontFamily: fontFamily.openSans.semiBold,
letterSpacing: 0.5,
}}
>
Date of Birth
</Text>
<Text
style={{
fontFamily: fontFamily.openSans.semiBold,
}}
>
{formatDate(dob)}
</Text>
</View>
</View>
<DateTimePicker
display="inline"
isVisible={isDatePickerVisible}
mode="date"
date={dob}
maximumDate={new Date()}
onConfirm={handleConfirm}
onCancel={hideDatePicker}
/>
</View>
</Pressable>
<View className="gap-2">
<Label nativeID="height">Height (cm)</Label>
<Input
placeholder="Enter your height"
value={height}
onChangeText={setHeight}
keyboardType="numeric"
/>
</View>
<View className="gap-2">
<Label nativeID="weight">Weight (kg)</Label>
<Input
placeholder="Enter your weight"
value={weight}
onChangeText={setWeight}
keyboardType="numeric"
/>
</View>
<FormSubmitButton isPending={isPending} onPress={handleSaveProfile}>
Continue
</FormSubmitButton>
<Link href="/goals" asChild>
<Button variant="link">
<Text>Skip for now</Text>
</Button>
</Link>
</View>
);
}
10 changes: 8 additions & 2 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ import type {
FunctionReference,
} from "convex/server";
import type * as ResendOTP from "../ResendOTP.js";
import type * as actions from "../actions.js";
import type * as auth from "../auth.js";
import type * as challenges from "../challenges.js";
import type * as emails_LTLoginOTP from "../emails/LTLoginOTP.js";
import type * as emails_LTUserInvitation from "../emails/LTUserInvitation.js";
import type * as emails_LTWelcome from "../emails/LTWelcome.js";
import type * as goalLogs from "../goalLogs.js";
import type * as goals from "../goals.js";
import type * as http from "../http.js";
import type * as invitations from "../invitations.js";
import type * as members from "../members.js";
import type * as organizations from "../organizations.js";
import type * as users from "../users.js";
import type * as utils from "../utils.js";

/**
* A utility for referencing Convex functions in your app's API.
Expand All @@ -37,16 +40,19 @@ import type * as users from "../users.js";
*/
declare const fullApi: ApiFromModules<{
ResendOTP: typeof ResendOTP;
actions: typeof actions;
auth: typeof auth;
challenges: typeof challenges;
"emails/LTLoginOTP": typeof emails_LTLoginOTP;
"emails/LTUserInvitation": typeof emails_LTUserInvitation;
"emails/LTWelcome": typeof emails_LTWelcome;
goalLogs: typeof goalLogs;
goals: typeof goals;
http: typeof http;
invitations: typeof invitations;
members: typeof members;
organizations: typeof organizations;
users: typeof users;
utils: typeof utils;
}>;
export declare const api: FilterApi<
typeof fullApi,
Expand Down
Loading
Loading