Skip to content

Commit

Permalink
Merge branch 'master' into LarsRefsgaard/health-as-a-service
Browse files Browse the repository at this point in the history
  • Loading branch information
LarsRefsgaard committed Dec 11, 2023
2 parents fe24aa9 + 3a06313 commit d17add6
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 223 deletions.
1 change: 1 addition & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions:
18 changes: 9 additions & 9 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ PODS:
- Flutter
- polar (0.0.1):
- Flutter
- PolarBleSdk (~> 5.4.0)
- PolarBleSdk (5.4.0):
- PolarBleSdk (~> 5.5.0)
- PolarBleSdk (5.5.0):
- RxSwift (~> 6.5.0)
- SwiftProtobuf (~> 1.0)
- ReachabilitySwift (5.0.0)
Expand All @@ -84,7 +84,7 @@ PODS:
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- SwiftProtobuf (1.25.1)
- SwiftProtobuf (1.25.2)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
Expand Down Expand Up @@ -213,7 +213,7 @@ SPEC CHECKSUMS:
audiofileplayer: 4aaff759a721ec3a850a682e0d9ec554e5f9e86f
battery_plus: 091633b7f01cb33dfc4aeedb450816f4d33818fa
camera_avfoundation: 3125e8cd1a4387f6f31c6c63abb8a55892a9eeeb
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
ESense: cbe103ad16c435424f2fd6f8b274f2468af84173
esense_flutter: 8bfadfdefe7b51d6f78366e43b2b64ec9b6144b6
Expand All @@ -236,17 +236,17 @@ SPEC CHECKSUMS:
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
pedometer: 381969883680ade42559782cc41a3bbd453d8234
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
polar: 3df98f4edaae7ff57d39fa6af2bd01b5b857e79d
PolarBleSdk: 980933f58cb2856c3627f40a54c91b9351a07736
polar: 8efdf64c1b4e0034e8fa3b1d12b64e5b27a877f1
PolarBleSdk: 2551160f3dcba0207723fc466a275e2d3aeda01f
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
RxSwift: 5710a9e6b17f3c3d6e40d6e559b9fa1e813b2ef8
screen_state: a7ae251997e97f3f001839df09b57313b0ddef18
sensors_plus: 4ee32bc7d61a055f27f88d3215ad6b6fb96a2b8e
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
SwiftProtobuf: 69f02cd54fb03201c5e6bf8b76f687c5ef7541a3
url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86
video_player_avfoundation: 8563f13d8fc8b2c29dc2d09e60b660e4e8128837
SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
video_player_avfoundation: e9e6f9cae7d7a6d9b43519b0aab382bca60fcfd1

PODFILE CHECKSUM: cf73571b196c8b5799c2f2111e13846fa4ef168b

