Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions openspec/changes/support-ms-teams-lessons/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-14
25 changes: 25 additions & 0 deletions openspec/changes/support-ms-teams-lessons/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Context

Users need the ability to join MS Teams video conference lessons directly from the application. We want to enable joining Teams lessons using an in-app WebView, while providing a seamless, auto-join experience that skips Microsoft's "Download App" gate pages.

## Goals / Non-Goals

**Goals:**
- Enable students to join MS Teams lessons via the "Attend Class" button.
- Provide a seamless experience by automatically bypassing the Teams "How do you want to join?" gate page.
- Ensure camera and microphone permissions are requested prior to joining.

**Non-Goals:**
- Reusing `LessonWebView` (it is designed for static content and lacks the necessary JS/Navigation flexibility for the Teams flow).

## Decisions

- **Dedicated WebView**: Created `TeamsVideoConferenceScreen` instead of reusing `LessonWebView`. This allows for unrestricted JavaScript execution and custom navigation handling.
- **Desktop User-Agent Spoofing**: We force a desktop Chrome User-Agent in the WebView. If we don't, Teams detects a mobile device and forces a "Download the Teams App" screen that cannot be bypassed.
- **JavaScript Auto-Join**: We use `TeamsAutoJoinHandler` to inject JavaScript on page load. The JS automatically finds and clicks the "Continue on this browser" button.
- **Loading Overlay**: To hide the intermediate gate page from the user, a loading overlay covers the WebView. It naturally drops via the `onPageFinished` navigation delegate only after the gate has been bypassed and the final lobby page has loaded.

## Risks / Trade-offs

- [Risk] MS Teams DOM changes. The JavaScript relies on specific CSS selectors (e.g., `[data-tid="joinOnWeb"]`). If Microsoft changes these, the auto-join will timeout. → Mitigation: We implemented text-based fallback selectors and a robust timeout mechanism that drops the loader and shows an error toast if it fails.
- [Risk] iOS vs Android Permission discrepancies. → Mitigation: We use `permission_handler` at the OS level before the WebView initializes, guaranteeing both platforms have hardware access before loading Teams.
21 changes: 21 additions & 0 deletions openspec/changes/support-ms-teams-lessons/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Why

Users need the ability to join MS Teams video conference lessons directly from the application. We want to enable joining Teams lessons using an in-app WebView, while providing a seamless, auto-join experience that skips Microsoft's "Download App" gate pages.

## What Changes

- Create a dedicated `TeamsVideoConferenceScreen` with a custom WebView to handle the complex auto-join flow.
- Inject Javascript via a `TeamsAutoJoinHandler` to seamlessly bypass the Microsoft Teams "Download App" interstitial gate.
- Update `VideoConferenceViewer` to route Teams meetings to this new dedicated screen.

## Capabilities

### New Capabilities
- `ms-teams-webview-join`: Seamless auto-join of MS Teams meetings via an in-app WebView without native SDK dependencies.

### Modified Capabilities
- (None)

## Impact

* **`packages/courses`**: Modifies `_joinMeeting` flow in `video_conference_viewer.dart` to push a new `TeamsVideoConferenceScreen` (`teams_web_view.dart`) instead of a generic browser.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## ADDED Requirements

### Requirement: MS Teams WebView Join
The system SHALL allow users to join MS Teams lessons using an in-app web view when tapping the "Attend Class" button.

#### Scenario: User clicks Attend Class on a Teams lesson
- **WHEN** the user taps "Attend Class" on a Video Conference lesson with the "teams" provider
- **THEN** the app requests Camera and Microphone permissions
- **AND** if granted, pushes a full-screen WebView loading the lesson's join URL wrapped in the standard `LessonDetailShell`
- **AND** the WebView permits necessary navigation redirects required by the MS Teams join flow

#### Scenario: User denies camera/microphone permissions
- **WHEN** the user denies permissions (or they are permanently denied)
- **THEN** the app displays a localized error toast ("Camera and microphone access is required. Enable them in Settings to join.")
- **AND** the app automatically pops the screen, returning the user to the syllabus

#### Scenario: User needs to exit the meeting
- **WHEN** the user is inside the Teams WebView (pre-join lobby or active call)
- **THEN** they can tap the native back button provided by the `LessonDetailShell` header to reliably exit the screen at any time

#### Scenario: Auto-join script fails
- **WHEN** the injected JavaScript for bypassing the Teams lobby fails
- **THEN** the error is silently reported to Sentry for tracking without disrupting the user's manual fallback flow
14 changes: 14 additions & 0 deletions openspec/changes/support-ms-teams-lessons/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## 1. Dedicated Teams WebView

