diff --git a/android-vault b/android-vault index 15225ede6c..2f81632319 160000 --- a/android-vault +++ b/android-vault @@ -1 +1 @@ -Subproject commit 15225ede6c44da8265e5cdaea34d49dbc47cb8f5 +Subproject commit 2f816323198928844c274ede6692e0fd8430d4d1 diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart index 86768767b3..b7f03e6be3 100644 --- a/apps/flutter_parent/lib/l10n/app_localizations.dart +++ b/apps/flutter_parent/lib/l10n/app_localizations.dart @@ -1715,4 +1715,14 @@ class AppLocalizations { String get needToEnablePermission => Intl.message('You need to enable exact alarm permission for this action', desc: 'Error message when the user tries to set a reminder without the permission'); + + String get submissionAndRubric => Intl.message( + 'Submission & Rubric', + desc: 'Button text for Submission and Rubric on Assignment Details Screen' + ); + + String get submission => Intl.message( + 'Submission', + desc: 'Title for WebView screen when opening submission' + ); } diff --git a/apps/flutter_parent/lib/network/utils/analytics.dart b/apps/flutter_parent/lib/network/utils/analytics.dart index 9254d431a0..c9eeb47f20 100644 --- a/apps/flutter_parent/lib/network/utils/analytics.dart +++ b/apps/flutter_parent/lib/network/utils/analytics.dart @@ -14,8 +14,8 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_parent/network/api/heap_api.dart'; -import 'package:flutter_parent/utils/features_utils.dart'; import 'package:flutter_parent/utils/debug_flags.dart'; +import 'package:flutter_parent/utils/features_utils.dart'; /// Event names /// The naming scheme for the majority of these is found in a google doc so that we can be consistent @@ -54,6 +54,7 @@ class AnalyticsEventConstants { static const USER_PROPERTY_BUILD_TYPE = 'build_type'; static const USER_PROPERTY_OS_VERSION = 'os_version'; static const VIEWED_OLD_REMINDER_MESSAGE = 'viewed_old_reminder_message'; + static const SUBMISSION_AND_RUBRIC_INTERACTION = 'submission_and_rubric_interaction'; } /// (Copied from canvas-api-2, make sure to stay in sync) diff --git a/apps/flutter_parent/lib/router/panda_router.dart b/apps/flutter_parent/lib/router/panda_router.dart index cf6f2d3d92..b4ea35148d 100644 --- a/apps/flutter_parent/lib/router/panda_router.dart +++ b/apps/flutter_parent/lib/router/panda_router.dart @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +import 'dart:convert'; import 'dart:core'; import 'package:fluro/fluro.dart'; @@ -34,13 +35,13 @@ import 'package:flutter_parent/screens/dashboard/dashboard_screen.dart'; import 'package:flutter_parent/screens/domain_search/domain_search_screen.dart'; import 'package:flutter_parent/screens/events/event_details_screen.dart'; import 'package:flutter_parent/screens/help/help_screen.dart'; -import 'package:flutter_parent/screens/settings/legal_screen.dart'; import 'package:flutter_parent/screens/help/terms_of_use_screen.dart'; import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_screen.dart'; import 'package:flutter_parent/screens/login_landing_screen.dart'; import 'package:flutter_parent/screens/not_a_parent_screen.dart'; import 'package:flutter_parent/screens/pairing/qr_pairing_screen.dart'; import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen.dart'; +import 'package:flutter_parent/screens/settings/legal_screen.dart'; import 'package:flutter_parent/screens/settings/settings_screen.dart'; import 'package:flutter_parent/screens/splash/splash_screen.dart'; import 'package:flutter_parent/screens/web_login/web_login_screen.dart'; @@ -152,8 +153,11 @@ class PandaRouter { static final String _simpleWebView = '/internal'; - static String simpleWebViewRoute(String url, String infoText) => - '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.infoText}=${Uri.encodeQueryComponent(infoText)}'; + static String simpleWebViewRoute(String url, String infoText, bool limitWebAccess) => + '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.infoText}=${Uri.encodeQueryComponent(infoText)}&${_RouterKeys.limitWebAccess}=${limitWebAccess}'; + + static String submissionWebViewRoute(String url, String title, Map cookies, bool limitWebAccess) => + '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.title}=${Uri.encodeQueryComponent(title)}&${_RouterKeys.cookies}=${jsonEncode(cookies)}&${_RouterKeys.limitWebAccess}=${limitWebAccess}'; static String settings() => '/settings'; @@ -376,7 +380,13 @@ class PandaRouter { static Handler _simpleWebViewHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { final url = params[_RouterKeys.url]![0]; final infoText = params[_RouterKeys.infoText]?.elementAt(0); - return SimpleWebViewScreen(url, url, infoText: infoText == null || infoText == 'null' ? null : infoText); + final titleParam = params[_RouterKeys.title]?.firstOrNull; + final title = (titleParam == null || titleParam.isEmpty) ? url : titleParam; + final cookiesParam = params[_RouterKeys.cookies]?.firstOrNull; + final cookies = (cookiesParam == null || cookiesParam.isEmpty) ? {} : jsonDecode(cookiesParam); + final limitWebAccess = params[_RouterKeys.limitWebAccess]?.firstOrNull == 'true'; + return SimpleWebViewScreen(url, title, limitWebAccess, + infoText: infoText == null || infoText == 'null' ? null : infoText, initialCookies: cookies); }); static Handler _syllabusHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { @@ -461,7 +471,7 @@ class PandaRouter { final url = await _interactor.getAuthUrl(link); if (limitWebAccess) { // Special case for limit webview access flag (We don't want them to be able to navigate within the webview) - locator().pushRoute(context, simpleWebViewRoute(url, L10n(context).webAccessLimitedMessage)); + locator().pushRoute(context, simpleWebViewRoute(url, L10n(context).webAccessLimitedMessage, true)); } else if (await locator().canLaunch(link) ?? false) { // No native route found, let's launch the url if possible, or show an error toast locator().launch(url); @@ -519,6 +529,9 @@ class _RouterKeys { static final accountName = 'accountName'; static final eventId = 'eventId'; static final infoText = 'infoText'; + static final title = 'title'; + static final cookies = 'cookies'; + static final limitWebAccess = 'limitWebAccess'; static final isCreatingAccount = 'isCreatingAccount'; static final loginFlow = 'loginFlow'; static final qrLoginUrl = 'qrLoginUrl'; diff --git a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart index 2814bad688..cf68adab8f 100644 --- a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart +++ b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart @@ -17,6 +17,7 @@ import 'package:flutter_parent/models/assignment.dart'; import 'package:flutter_parent/models/reminder.dart'; import 'package:flutter_parent/models/user.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; +import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/screens/assignments/assignment_details_interactor.dart'; import 'package:flutter_parent/screens/assignments/grade_cell.dart'; import 'package:flutter_parent/screens/inbox/create_conversation/create_conversation_screen.dart'; @@ -33,6 +34,7 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_svg/svg.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../network/utils/analytics.dart'; import '../../utils/veneers/flutter_snackbar_veneer.dart'; class AssignmentDetailsScreen extends StatefulWidget { @@ -183,6 +185,30 @@ class _AssignmentDetailsScreenState extends State { ], ), ), + Divider(), + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 16.0), + child: OutlinedButton( + onPressed: () { + _onSubmissionAndRubricClicked(assignment.htmlUrl, l10n.submission); + }, + child: Align( + alignment: Alignment.center, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text( + l10n.submissionAndRubric, + style: textTheme.titleMedium?.copyWith(color: ParentTheme.of(context)?.studentColor), + ), + SizedBox(width: 6), + Icon(CanvasIconsSolid.arrow_open_right, color: ParentTheme.of(context)?.studentColor, size: 14) + ])), + style: OutlinedButton.styleFrom( + minimumSize: Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6.0), + ), + side: BorderSide(width: 0.5, color: ParentTheme.of(context)?.onSurfaceColor ?? Colors.grey), + ))), if (!fullyLocked) ...[ Divider(), ..._rowTile( @@ -372,4 +398,17 @@ class _AssignmentDetailsScreenState extends State { locator.get().push(context, screen); } } + + _onSubmissionAndRubricClicked(String? assignmentUrl, String title) { + if (assignmentUrl == null) return; + final parentId = ApiPrefs.getUser()?.id ?? 0; + final currentStudentId = _currentStudent?.id ?? 0; + locator().pushRoute(context, PandaRouter.submissionWebViewRoute( + assignmentUrl, + title, + {"k5_observed_user_for_$parentId": "$currentStudentId"}, + false + )); + locator().logEvent(AnalyticsEventConstants.SUBMISSION_AND_RUBRIC_INTERACTION); + } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart index cc186be9dc..dcb7c6ba10 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart @@ -19,12 +19,24 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/web_view_utils.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import '../../service_locator.dart'; +import '../../url_launcher.dart'; + class SimpleWebViewScreen extends StatefulWidget { - final String _url; - final String _title; - final String? _infoText; + final String url; + final String title; + final String? infoText; + final Map? initialCookies; + final bool limitWebAccess; - SimpleWebViewScreen(this._url, this._title, {String? infoText}) : _infoText = infoText; + SimpleWebViewScreen( + this.url, + this.title, + this.limitWebAccess, { + String? infoText, + Map? initialCookies, + }) : this.infoText = infoText, + this.initialCookies = initialCookies; @override State createState() => _SimpleWebViewScreenState(); @@ -43,7 +55,7 @@ class _SimpleWebViewScreenState extends State { backgroundColor: Colors.transparent, iconTheme: Theme.of(context).iconTheme, bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), - title: Text(widget._title, style: Theme.of(context).textTheme.titleLarge), + title: Text(widget.title, style: Theme.of(context).textTheme.titleLarge), ), body: WebView( javascriptMode: JavascriptMode.unrestricted, @@ -52,34 +64,51 @@ class _SimpleWebViewScreenState extends State { navigationDelegate: _handleNavigation, onWebViewCreated: (controller) { _controller = controller; - controller.loadUrl(widget._url); + controller.loadUrl(widget.url); }, onPageFinished: _handlePageLoaded, + initialCookies: _getCookies(), ), ), ); } NavigationDecision _handleNavigation(NavigationRequest request) { - if (!request.isForMainFrame || widget._url.startsWith(request.url)) return NavigationDecision.navigate; + if (request.url.contains('/download?download_frd=')) { + locator().launch(request.url); + return NavigationDecision.prevent; + } + if (!request.isForMainFrame || widget.url.startsWith(request.url) || !widget.limitWebAccess) return NavigationDecision.navigate; return NavigationDecision.prevent; } void _handlePageLoaded(String url) async { // If there's no info to show, just return - if (widget._infoText == null || widget._infoText!.isEmpty) return; + if (widget.infoText == null || widget.infoText!.isEmpty) return; // Run javascript to show the info alert await _controller?.evaluateJavascript(_showAlertJavascript); } + String _getDomain() { + final uri = Uri.parse(widget.url); + return uri.host; + } + + List _getCookies() { + return widget.initialCookies?.entries + .map((entry) => WebViewCookie(name: entry.key.toString(), value: entry.value.toString(), domain: _getDomain())) + .toList() ?? + []; + } + String get _showAlertJavascript => """ const floatNode = `