Expand Down
10 changes: 4 additions & 6 deletions lib/blocs/app_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ class StudyAppBLoC {

get stateStream => _stateStream;

List<ActiveParticipationInvitation> _invitations = [];
set invitations(value) => _invitations = value;
get invitations => _invitations;
List<ActiveParticipationInvitation> invitations = [];

List<Message> _messages = [];
final StreamController<int> _messageStreamController =
Expand Down Expand Up @@ -219,8 +217,8 @@ class StudyAppBLoC {
/// * shown to the user
/// * accepted by the user
/// * successfully uploaded to CARP
set setHasInformedConsentBeenAccepted(bool accepted) =>
LocalSettings().setHasInformedConsentBeenAccepted = accepted;
set hasInformedConsentBeenAccepted(bool accepted) =>
LocalSettings().hasInformedConsentBeenAccepted = accepted;

/// Refresh the list of messages (news, announcements, articles) to be shown in
/// the Study Page of the app.
Expand Down Expand Up @@ -336,7 +334,7 @@ class StudyAppBLoC {
/// available.
Future<void> leaveStudy() async {
_state = StudyAppState.initialized;
setHasInformedConsentBeenAccepted = false;
hasInformedConsentBeenAccepted = false;
await LocalSettings().eraseStudyIds();
await Sensing().removeStudy();
}
Expand Down
7 changes: 5 additions & 2 deletions lib/carp_study_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ part of carp_study_app;

final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
const String firstRoute = '/about';

class CarpStudyApp extends StatefulWidget {
const CarpStudyApp({super.key});
Expand Down Expand Up @@ -38,7 +39,9 @@ class CarpStudyAppState extends State<CarpStudyApp> {
parentNavigatorKey: _shellNavigatorKey,
redirect: (context, state) => !CarpService().authenticated
? '/login'
: (bloc.hasInformedConsentBeenAccepted ? '/tasks' : '/consent'),
: (bloc.hasInformedConsentBeenAccepted
? firstRoute
: '/consent'),
),
GoRoute(
path: '/tasks',
Expand Down Expand Up @@ -105,7 +108,7 @@ class CarpStudyAppState extends State<CarpStudyApp> {
path: '/consent',
parentNavigatorKey: _rootNavigatorKey,
redirect: (context, state) => bloc.hasInformedConsentBeenAccepted
? '/tasks'
? firstRoute
: (bloc.studyId == null ? '/invitations' : null),
builder: (context, state) => InformedConsentPage(
bloc.data.informedConsentViewModel,
Expand Down
46 changes: 31 additions & 15 deletions lib/data/carp_backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ class CarpBackend {
/// Has the user been authenticated?
bool get isAuthenticated => CarpService().authenticated;

/// The authenticated user
CarpUser? get user => CarpService().currentUser;

/// The URI of the CANS server - depending on deployment mode.
Uri get uri => Uri(
scheme: 'https',
Expand All @@ -44,11 +41,11 @@ class CarpBackend {
],
);

OAuthToken? get oauthToken => LocalSettings().oauthToken;
set oauthToken(OAuthToken? token) => LocalSettings().oauthToken = token;
CarpUser? get user => LocalSettings().user;
set user(CarpUser? user) => LocalSettings().user = user;

String? get username => LocalSettings().username;
set username(String? username) => LocalSettings().username = username;
String? get username => user?.username;
OAuthToken? get oauthToken => user?.token;

String? get studyId => bloc.studyId;
set studyId(String? id) {
Expand All @@ -73,7 +70,7 @@ class CarpBackend {
name: "CAWS @ DTU",
uri: uri.replace(pathSegments: [uris[bloc.deploymentMode]!]),
authURL: uri,
clientId: 'carp-webservices-dart',
clientId: 'studies-app',
redirectURI: Uri.parse('carp-studies-auth://auth'),
discoveryURL: uri.replace(pathSegments: [
...uri.pathSegments,
Expand All @@ -85,20 +82,39 @@ class CarpBackend {
);

CarpService().configure(app!);
if (user != null) {
CarpService().currentUser = user;
if (oauthToken!.hasExpired) {
try {
await refresh();
} catch (error) {
CarpService().currentUser = null;
warning('Failed to refresh access token - $error');
}
}
}

CarpParticipationService().configureFrom(CarpService());

info('$runtimeType initialized - app: $app');
}

Future<CarpUser> authenticate() async {
var response = await CarpService().authenticate();
bloc.stateStream.sink.add(StudiesAppState.authenticating);
user = await CarpService().authenticate();

username = response.username;
oauthToken = response.token;
bloc.stateStream.sink.add(StudiesAppState.accessTokenRetrieved);

CarpParticipationService().configureFrom(CarpService());
return user as CarpUser;
}

Future<CarpUser> refresh() async {
bloc.stateStream.sink.add(StudiesAppState.authenticating);
user = await CarpService().refresh();

bloc.stateStream.sink.add(StudiesAppState.accessTokenRetrieved);

return response;
return user as CarpUser;
}

Future<ConsentDocument?> uploadInformedConsent(
Expand All @@ -113,9 +129,9 @@ class CarpBackend {
document = await CarpService().createConsentDocument(informedConsent);
info(
'Informed consent document uploaded successfully - id: ${document.id}');
bloc.setHasInformedConsentBeenAccepted = true;
bloc.hasInformedConsentBeenAccepted = true;
} on Exception {
bloc.setHasInformedConsentBeenAccepted = false;
bloc.hasInformedConsentBeenAccepted = false;
warning('Informed consent upload failed for username: $username');
}

Expand Down
67 changes: 19 additions & 48 deletions lib/data/local_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,33 @@ class LocalSettings {
static const String studyIdKey = 'study_id';
static const String studyDeploymentIdKey = 'study_deployment_id';
static const String deviceRolenameKey = 'role_name';
static const String oauthTokenKey = 'token';
static const String accessTokenKey = 'access_token';
static const String usernameKey = 'username';
static const informedConsentAcceptedKey = 'informed_consent_accepted';
static const String userKey = 'user';
static const String informedConsentAcceptedKey = 'informed_consent_accepted';

static final LocalSettings _instance = LocalSettings._();
factory LocalSettings() => _instance;
LocalSettings._() : super();

OAuthToken? _oauthToken;
String? _accessToken;
String? _studyId;
String? _studyDeploymentId;
String? _deviceRolename;
bool? _hasInformedConsentBeenAccepted;
String? _username;
CarpUser? _user;

String? get accessToken {
if (_accessToken == null) {
String? accessTokenString =
Settings().preferences!.getString(accessTokenKey);
CarpUser? get user {
if (_user == null) {
String? userString = Settings().preferences!.getString(userKey);

_accessToken = (accessTokenString != null) ? accessTokenString : null;
}
return _accessToken;
}

set accessToken(String? value) {
_accessToken = value;
Settings().preferences!.setString(accessTokenKey, value!);
}

OAuthToken? get oauthToken {
if (_oauthToken == null) {
String? ouathTokenString =
Settings().preferences!.getString(oauthTokenKey);

_oauthToken = (ouathTokenString != null)
? OAuthToken.fromJson(jsonDecode(ouathTokenString))
_user = (userString != null)
? CarpUser.fromJson(jsonDecode(userString))
: null;
}
return _oauthToken;
return _user;
}

set oauthToken(OAuthToken? token) {
_oauthToken = token;
Settings()
.preferences!
.setString(oauthTokenKey, jsonEncode(token?.toJson()));
}

String? get username =>
(_username ??= Settings().preferences!.getString(usernameKey));

set username(String? username) {
_username = username;
Settings().preferences!.setString(usernameKey, username!);
set user(CarpUser? user) {
_user = user;
Settings().preferences!.setString(userKey, jsonEncode(user!.toJson()));
}

String? get studyId =>
Expand Down Expand Up @@ -124,12 +94,15 @@ class LocalSettings {
: await Settings().getCacheBasePath(studyDeploymentId!);

/// Has the informed consent been shown to, and accepted by the user?
/// Is `true` if there is no informed consent.
bool get hasInformedConsentBeenAccepted => _hasInformedConsentBeenAccepted ??=
Settings().preferences!.getBool(informedConsentAcceptedKey) ?? false;

/// Specify if the informed consent has been handled.
set setHasInformedConsentBeenAccepted(bool accepted) =>
Settings().preferences!.setBool(informedConsentAcceptedKey, accepted);
set hasInformedConsentBeenAccepted(bool accepted) {
_hasInformedConsentBeenAccepted = accepted;
Settings().preferences!.setBool(informedConsentAcceptedKey, accepted);
}

Future<void> eraseStudyIds() async {
_studyId = null;
Expand All @@ -141,9 +114,7 @@ class LocalSettings {
}

Future<void> eraseAuthCredentials() async {
_username = null;
_oauthToken = null;
await Settings().preferences!.remove(usernameKey);
await Settings().preferences!.remove(oauthTokenKey);
_user = null;
await Settings().preferences!.remove(userKey);
}
}
2 changes: 1 addition & 1 deletion lib/ui/pages/audio_task_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ class AudioTaskPageState extends State<AudioTaskPage> {
TextButton(
child: Text(locale.translate("YES")),
onPressed: () {
context.pushReplacement('/tasks');
context.pushReplacement('/');
},
)
],
Expand Down
5 changes: 3 additions & 2 deletions lib/ui/pages/informed_consent_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class InformedConsentState extends State<InformedConsentPage> {
void resultCallback(RPTaskResult result) async {
await widget.model.informedConsentHasBeenAccepted(result);
if (context.mounted) {
context.go('/tasks');
context.go('/');
}
}

Expand All @@ -29,7 +29,8 @@ class InformedConsentState extends State<InformedConsentPage> {
future: widget.model.getInformedConsent(localization.locale).then(
(value) {
if (value == null) {
context.go('/tasks');
bloc.hasInformedConsentBeenAccepted = true;
context.go('/');
}
return value;
},
Expand Down
5 changes: 2 additions & 3 deletions lib/ui/pages/study_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ class StudyDetailsPage extends StatelessWidget {
children: [
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
IconButton(
onPressed: () => context.canPop()
? context.pop()
: context.replace('/tasks'),
onPressed: () =>
context.canPop() ? context.pop() : context.replace('/'),
icon: const Icon(Icons.close))
]),
Flexible(
Expand Down
2 changes: 0 additions & 2 deletions lib/ui/pages/task_list_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ class TaskListPageState extends State<TaskListPage> {
@override
Widget build(BuildContext context) {
RPLocalizations locale = RPLocalizations.of(context)!;
bloc.configurePermissions(context);

return Scaffold(
backgroundColor: Theme.of(context).colorScheme.secondary,
body: SafeArea(
Expand Down
2 changes: 1 addition & 1 deletion lib/view_models/informed_consent_page_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class InformedConsentViewModel extends ViewModel {
RPTaskResult informedConsentResult,
) async {
info('Informed consent has been accepted by user.');
bloc.setHasInformedConsentBeenAccepted = true;
bloc.hasInformedConsentBeenAccepted = true;
await bloc.backend.uploadInformedConsent(informedConsentResult);
}
}
14 changes: 14 additions & 0 deletions lib/view_models/invitations_list_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@ part of carp_study_app;

class InvitationsListViewModel extends ViewModel {
Future<List<ActiveParticipationInvitation>> get invitations async {
CarpParticipationService().configureFrom(CarpService());

bloc.invitations =
await CarpParticipationService().getActiveParticipationInvitations();

/// Filter the invitations to only include those that
/// have a smartphone as a device in [ActiveParticipationInvitation.assignedDevices] list
/// (i.e. the invitation is for a smartphone).
/// This is done to avoid showing invitations for other devices (e.g. [WebBrowser]).
bloc.invitations = bloc.invitations
.where((invitation) =>
invitation.assignedDevices
?.any((device) => device.device is Smartphone) ??
false)
.toList();

return bloc.invitations;
}

Expand Down
Loading

0 comments on commit d17add6

Please sign in to comment.