- [x] 1.1 Create `TeamsVideoConferenceScreen` in `teams_web_view.dart` to handle the specific requirements of MS Teams (desktop user agent, custom navigation delegates).
- [x] 1.2 Implement `TeamsAutoJoinHandler` to inject JavaScript into the Teams gate page to automatically click "Continue on this browser".
- [x] 1.3 Add `TeamsPermissionHandler` to request camera/microphone permissions before launching the WebView.
- [x] 1.4 Add loading overlay logic that naturally drops on `onPageFinished` after successful bypass.

## 2. Integration

- [x] 2.1 Update `_joinMeeting` in `VideoConferenceViewer` (`packages/courses/lib/widgets/lesson_detail/video_conference_viewer.dart`) to intercept Teams lessons and push the new `TeamsVideoConferenceScreen`.

## 3. Verification

- [x] 3.1 Run `flutter analyze` and confirm no compile or lint issues exist.
18 changes: 18 additions & 0 deletions packages/core/lib/generated/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5710,6 +5710,24 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Failed to join the meeting. Please try again.'**
String get liveStreamJoinFailed;

/// No description provided for @teamsPermissionRequired.
///
/// In en, this message translates to:
/// **'Camera and microphone access is required. Enable them in Settings to join.'**
String get teamsPermissionRequired;

/// No description provided for @teamsJoinMeetingLoading.
///
/// In en, this message translates to:
/// **'Joining meeting...'**
String get teamsJoinMeetingLoading;

/// No description provided for @teamsMissingJoinLink.
///
/// In en, this message translates to:
/// **'Meeting link is missing'**
String get teamsMissingJoinLink;
}

class _AppLocalizationsDelegate
Expand Down
10 changes: 10 additions & 0 deletions packages/core/lib/generated/l10n/app_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3158,4 +3158,14 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get liveStreamJoinFailed =>
'فشل الانضمام إلى الاجتماع. يُرجى المحاولة مرة أخرى.';

@override
String get teamsPermissionRequired =>
'يلزم الوصول إلى الكاميرا والميكروفون. قم بتمكينها في الإعدادات للانضمام.';

@override
String get teamsJoinMeetingLoading => 'جارِ الانضمام إلى الاجتماع...';

@override
String get teamsMissingJoinLink => 'رابط الاجتماع مفقود';
}
10 changes: 10 additions & 0 deletions packages/core/lib/generated/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3158,4 +3158,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get liveStreamJoinFailed =>
'Failed to join the meeting. Please try again.';

@override
String get teamsPermissionRequired =>
'Camera and microphone access is required. Enable them in Settings to join.';

@override
String get teamsJoinMeetingLoading => 'Joining meeting...';

