From eb92029cb0db66e143f9e9887bfb67c317752f8a Mon Sep 17 00:00:00 2001 From: Adam Xu Date: Thu, 26 Feb 2026 10:23:33 -0800 Subject: [PATCH 1/2] add mobile onboarding route and bridge integration --- src/app/mobile/onboarding/page.tsx | 7 ++ src/app/onboarding/page.tsx | 2 +- src/components/LayoutWrapper.tsx | 11 ++- src/components/onboarding/ConnectLmsStep.tsx | 21 ++++- .../onboarding/OnboardingController.tsx | 53 ++++++++++-- src/lib/mobileBridge.ts | 80 +++++++++++++++++++ src/types/mobileBridge.d.ts | 16 ++++ 7 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 src/app/mobile/onboarding/page.tsx create mode 100644 src/lib/mobileBridge.ts create mode 100644 src/types/mobileBridge.d.ts diff --git a/src/app/mobile/onboarding/page.tsx b/src/app/mobile/onboarding/page.tsx new file mode 100644 index 0000000..408df0b --- /dev/null +++ b/src/app/mobile/onboarding/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { OnboardingController } from '@/components/onboarding/OnboardingController'; + +export default function MobileOnboardingPage() { + return ; +} diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index ac35f64..8a861ef 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -3,5 +3,5 @@ import { OnboardingController } from '@/components/onboarding/OnboardingController'; export default function OnboardingPage() { - return ; + return ; } diff --git a/src/components/LayoutWrapper.tsx b/src/components/LayoutWrapper.tsx index bc3210d..17db1d7 100644 --- a/src/components/LayoutWrapper.tsx +++ b/src/components/LayoutWrapper.tsx @@ -13,10 +13,12 @@ type AuthState = { export function LayoutWrapper({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const [authState, setAuthState] = useState(null); + const isHelpRoute = pathname.startsWith('/help'); + const isMobileOnboardingRoute = pathname.startsWith('/mobile/onboarding'); useEffect(() => { // Skip auth check for help/docs routes - if (pathname.startsWith('/help')) { + if (isHelpRoute) { return; } @@ -40,10 +42,10 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { }; checkAuth(); - }, [pathname]); + }, [isHelpRoute, pathname]); // Bypass auth check for help/docs routes - if (pathname.startsWith('/help')) { + if (isHelpRoute) { return <>{children}; } @@ -54,6 +56,9 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { // If authenticated if (authState.isAuthenticated) { + if (isMobileOnboardingRoute) { + return <>{children}; + } // Check if onboarding is incomplete if (authState.onboardingStep && authState.onboardingStep !== 'completed') { return ; diff --git a/src/components/onboarding/ConnectLmsStep.tsx b/src/components/onboarding/ConnectLmsStep.tsx index 2b16bde..4947c57 100644 --- a/src/components/onboarding/ConnectLmsStep.tsx +++ b/src/components/onboarding/ConnectLmsStep.tsx @@ -3,10 +3,17 @@ import { useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { OnboardingSlide } from './OnboardingSlide'; +import { mobileBridge } from '@/lib/mobileBridge'; const BACKGROUND_COLOR = '#2563eb'; -export function ConnectLmsStep() { +type OnboardingMode = 'web' | 'mobile'; + +interface ConnectLmsStepProps { + mode?: OnboardingMode; +} + +export function ConnectLmsStep({ mode = 'web' }: ConnectLmsStepProps) { const searchParams = useSearchParams(); const error = searchParams.get('error'); const [overrideOpen, setOverrideOpen] = useState(false); @@ -22,7 +29,13 @@ export function ConnectLmsStep() { }; }, []); - const handleConnect = () => { + const handleConnect = async () => { + if (mode === 'mobile') { + const bridged = await mobileBridge.startSchoologyOAuth(); + if (bridged) { + return; + } + } window.location.href = `${process.env.NEXT_PUBLIC_BACKEND_URL}/oauth/schoology/start`; }; @@ -87,7 +100,9 @@ export function ConnectLmsStep() {

)} + + + ); + } return null; } @@ -34,7 +77,7 @@ export function OnboardingController() { case 'welcome': return ; case 'connect_lms': - return ; + return ; case 'smart_consent': return ; case 'completed': diff --git a/src/lib/mobileBridge.ts b/src/lib/mobileBridge.ts new file mode 100644 index 0000000..26d51fc --- /dev/null +++ b/src/lib/mobileBridge.ts @@ -0,0 +1,80 @@ +export type MobileBridgeContext = { + platform?: string; + appVersion?: string; + buildNumber?: string; + bridgeVersion?: string; + [key: string]: unknown; +}; + +type MobileBridgeV1 = { + startSchoologyOAuth: () => void | Promise; + openExternalURL: (url: string) => void | Promise; + onboardingComplete: () => void | Promise; + getContext: () => MobileBridgeContext | Promise; +}; + +type PartialMobileBridgeV1 = Partial; + +function getBridge(): PartialMobileBridgeV1 | null { + if (typeof window === 'undefined') { + return null; + } + return window.mobileBridge?.v1 ?? null; +} + +export const mobileBridge = { + isAvailable(): boolean { + return getBridge() !== null; + }, + + async startSchoologyOAuth(): Promise { + const bridge = getBridge(); + if (!bridge?.startSchoologyOAuth) { + return false; + } + try { + await bridge.startSchoologyOAuth(); + return true; + } catch { + return false; + } + }, + + async openExternalURL(url: string): Promise { + const bridge = getBridge(); + if (!bridge?.openExternalURL) { + return false; + } + try { + await bridge.openExternalURL(url); + return true; + } catch { + return false; + } + }, + + async onboardingComplete(): Promise { + const bridge = getBridge(); + if (!bridge?.onboardingComplete) { + return false; + } + try { + await bridge.onboardingComplete(); + return true; + } catch { + return false; + } + }, + + async getContext(): Promise { + const bridge = getBridge(); + if (!bridge?.getContext) { + return null; + } + try { + return await bridge.getContext(); + } catch { + return null; + } + }, +}; diff --git a/src/types/mobileBridge.d.ts b/src/types/mobileBridge.d.ts new file mode 100644 index 0000000..e1cc1a5 --- /dev/null +++ b/src/types/mobileBridge.d.ts @@ -0,0 +1,16 @@ +import type { MobileBridgeContext } from '@/lib/mobileBridge'; + +declare global { + interface Window { + mobileBridge?: { + v1?: { + startSchoologyOAuth?: () => void | Promise; + openExternalURL?: (url: string) => void | Promise; + onboardingComplete?: () => void | Promise; + getContext?: () => MobileBridgeContext | Promise; + }; + }; + } +} + +export {}; From 0ccfa95b2bb8d736cc717e8963c434774abab2f6 Mon Sep 17 00:00:00 2001 From: Adam Xu Date: Thu, 26 Feb 2026 10:26:42 -0800 Subject: [PATCH 2/2] Add literature course mapping and icons --- public/icons/courses/pencil.line.svg | 12 ++++++++++++ public/icons/courses/text.book.closed.svg | 13 +++++++++++++ public/meta/course-match.json | 11 +++++++++++ 3 files changed, 36 insertions(+) create mode 100644 public/icons/courses/pencil.line.svg create mode 100644 public/icons/courses/text.book.closed.svg diff --git a/public/icons/courses/pencil.line.svg b/public/icons/courses/pencil.line.svg new file mode 100644 index 0000000..48589b8 --- /dev/null +++ b/public/icons/courses/pencil.line.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/icons/courses/text.book.closed.svg b/public/icons/courses/text.book.closed.svg new file mode 100644 index 0000000..0ad9386 --- /dev/null +++ b/public/icons/courses/text.book.closed.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/meta/course-match.json b/public/meta/course-match.json index fc27657..6ad3918 100644 --- a/public/meta/course-match.json +++ b/public/meta/course-match.json @@ -64,6 +64,17 @@ ], "icon": "/icons/courses/cs.svg", "color": "#00a3ff" + }, + { + "subject": "Literature", + "keywords": [ + { + "keyword": "Literature", + "weight": 1 + } + ], + "icon": "/icons/courses/text.book.closed.svg", + "color": "#3662e3" } ], "catchAll": {