Skip to content
Open
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
65 changes: 55 additions & 10 deletions app/lib/screens/app_onboarding/iframe_helper.dart
Original file line number Diff line number Diff line change
@@ -1,25 +1,70 @@
import 'dart:async';
import 'dart:convert';

import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_core/core.dart' show Study;
import 'package:studyu_core/env.dart' as env;
import "package:universal_html/html.dart" as html;

typedef PreviewNavigationHandler = Future<void> Function(String? route);

class IFrameHelper {
static StreamSubscription<html.MessageEvent>? _messageSubscription;

String _designerOrigin() {
final referrer = html.document.referrer;
if (referrer.isNotEmpty) {
final uri = Uri.tryParse(referrer);
if (uri != null && uri.hasScheme && uri.host.isNotEmpty) {
return uri.origin;
}
}
return env.designerUrl!;
}

void postPreviewStatus({required String status, String? message}) {
html.window.parent!.postMessage(
jsonEncode({
'type': 'previewStatus',
'status': status,
if (message != null) 'message': message,
}),
_designerOrigin(),
);
}

void postRouteFinished() {
// Go back to the selected origin route
html.window.parent!.postMessage('routeFinished', env.designerUrl!);
html.window.parent!.postMessage('routeFinished', _designerOrigin());
}

void listen(AppState state) {
html.window.onMessage.listen((event) {
final message = event.data as String;
final messageContent = jsonDecode(message) as Map<String, dynamic>;
// if (messageContent['intervention'] != null) {
// print(messageContent['intervention']);
// print("AppListen: " + messageContent.toString());
void listen(AppState state, {PreviewNavigationHandler? onNavigate}) {
_messageSubscription?.cancel();
_messageSubscription = html.window.onMessage.listen((event) async {
final data = event.data;
if (data is! String) return;

final Object? decodedMessage;
try {
decodedMessage = jsonDecode(data);
} catch (_) {
return;
}
if (decodedMessage is! Map<String, dynamic>) return;

final messageContent = decodedMessage;
if (messageContent['type'] == 'previewNavigate') {
await onNavigate?.call(messageContent['route'] as String?);
return;
}

if (messageContent.containsKey('type') ||
messageContent['id'] is! String ||
messageContent['user_id'] is! String) {
return;
}

state.updateStudy(Study.fromJson(messageContent));
// }
});
}
}
165 changes: 150 additions & 15 deletions app/lib/screens/app_onboarding/loading_screen.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/main.dart' show navigatorKey;
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/screens/app_onboarding/iframe_helper.dart';
Expand All @@ -24,6 +25,10 @@ class LoadingScreen extends StatefulWidget {
}

class _LoadingScreenState extends State<LoadingScreen> {
final IFrameHelper _iFrameHelper = IFrameHelper();
bool _previewNavigationInProgress = false;
String? _pendingPreviewRoute;

@override
void initState() {
super.initState();
Expand All @@ -32,13 +37,27 @@ class _LoadingScreenState extends State<LoadingScreen> {

Future<void> initStudy() async {
final state = context.read<AppState>();
await _initPreview(state);
try {
await _initPreview(state);
} catch (error, stackTrace) {
StudyULogger.error(
'Preview failed to initialize.',
error: error,
stackTrace: stackTrace,
);
_iFrameHelper.postPreviewStatus(
status: 'error',
message:
'The preview could not be opened for this study yet. Please try resetting the preview.',
);
rethrow;
}

final selectedSubjectId = await getActiveSubjectId();
if (!mounted) return;

if (selectedSubjectId == null) {
await noSubjectFound();
await noSubjectFound(state);
return;
}
StudyULogger.info("Retrieving subject with ID: $selectedSubjectId");
Expand All @@ -61,14 +80,18 @@ class _LoadingScreenState extends State<LoadingScreen> {
}
}

Future<void> noSubjectFound() async {
Future<void> noSubjectFound(AppState state) async {
await cancelNotifications(context);

final bool onBoarded = await SecureStorage.readBool('onboarded') ?? false;
// If no subject found and user has not done any onboarding, redirect to onboarding
final route = onBoarded ? Routes.terms : Routes.onboarding;
// Designer previews should skip the generic app introduction and go straight
// to the participant study flow.
final route = state.isPreview || onBoarded
? Routes.terms
: Routes.onboarding;

if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
Navigator.pushReplacementNamed(context, route);
}

Expand Down Expand Up @@ -125,41 +148,52 @@ class _LoadingScreenState extends State<LoadingScreen> {
);
final lang = AppLanguage(AppLocalizations.supportedLocales);
final preview = study_preview.Preview(widget.queryParameters, lang);
final iFrameHelper = IFrameHelper();
state.isPreview = true;
_iFrameHelper.postPreviewStatus(status: 'loading');
await preview.init();

// Authorize
if (!await preview.handleAuthorization()) {
final isAuthorized = await preview.handleAuthorization();
if (!isAuthorized) {
_iFrameHelper.postPreviewStatus(
status: 'error',
message:
'The preview could not be opened right now. Please try resetting the preview.',
);
return;
}
state.selectedStudy = preview.study;

await preview.runCommands();

iFrameHelper.listen(state);
_iFrameHelper.listen(
state,
onNavigate: (route) => _navigatePreviewRoute(state, route),
);

if (preview.hasRoute()) {
// print('[PreviewApp]: Found preview route:: ${preview.selectedRoute}');

// ELIGIBILITY CHECK
if (preview.selectedRoute == '/eligibilityCheck') {
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
// if we remove the await, we can push multiple times. warning: do not run in while(true)
await Navigator.push<EligibilityResult>(
context,
EligibilityScreen.routeFor(study: preview.study),
);
// either do the same navigator push again or --> send a message back to designer and let it reload the whole page <--
iFrameHelper.postRouteFinished();
_iFrameHelper.postRouteFinished();
return;
}

// INTERVENTION SELECTION
if (preview.selectedRoute == Routes.interventionSelection) {
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
await Navigator.pushNamed(context, Routes.interventionSelection);
iFrameHelper.postRouteFinished();
_iFrameHelper.postRouteFinished();
return;
}

Expand All @@ -171,24 +205,27 @@ class _LoadingScreenState extends State<LoadingScreen> {
// CONSENT
if (preview.selectedRoute == Routes.consent) {
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
await Navigator.pushNamed<bool>(context, Routes.consent);
iFrameHelper.postRouteFinished();
_iFrameHelper.postRouteFinished();
return;
}

// JOURNEY
if (preview.selectedRoute == Routes.journey) {
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
await Navigator.pushNamed(context, Routes.journey);
iFrameHelper.postRouteFinished();
_iFrameHelper.postRouteFinished();
return;
}

// DASHBOARD
if (preview.selectedRoute == Routes.dashboard) {
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
await Navigator.pushReplacementNamed(context, Routes.dashboard);
iFrameHelper.postRouteFinished();
_iFrameHelper.postRouteFinished();
return;
}

Expand All @@ -200,8 +237,9 @@ class _LoadingScreenState extends State<LoadingScreen> {
state.selectedStudy!.schedule.includeBaseline = false;
state.activeSubject!.study.schedule.includeBaseline = false;
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
await Navigator.pushReplacementNamed(context, Routes.dashboard);
iFrameHelper.postRouteFinished();
_iFrameHelper.postRouteFinished();
return;
}

Expand All @@ -213,6 +251,7 @@ class _LoadingScreenState extends State<LoadingScreen> {
),
];
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
await Navigator.push<bool>(
context,
TaskScreen.routeFor(
Expand All @@ -222,7 +261,7 @@ class _LoadingScreenState extends State<LoadingScreen> {
),
),
);
iFrameHelper.postRouteFinished();
_iFrameHelper.postRouteFinished();
return;
}
} else {
Expand All @@ -231,21 +270,117 @@ class _LoadingScreenState extends State<LoadingScreen> {
if (subject != null) {
state.activeSubject = subject;
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
Navigator.pushReplacementNamed(context, Routes.dashboard);
return;
} else {
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
Navigator.pushReplacementNamed(context, Routes.studyOverview);
return;
}
} else {
if (!mounted) return;
_iFrameHelper.postPreviewStatus(status: 'loaded');
Navigator.pushReplacementNamed(context, Routes.welcome);
return;
}
}
}

Future<void> _navigatePreviewRoute(AppState state, String? route) async {
if (_previewNavigationInProgress) {
_pendingPreviewRoute = route;
return;
}
_previewNavigationInProgress = true;

try {
final navigator = navigatorKey.currentState;
if (navigator == null) return;

Future<bool> ensureSubject() async {
if (state.activeSubject != null) return true;
if (state.selectedStudy == null) return false;

final preview = study_preview.Preview(
{
...?widget.queryParameters,
if (route != null) 'route': route,
},
AppLanguage(AppLocalizations.supportedLocales),
);
await preview.init();
preview.study = state.selectedStudy;
state.activeSubject = await preview.getStudySubject(
state,
createSubject: true,
);
return state.activeSubject != null;
}

Future<void> waitForNavigator() async {
await WidgetsBinding.instance.endOfFrame;
await Future<void>.delayed(const Duration(milliseconds: 120));
}

Future<void> replaceNamed(String routeName) async {
await waitForNavigator();
navigatorKey.currentState?.pushReplacementNamed(routeName);
}

Future<void> replaceWithEligibility() async {
if (state.selectedStudy == null) return;
await waitForNavigator();
navigatorKey.currentState?.pushReplacement(
EligibilityScreen.routeFor(study: state.selectedStudy),
);
}

if (route == null || route.isEmpty) {
await replaceNamed(Routes.studyOverview);
return;
}

if (route == 'eligibilityCheck') {
if (state.selectedStudy == null) return;
await replaceWithEligibility();
return;
}

if (route == Routes.interventionSelection ||
route == 'interventionSelection') {
await replaceNamed(Routes.interventionSelection);
return;
}

if (!await ensureSubject()) {
_iFrameHelper.postPreviewStatus(
status: 'error',
message: 'The preview route could not be opened right now.',
);
return;
}

if (route == 'consent') {
await replaceNamed(Routes.consent);
} else if (route == 'journey') {
await replaceNamed(Routes.journey);
} else if (route == 'dashboard') {
await replaceNamed(Routes.dashboard);
}
} finally {
_previewNavigationInProgress = false;
final pendingRoute = _pendingPreviewRoute;
_pendingPreviewRoute = null;
if (pendingRoute != null && pendingRoute != route) {
await _navigatePreviewRoute(state, pendingRoute);
} else {
_iFrameHelper.postPreviewStatus(status: 'loaded');
}
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
Expand Down
Loading
Loading