diff --git a/app/(onboarding)/_layout.tsx b/app/(onboarding)/_layout.tsx index 8867eb2bc..b10796ccf 100644 --- a/app/(onboarding)/_layout.tsx +++ b/app/(onboarding)/_layout.tsx @@ -6,140 +6,145 @@ import { Stack } from '@/utils/native/AnimatedNavigator'; import { screenOptions } from "@/utils/theme/ScreenOptions"; export default function OnboardingLayout() { - const newScreenOptions = React.useMemo(() => ({ - ...screenOptions, - headerShown: false, - headerBackVisible: true, - headerTitle: '', - gestureEnabled: false, - headerTransparent: true, - headerTintColor: "#FFFFFF", - headerBackButtonDisplayMode: "minimal", - headerBackButtonMenuEnabled: false - }), []); + const newScreenOptions = React.useMemo(() => ({ + ...screenOptions, + headerShown: false, + headerBackVisible: true, + headerTitle: '', + gestureEnabled: false, + headerTransparent: true, + headerTintColor: "#FFFFFF", + headerBackButtonDisplayMode: "minimal", + headerBackButtonMenuEnabled: false + }), []); - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } \ No newline at end of file diff --git a/app/(onboarding)/utils/constants.tsx b/app/(onboarding)/utils/constants.tsx index a7c58faad..316a6d286 100644 --- a/app/(onboarding)/utils/constants.tsx +++ b/app/(onboarding)/utils/constants.tsx @@ -58,6 +58,17 @@ export function GetSupportedServices(redirect: (path: { pathname: string, option variant: 'service' as const, color: 'light' as const, }, + { + name: "webuntis", + title: "Web Untis", + type: "main", + image: require("@/assets/images/service_webuntis.png"), + onPress: () => { + redirect({ pathname: './webuntis/credentials', options: { service: Services.WEBUNTIS } }); + }, + variant: 'service' as const, + color: 'light' as const, + }, { name: "separator", title: "separator", diff --git a/app/(onboarding)/webuntis/credentials.tsx b/app/(onboarding)/webuntis/credentials.tsx new file mode 100644 index 000000000..bb51c28e6 --- /dev/null +++ b/app/(onboarding)/webuntis/credentials.tsx @@ -0,0 +1,290 @@ +import { useTheme } from "@react-navigation/native"; +import { router } from "expo-router"; +import React, { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Keyboard, + KeyboardAvoidingView, + Platform, + Pressable, + View, +} from "react-native"; +import Reanimated, { + useSharedValue, + withTiming, +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Credentials, WebUntisClient } from "webuntis-client"; + +import OnboardingBackButton from "@/components/onboarding/OnboardingBackButton"; +import OnboardingInput from "@/components/onboarding/OnboardingInput"; +import { useAccountStore } from "@/stores/account"; +import { Account, Services } from "@/stores/account/types"; +import { useAlert } from "@/ui/components/AlertProvider"; +import AnimatedPressable from "@/ui/components/AnimatedPressable"; +import Button from "@/ui/components/Button"; +import Stack from "@/ui/components/Stack"; +import Typography from "@/ui/components/Typography"; +import uuid from "@/utils/uuid/uuid"; + +const ANIMATION_DURATION = 170; +export const PlatformPressable = + Platform.OS === "android" ? Pressable : AnimatedPressable; + +export default function WebUntisLoginWithCredentials() { + const insets = useSafeAreaInsets(); + const theme = useTheme(); + + const alert = useAlert(); + const { t } = useTranslation(); + + const [url, setUrl] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const [isLoggingIn, setIsLoggingIn] = useState(false); + + const opacity = useSharedValue(1); + const scale = useSharedValue(1); + + const keyboardListeners = useMemo( + () => ({ + show: () => { + "worklet"; + opacity.value = withTiming(0, { duration: ANIMATION_DURATION }); + scale.value = withTiming(0.8, { duration: ANIMATION_DURATION }); + }, + hide: () => { + "worklet"; + opacity.value = withTiming(1, { duration: ANIMATION_DURATION }); + scale.value = withTiming(1, { duration: ANIMATION_DURATION }); + }, + }), + [opacity] + ); + + useEffect(() => { + const showSub = Keyboard.addListener( + "keyboardWillShow", + keyboardListeners.show + ); + const hideSub = Keyboard.addListener( + "keyboardWillHide", + keyboardListeners.hide + ); + + return () => { + showSub.remove(); + hideSub.remove(); + }; + }, [keyboardListeners]); + + const handleLogin = async () => { + if (!url.trim() || !username.trim() || !password.trim()) { + alert.showAlert({ + title: t("ERROR_AUTHENTICATION"), + description: t("ERROR_MISSING_FIELDS"), + icon: "TriangleAlert", + color: "#D60046", + withoutNavbar: true, + }); + return; + } + + setIsLoggingIn(true); + Keyboard.dismiss(); + + const school = url.trim().split(".")[0]; + const identity = "PapillonApp"; + + const credentials = new Credentials(identity, school, username, password); + const client = new WebUntisClient(credentials); + + const accountId = uuid(); + const store = useAccountStore.getState(); + + try { + const session = await client.login(); + + if (!session) { + throw new Error("No session information returned from WebUntis"); + } + + await client.getAppData(); + + const displayName = client.getStudentDisplayName(); + const schoolName = client.getTenantDisplayName(); + + const account: Account = { + id: accountId, + firstName: displayName, + lastName: "", + schoolName: schoolName, + services: [ + { + id: accountId, + auth: { + accessToken: session.sessionId, + refreshToken: password, + additionals: { + school: school, + username: username, + url: url, + password: password, + }, + }, + serviceId: Services.WEBUNTIS, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + store.addAccount(account); + store.setLastUsedAccount(accountId); + + queueMicrotask(() => { + router.push({ + pathname: "../end/color", + params: { accountId }, + }); + }); + } catch (e) { + setIsLoggingIn(false); + + alert.showAlert({ + title: t("ERROR_AUTHENTICATION"), + description: t("ERROR_WEBUNTIS_LOGIN"), + icon: "TriangleAlert", + color: "#D60046", + technical: String(e), + withoutNavbar: true, + }); + } + }; + + return ( + + + + + + + {t("STEP")} 2 + + + {t("STEP_OUTOF")} 3 + + + + {t("ONBOARDING_LOGIN_CREDENTIALS")} Web Untis + + + + + + + + { + Keyboard.dismiss(); + + if ( + !isLoggingIn && + url.trim() && + username.trim() && + password.trim() + ) { + handleLogin(); + } + }, + returnKeyType: "done", + editable: !isLoggingIn, + }} + /> +