@override
String get teamsMissingJoinLink => 'Meeting link is missing';
}
10 changes: 10 additions & 0 deletions packages/core/lib/generated/l10n/app_localizations_ml.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3212,4 +3212,14 @@ class AppLocalizationsMl extends AppLocalizations {
@override
String get liveStreamJoinFailed =>
'മീറ്റിംഗിൽ ചേരുന്നതിൽ പരാജയപ്പെട്ടു. വീണ്ടും ശ്രമിക്കുക.';

@override
String get teamsPermissionRequired =>
'ക്യാമറ, മൈക്രോഫോൺ ആക്സസ് ആവശ്യമാണ്. ചേരുന്നതിന് ക്രമീകരണങ്ങളിൽ അവ പ്രവർത്തനക്ഷമമാക്കുക.';

@override
String get teamsJoinMeetingLoading => 'മീറ്റിംഗിൽ ചേരുന്നു...';

@override
String get teamsMissingJoinLink => 'മീറ്റിംഗ് ലിങ്ക് ലഭ്യമല്ല';
}
10 changes: 10 additions & 0 deletions packages/core/lib/generated/l10n/app_localizations_ta.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3208,4 +3208,14 @@ class AppLocalizationsTa extends AppLocalizations {
@override
String get liveStreamJoinFailed =>
'கூட்டத்தில் சேர முடியவில்லை. மீண்டும் முயற்சிக்கவும்.';

@override
String get teamsPermissionRequired =>
'சேர, கேமரா மற்றும் மைக்ரோஃபோன் அணுகல் தேவை. அமைப்புகளில் அவற்றை இயக்கவும்.';

@override
String get teamsJoinMeetingLoading => 'கூட்டத்தில் சேர்கிறோம்...';

@override
String get teamsMissingJoinLink => 'சந்திப்பு இணைப்பு காணவில்லை';
}
5 changes: 4 additions & 1 deletion packages/core/lib/l10n/app_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1203,5 +1203,8 @@
"liveStreamSessionDetails": "تفاصيل الجلسة",
"liveStreamStatus": "الحالة",
"liveStreamStatusLive": "مباشر",
"liveStreamJoinFailed": "فشل الانضمام إلى الاجتماع. يُرجى المحاولة مرة أخرى."
"liveStreamJoinFailed": "فشل الانضمام إلى الاجتماع. يُرجى المحاولة مرة أخرى.",
"teamsPermissionRequired": "يلزم الوصول إلى الكاميرا والميكروفون. قم بتمكينها في الإعدادات للانضمام.",
"teamsJoinMeetingLoading": "جارِ الانضمام إلى الاجتماع...",
"teamsMissingJoinLink": "رابط الاجتماع مفقود"
}
5 changes: 4 additions & 1 deletion packages/core/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1563,5 +1563,8 @@
"liveStreamSessionDetails": "Session Details",
"liveStreamStatus": "Status",
"liveStreamStatusLive": "Live",
"liveStreamJoinFailed": "Failed to join the meeting. Please try again."
"liveStreamJoinFailed": "Failed to join the meeting. Please try again.",
"teamsPermissionRequired": "Camera and microphone access is required. Enable them in Settings to join.",
"teamsJoinMeetingLoading": "Joining meeting...",
"teamsMissingJoinLink": "Meeting link is missing"
}
5 changes: 4 additions & 1 deletion packages/core/lib/l10n/app_ml.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1203,5 +1203,8 @@
"liveStreamSessionDetails": "സെഷൻ വിവരങ്ങൾ",
"liveStreamStatus": "നില",
"liveStreamStatusLive": "ലൈവ്",
"liveStreamJoinFailed": "മീറ്റിംഗിൽ ചേരുന്നതിൽ പരാജയപ്പെട്ടു. വീണ്ടും ശ്രമിക്കുക."
"liveStreamJoinFailed": "മീറ്റിംഗിൽ ചേരുന്നതിൽ പരാജയപ്പെട്ടു. വീണ്ടും ശ്രമിക്കുക.",
"teamsPermissionRequired": "ക്യാമറ, മൈക്രോഫോൺ ആക്സസ് ആവശ്യമാണ്. ചേരുന്നതിന് ക്രമീകരണങ്ങളിൽ അവ പ്രവർത്തനക്ഷമമാക്കുക.",
"teamsJoinMeetingLoading": "മീറ്റിംഗിൽ ചേരുന്നു...",
"teamsMissingJoinLink": "മീറ്റിംഗ് ലിങ്ക് ലഭ്യമല്ല"
}
5 changes: 4 additions & 1 deletion packages/core/lib/l10n/app_ta.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1437,5 +1437,8 @@
"liveStreamSessionDetails": "அமர்வு விவரங்கள்",
"liveStreamStatus": "நிலை",
"liveStreamStatusLive": "நேரலை",
"liveStreamJoinFailed": "கூட்டத்தில் சேர முடியவில்லை. மீண்டும் முயற்சிக்கவும்."
"liveStreamJoinFailed": "கூட்டத்தில் சேர முடியவில்லை. மீண்டும் முயற்சிக்கவும்.",
"teamsPermissionRequired": "சேர, கேமரா மற்றும் மைக்ரோஃபோன் அணுகல் தேவை. அமைப்புகளில் அவற்றை இயக்கவும்.",
"teamsJoinMeetingLoading": "கூட்டத்தில் சேர்கிறோம்...",
"teamsMissingJoinLink": "சந்திப்பு இணைப்பு காணவில்லை"
}
4 changes: 4 additions & 0 deletions packages/courses/lib/models/course_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ class Lesson {
bool get isZoom =>
liveStreamProvider?.toLowerCase().contains('zoom') ?? false;

/// Whether this live stream or conference uses MS Teams.
bool get isTeams =>
liveStreamProvider?.toLowerCase().contains('teams') ?? false;

/// Checks if the lesson has enough metadata to be rendered without a specialized loader.
bool get isComplete {
if (isDetailFetched) return true;
Expand Down
4 changes: 3 additions & 1 deletion packages/courses/lib/screens/lesson_detail_orchestrator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,9 @@ class _LessonDetailOrchestratorState
case LessonType.videoConference:
final isEnded = lesson.streamStatus?.toLowerCase() == 'completed' ||
lesson.streamStatus?.toLowerCase() == 'ended';
if (isEnded && lesson.isZoom && lesson.showRecordedVideo) {
if (isEnded &&
(lesson.isZoom || lesson.isTeams) &&
lesson.showRecordedVideo) {
return VideoLessonViewer(
lesson: lesson,
onComplete: _markAsCompleted,
Expand Down
Loading