From 7f1c02098a69c676af127040e4b5c0a46a5a9b96 Mon Sep 17 00:00:00 2001 From: lcaohoanq Date: Thu, 23 Jul 2026 10:19:25 +0700 Subject: [PATCH 1/2] feat: add expo module --- apps/mobile/.env.example | 5 + apps/mobile/README.md | 5 + apps/mobile/app.json | 33 + apps/mobile/app/(auth)/_layout.tsx | 4 + apps/mobile/app/(auth)/login.tsx | 95 + apps/mobile/app/(auth)/register.tsx | 81 + apps/mobile/app/(tabs)/_layout.tsx | 32 + apps/mobile/app/(tabs)/courses.tsx | 36 + apps/mobile/app/(tabs)/index.tsx | 42 + apps/mobile/app/(tabs)/learning.tsx | 51 + apps/mobile/app/(tabs)/profile.tsx | 38 + apps/mobile/app/_layout.tsx | 41 + apps/mobile/app/certificate/[courseId].tsx | 63 + apps/mobile/app/course/[id].tsx | 88 + apps/mobile/app/index.tsx | 8 + .../app/lesson/[courseId]/[lectureId].tsx | 155 + apps/mobile/app/open-mobile.tsx | 11 + apps/mobile/app/profile/edit.tsx | 54 + apps/mobile/components/course-card.tsx | 44 + apps/mobile/components/ui.tsx | 98 + apps/mobile/eas.json | 9 + apps/mobile/lib/api.ts | 85 + apps/mobile/lib/last-lecture.ts | 8 + apps/mobile/lib/learner.ts | 35 + apps/mobile/lib/session.ts | 10 + apps/mobile/lib/types.ts | 56 + apps/mobile/package.json | 42 + apps/mobile/providers/auth.tsx | 55 + apps/mobile/tsconfig.json | 5 + pnpm-lock.yaml | 15250 +++++++++++----- 30 files changed, 11730 insertions(+), 4809 deletions(-) create mode 100644 apps/mobile/.env.example create mode 100644 apps/mobile/README.md create mode 100644 apps/mobile/app.json create mode 100644 apps/mobile/app/(auth)/_layout.tsx create mode 100644 apps/mobile/app/(auth)/login.tsx create mode 100644 apps/mobile/app/(auth)/register.tsx create mode 100644 apps/mobile/app/(tabs)/_layout.tsx create mode 100644 apps/mobile/app/(tabs)/courses.tsx create mode 100644 apps/mobile/app/(tabs)/index.tsx create mode 100644 apps/mobile/app/(tabs)/learning.tsx create mode 100644 apps/mobile/app/(tabs)/profile.tsx create mode 100644 apps/mobile/app/_layout.tsx create mode 100644 apps/mobile/app/certificate/[courseId].tsx create mode 100644 apps/mobile/app/course/[id].tsx create mode 100644 apps/mobile/app/index.tsx create mode 100644 apps/mobile/app/lesson/[courseId]/[lectureId].tsx create mode 100644 apps/mobile/app/open-mobile.tsx create mode 100644 apps/mobile/app/profile/edit.tsx create mode 100644 apps/mobile/components/course-card.tsx create mode 100644 apps/mobile/components/ui.tsx create mode 100644 apps/mobile/eas.json create mode 100644 apps/mobile/lib/api.ts create mode 100644 apps/mobile/lib/last-lecture.ts create mode 100644 apps/mobile/lib/learner.ts create mode 100644 apps/mobile/lib/session.ts create mode 100644 apps/mobile/lib/types.ts create mode 100644 apps/mobile/package.json create mode 100644 apps/mobile/providers/auth.tsx create mode 100644 apps/mobile/tsconfig.json diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example new file mode 100644 index 00000000..2381b0c9 --- /dev/null +++ b/apps/mobile/.env.example @@ -0,0 +1,5 @@ +EXPO_PUBLIC_API_BASE_URL=https://api.example.com +EXPO_PUBLIC_GOOGLE_EXPO_CLIENT_ID= +EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID= +EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID= +# Set EAS project metadata in app.json locally; never commit client secrets. diff --git a/apps/mobile/README.md b/apps/mobile/README.md new file mode 100644 index 00000000..19d4a216 --- /dev/null +++ b/apps/mobile/README.md @@ -0,0 +1,5 @@ +# BIT Learning mobile + +Expo managed learner app. Copy `.env.example` to `.env` and set the API URL and public Google OAuth client IDs. EAS project ID, signing credentials, and OAuth secrets are deliberately not committed. + +The backend must allow this app's OAuth redirect URIs and serve Android Asset Links and `apple-app-site-association` for `bit-learning.lch.id.vn`. diff --git a/apps/mobile/app.json b/apps/mobile/app.json new file mode 100644 index 00000000..a1ad8b49 --- /dev/null +++ b/apps/mobile/app.json @@ -0,0 +1,33 @@ +{ + "expo": { + "name": "BIT Learning", + "slug": "bit-learning", + "scheme": "bitlearning", + "version": "1.0.0", + "orientation": "portrait", + "userInterfaceStyle": "light", + "android": { + "package": "com.app.bitlearning", + "intentFilters": [ + { + "action": "VIEW", + "data": [ + { + "scheme": "https", + "host": "bit-learning.lch.id.vn", + "pathPrefix": "/open-mobile" + } + ], + "category": ["BROWSABLE", "DEFAULT"] + } + ] + }, + "ios": { + "bundleIdentifier": "com.app.bitlearning", + "associatedDomains": ["applinks:bit-learning.lch.id.vn"] + }, + "plugins": ["expo-router", "expo-secure-store", "expo-video"], + "experiments": { "typedRoutes": true }, + "extra": { "eas": { "projectId": "REPLACE_WITH_EAS_PROJECT_ID" } } + } +} diff --git a/apps/mobile/app/(auth)/_layout.tsx b/apps/mobile/app/(auth)/_layout.tsx new file mode 100644 index 00000000..71603f50 --- /dev/null +++ b/apps/mobile/app/(auth)/_layout.tsx @@ -0,0 +1,4 @@ +import { Stack } from "expo-router"; +export default function AuthLayout() { + return ; +} diff --git a/apps/mobile/app/(auth)/login.tsx b/apps/mobile/app/(auth)/login.tsx new file mode 100644 index 00000000..5e7dd7ce --- /dev/null +++ b/apps/mobile/app/(auth)/login.tsx @@ -0,0 +1,95 @@ +import * as Google from "expo-auth-session/providers/google"; +import { Link, useRouter } from "expo-router"; +import { useEffect, useState } from "react"; +import { StyleSheet, Text, View } from "react-native"; +import { Button, ErrorText, Field, colors } from "@/components/ui"; +import { api } from "@/lib/api"; +import { useAuth } from "@/providers/auth"; +import type { Session } from "@/lib/types"; +export default function Login() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const { signIn } = useAuth(); + const router = useRouter(); + const [request, response, promptAsync] = Google.useAuthRequest({ + clientId: process.env.EXPO_PUBLIC_GOOGLE_EXPO_CLIENT_ID, + androidClientId: process.env.EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID, + iosClientId: process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID, + }); + useEffect(() => { + if (response?.type === "success") + api + .post("/auth/oauth2/google", { code: response.params.code }) + .then(signIn) + .then(() => router.replace("/(tabs)")) + .catch((e) => setError(e.message)); + }, [response, signIn, router]); + const login = async () => { + if (!email || !password) return setError("Enter your email and password."); + setBusy(true); + setError(""); + try { + await signIn( + await api.post("/auth/login-user", { + email, + password, + role: "STUDENT", + }), + ); + router.replace("/(tabs)"); + } catch (e) { + setError(e instanceof Error ? e.message : "Unable to sign in"); + } finally { + setBusy(false); + } + }; + return ( + + BIT Learning + Welcome back + + + {error ? {error} : null} +