diff --git a/backends/carp_webservices/CHANGELOG.md b/backends/carp_webservices/CHANGELOG.md index 575a66f10..aab1769fd 100644 --- a/backends/carp_webservices/CHANGELOG.md +++ b/backends/carp_webservices/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.8.0 + +* anonymous authentication +* upgrading packages + ## 3.7.0 * fix of issues [#467](https://github.com/cph-cachet/carp.sensing-flutter/issues/467) diff --git a/backends/carp_webservices/README.md b/backends/carp_webservices/README.md index 1ec8444b7..dc1de69c3 100644 --- a/backends/carp_webservices/README.md +++ b/backends/carp_webservices/README.md @@ -22,7 +22,7 @@ This package uses the [oidc](https://pub.dev/packages/oidc) plugin for authentic ### Android On Android you need to edit both the `build.gradle` file and the `AndroidManifest.xml` file plus disable some backup settings. -You also need to add an activity to the `AndroidManifest.xml` to allow for redirection to/from the web view for authentication (if you are using the `authenticate()` method in the package). You manifest file would look something like this: +You also need to add an activity to the `AndroidManifest.xml` to allow for redirection to/from the web view for authentication (if you are using the `authenticate()` or `authenticateWithMagicLink` methods in the package). You manifest file would look something like this: ```xml ... @@ -31,7 +31,7 @@ You also need to add an activity to the `AndroidManifest.xml` to allow for redir android:name="${applicationName}" android:label="CAWS Example" android:fullBackupContent="@xml/backup_rules" - android:dataExtractionRules="@xml/data_extraction_rules" + android:dataExtractionRules="@xml/data_extraction_rules" android:icon="@mipmap/ic_launcher"> @@ -55,6 +55,18 @@ You also need to add an activity to the `AndroidManifest.xml` to allow for redir + + + + + + + + + + ``` ### iOS @@ -70,31 +82,32 @@ Add the following `CFBundleURLTypes` entry in your `Info.plist` file: CFBundleURLSchemes com.my.app + my-redirect-uri ``` -Replace `com.my.app` with your application id. +Replace `com.my.app` with your application id and `my-redirect-url` with your redirect uri. ## Services CARP Web Services (CAWS) consists of a set of sub-services, which are accessible for the client: -* [`CarpAuthService`](https://pub.dev/documentation/carp_webservices/latest/carp_auth/CarpAuthService-class.html) - authentication service for CAWS -* [`CarpParticipationService`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CarpParticipationService-class.html) - CAWS-specific implementation of the [ParticipationService](https://github.com/cph-cachet/carp.core-kotlin/blob/develop/docs/carp-deployments.md#participationservice) -* [`CarpDeploymentService`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CarpDeploymentService-class.html) - CAWS-specific implementation of the [DeploymentService](https://github.com/cph-cachet/carp.core-kotlin/blob/develop/docs/carp-deployments.md#deploymentservice) -* [`CarpDataStreamService`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CarpDataStreamService-class.html) - CAWS-specific implementation of the [DataStreamService]() -* [`CarpService`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CarpService-class.html) - resource management (folders, documents, and files) and alternative data management service +- [`CarpAuthService`](https://pub.dev/documentation/carp_webservices/latest/carp_auth/CarpAuthService-class.html) - authentication service for CAWS +- [`CarpParticipationService`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CarpParticipationService-class.html) - CAWS-specific implementation of the [ParticipationService](https://github.com/cph-cachet/carp.core-kotlin/blob/develop/docs/carp-deployments.md#participationservice) +- [`CarpDeploymentService`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CarpDeploymentService-class.html) - CAWS-specific implementation of the [DeploymentService](https://github.com/cph-cachet/carp.core-kotlin/blob/develop/docs/carp-deployments.md#deploymentservice) +- [`CarpDataStreamService`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CarpDataStreamService-class.html) - CAWS-specific implementation of the [DataStreamService](https://github.com/cph-cachet/carp.core-kotlin/blob/develop/docs/carp-data.md#datastreamservice) +- [`CarpService`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CarpService-class.html) - resource management (folders, documents, and files) and alternative data management service The `CarpParticipationService`, `CarpDeploymentService`, and `CarpDataStreamService` follows the [CARP Core architecture](https://github.com/cph-cachet/carp.core-kotlin?tab=readme-ov-file#architecture), and are CAWS-specific implementations of the ParticipationService, DeploymentService, and DataStreamService, respectively. The`CarpAuthService` and `CarpService` are only part of the CAWS architecture ("non-core" endpoints). ## Configuration -All CAWS services needs to be configured before used, using the `configure` method taking a [`CarpApp`](https://pub.dev/documentation/carp_webservices/latest/carp_services/CarpApp-class.html) configuration. +All CAWS services needs to be configured before used, using the `configure` method taking a [`CarpApp`](https://pub.dev/documentation/carp_webservices/latest/carp_services/CarpApp-class.html) configuration. -````dart +```dart // The URI of the CAWS server to connect to. final Uri uri = Uri( scheme: 'https', @@ -108,7 +121,7 @@ final CarpApp app = CarpApp( // Configure the CARP Service with this app. CarpService().configure(app); -```` +``` The singleton can now be accessed via `CarpService()`. @@ -126,8 +139,9 @@ Authentication is done using the `CarpAuthService` singleton, which is configure // The authentication configuration late CarpAuthProperties authProperties = CarpAuthProperties( authURL: uri, - clientId: 'studies-app', - redirectURI: Uri.parse('carp-studies-auth://auth'), + clientId: 'my-client-id', + redirectURI: Uri.parse('my-redirect-uri:/my-path'), + anonymousRedirectURI: Uri.parse('my-redirect-uri:/my-anonymous-user-path'), // For authentication at CAWS the path is '/auth/realms/Carp' discoveryURL: uri.replace(pathSegments: [ 'auth', @@ -148,7 +162,7 @@ CarpUser user = await CarpAuthService().authenticate(); This [`CarpUser`](https://pub.dev/documentation/carp_webservices/latest/carp_auth/CarpUser-class.html) object contains the OAuth token in the `token` (of type [`OAuthToken`](https://pub.dev/documentation/carp_webservices/latest/carp_auth/OAuthToken-class.html)) parameter. Since the `CarpUser` object can be serialized to JSON, the user and the (valid) OAuth token can be stored on the phone. -To refresh the OAuth token the client (Flutter) simply call: +The OAuth token can be refreshed by calling the `refresh()` method: ```dart await CarpAuthService().refresh() @@ -162,6 +176,12 @@ To authenticate using username and password without opening the web view, use th CarpUser user = await CarpAuthService().authenticateWithUsernamePassword('username', 'password'); ``` +To authenticate using a magic link (e.g., as read from a QR code) use the `authenticateWithMagicLink` method. This method takes the URL as a `String` parameter, authenticates the user, and generates and returns a `CarpUser` object. + +```dart +CarpUser user = await CarpAuthService().authenticateWithMagicLink(qrcode); +``` + To log out, just call the `logout` or `logoutNoContext` methods: ```dart @@ -172,15 +192,15 @@ await CarpAuthService().logout() A core notion of CARP is the [Deployment](https://github.com/cph-cachet/carp.core-kotlin/blob/develop/docs/carp-deployments.md) subsystem, which has two services: -* **Participation Service** - allows retrieving participation information for study deployments, and managing data related to participants which is input by users. -* **Deployment Service** - allows for retrieving primary device deployments for participating primary devices as defined in the study protocol. +- **Participation Service** - allows retrieving participation information for study deployments, and managing data related to participants which is input by users. +- **Deployment Service** - allows for retrieving primary device deployments for participating primary devices as defined in the study protocol. ### Participation Service Enables the client to get invitations for a specific `accountId`, i.e. a user. Default is the user who is authenticated to the CARP Service. ```dart -// We assume that we are authenticated to CAWS and that the CarpService() +// We assume that we are authenticated to CAWS and that the CarpService() // instance has been configured. // configure from another CAWS service @@ -286,7 +306,7 @@ The Deployment Service handles "deployment" configurations, i.e. configurations The [`CarpDeploymentService`](https://pub.dev/documentation/carp_webservices/latest/carp_services/CarpDeploymentService-class.html) has methods for getting deployments and for updating deployment and device status. Here are a list of examples: ```dart -// We assume that we are authenticated to CAWS and that the CarpService() +// We assume that we are authenticated to CAWS and that the CarpService() // instance has been configured. CarpDeploymentService().configureFrom(CarpService()); @@ -317,9 +337,9 @@ await CarpDeploymentService().deviceDeployed( However, instead of keeping track of deployment IDs, a more convenient way to access deployments are to use a [`DeploymentReference`](https://pub.dev/documentation/carp_webservices/latest/carp_services/DeploymentReference-class.html): -````dart -// We assume that we are authenticated to CAWS, that the CarpService() -// instance has been configured, and that the deployment information has +```dart +// We assume that we are authenticated to CAWS, that the CarpService() +// instance has been configured, and that the deployment information has // be saved by setting the invitation (using the 'setInvitation' method). CarpDeploymentService().configureFrom(CarpService()); @@ -340,7 +360,7 @@ var deployment = await deploymentReference.get(); // mark the deployment as a successfully deployed status = await deploymentReference.deployed(); -```` +``` ## Data Stream Service @@ -380,10 +400,10 @@ However, you would rarely need to use these endpoints in your app, since the [ca The [`CarpService`](https://pub.dev/documentation/carp_webservices/latest/carp_services/CarpService-class.html) provides access to a set of "non-core" endpoints in CAWS. These "non-core" endpoints are: -* JSON Documents organized in Collections -* File Management -* Informed Consent Documents -* Data Points +- JSON Documents organized in Collections +- File Management +- Informed Consent Documents +- Data Points All of these endpoints can be considered as additional "resources" which are available for up- or download from clients. @@ -393,10 +413,10 @@ CARP Web Service supports storing JSON documents in nested collections. A [`CollectionReference`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/CollectionReference-class.html) is used to access collections and a [`DocumentReference`](https://pub.dev/documentation/carp_webservices/latest/carp_services/DocumentReference-class.html) is used to access documents. Both of these can be used to: -* creating, updating, and deleting documents -* accessing documents in collections +- creating, updating, and deleting documents +- accessing documents in collections -`````dart +```dart // access a document // - if the document id is not specified, a new document (with a new id) // is created @@ -428,7 +448,7 @@ List collections = newDocument.collections; // get all documents in a collection. List documents = await CarpService().collection('users').documents; -````` +``` ### File Management @@ -436,15 +456,15 @@ CARP Web Service supports storing raw binary file. A [`FileStorageReference`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/FileStorageReference-class.html) is used to manage files and have methods for: -* uploading a file -* downloading a file -* getting a file object -* getting all file objects -* deleting a file +- uploading a file +- downloading a file +- getting a file object +- getting all file objects +- deleting a file When uploading a file, you can add metadata as a `Map`. -````dart +```dart // first upload a file final File uploadFile = File('test/img.jpg'); final FileUploadTask uploadTask = CarpService() @@ -474,7 +494,7 @@ final List results = // finally, delete the file responseCode = await CarpService().getFileStorageReference(id).delete(); -```` +``` ### Informed Consent Document @@ -504,12 +524,12 @@ try { A [`DataPointReference`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/DataPointReference-class.html) is used to manage [`DataPoint`](https://pub.dartlang.org/documentation/carp_webservices/latest/carp_services/DataPoint-class.html) objects on a CARP Web Service, and have CRUD methods for: -* post a data point -* batch upload multiple data points -* get a data point -* delete data points +- post a data point +- batch upload multiple data points +- get a data point +- delete data points -````dart +```dart // Create a piece of data final lightData = AmbientLight( maxLux: 12, @@ -535,7 +555,7 @@ await CarpService().getDataPointReference().batchPostDataPoint(file); // delete the data point await CarpService().getDataPointReference().deleteDataPoint(dataPointId); -```` +``` ## Features and bugs diff --git a/backends/carp_webservices/example/android/app/build.gradle b/backends/carp_webservices/example/android/app/build.gradle index 651db0fde..ea72550ee 100644 --- a/backends/carp_webservices/example/android/app/build.gradle +++ b/backends/carp_webservices/example/android/app/build.gradle @@ -10,12 +10,13 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + coreLibraryDesugaringEnabled true } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17 } sourceSets { @@ -52,3 +53,7 @@ android { flutter { source '../..' } + +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' +} \ No newline at end of file diff --git a/backends/carp_webservices/example/android/app/src/main/AndroidManifest.xml b/backends/carp_webservices/example/android/app/src/main/AndroidManifest.xml index 7b97be8a8..d535bfc42 100644 --- a/backends/carp_webservices/example/android/app/src/main/AndroidManifest.xml +++ b/backends/carp_webservices/example/android/app/src/main/AndroidManifest.xml @@ -1,14 +1,15 @@ - + + - + + + + + + + + + + + CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/backends/carp_webservices/example/ios/Podfile b/backends/carp_webservices/example/ios/Podfile index e549ee22f..abab77261 100644 --- a/backends/carp_webservices/example/ios/Podfile +++ b/backends/carp_webservices/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -37,6 +37,13 @@ target 'Runner' do end post_install do |installer| + installer.generated_projects.each do |project| + project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + end + end + end installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end diff --git a/backends/carp_webservices/example/ios/Runner.xcodeproj/project.pbxproj b/backends/carp_webservices/example/ios/Runner.xcodeproj/project.pbxproj index 86e08df64..d7b18b637 100644 --- a/backends/carp_webservices/example/ios/Runner.xcodeproj/project.pbxproj +++ b/backends/carp_webservices/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,15 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 0657D25496622872DF99F367 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93B688AB7918D531FF689243 /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 72165B41B60BCF74600DA7D3 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CF3ECDDD84A6A366482BECD /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 799A7E68E99983A3C6D7BC7B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD2DC4951A885CE53046AB17 /* Pods_RunnerTests.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A2B3252C6672755000F0B455 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83FF1F6FB1A0C3C0C3057D1B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,19 +42,17 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0CF3ECDDD84A6A366482BECD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 13F27597EBD6DE109B72B4B2 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2079A154F38AADEA3137AA17 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 27A35C6E5576827EB66F991D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 69909DA31FF680C4C26A004F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 83FF1F6FB1A0C3C0C3057D1B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 865B223993943E75694FB50F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 93B688AB7918D531FF689243 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -62,9 +60,11 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AFF9A7C281700F7C44ABB108 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - BB6C9BE0C31D035E1D10E8A7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - D7DF891148046E4555FF3EDA /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + A47B808BE6ACD15C77E1B1AA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + DD2DC4951A885CE53046AB17 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E5A5B624AB566C830331DCA1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F8702A9996666982E6768873 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F9748A7EC2E30806F27FD9A5 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,7 +72,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 72165B41B60BCF74600DA7D3 /* Pods_RunnerTests.framework in Frameworks */, + 799A7E68E99983A3C6D7BC7B /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,7 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A2B3252C6672755000F0B455 /* Pods_Runner.framework in Frameworks */, + 0657D25496622872DF99F367 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -98,17 +98,25 @@ 3E5AA760329A9B9A25E7F7CC /* Pods */ = { isa = PBXGroup; children = ( - 69909DA31FF680C4C26A004F /* Pods-Runner.debug.xcconfig */, - AFF9A7C281700F7C44ABB108 /* Pods-Runner.release.xcconfig */, - BB6C9BE0C31D035E1D10E8A7 /* Pods-Runner.profile.xcconfig */, - D7DF891148046E4555FF3EDA /* Pods-RunnerTests.debug.xcconfig */, - 865B223993943E75694FB50F /* Pods-RunnerTests.release.xcconfig */, - 13F27597EBD6DE109B72B4B2 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; + E5A5B624AB566C830331DCA1 /* Pods-Runner.debug.xcconfig */, + A47B808BE6ACD15C77E1B1AA /* Pods-Runner.release.xcconfig */, + 2079A154F38AADEA3137AA17 /* Pods-Runner.profile.xcconfig */, + F8702A9996666982E6768873 /* Pods-RunnerTests.debug.xcconfig */, + 27A35C6E5576827EB66F991D /* Pods-RunnerTests.release.xcconfig */, + F9748A7EC2E30806F27FD9A5 /* Pods-RunnerTests.profile.xcconfig */, + ); path = Pods; sourceTree = ""; }; + 5F91D5AA905742E4EF2CF992 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 93B688AB7918D531FF689243 /* Pods_Runner.framework */, + DD2DC4951A885CE53046AB17 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -128,7 +136,7 @@ 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 3E5AA760329A9B9A25E7F7CC /* Pods */, - CAFBE04803D76EF2E0AA0002 /* Frameworks */, + 5F91D5AA905742E4EF2CF992 /* Frameworks */, ); sourceTree = ""; }; @@ -156,15 +164,6 @@ path = Runner; sourceTree = ""; }; - CAFBE04803D76EF2E0AA0002 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 83FF1F6FB1A0C3C0C3057D1B /* Pods_Runner.framework */, - 0CF3ECDDD84A6A366482BECD /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -172,7 +171,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 42EA91BC75AECD152D579EB4 /* [CP] Check Pods Manifest.lock */, + 49124878560B70E4455FFAA6 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 5497E7C10C44DF00597297D1 /* Frameworks */, @@ -191,15 +190,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 416A4BCDEFAC86B5F7B3026B /* [CP] Check Pods Manifest.lock */, + 5A20305488CCEE546D681D40 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 087099483AE4FBD547CD162C /* [CP] Embed Pods Frameworks */, - 9E898933E584A570C95695E1 /* [CP] Copy Pods Resources */, + 8A53767D787EA8FCA422E5CB /* [CP] Embed Pods Frameworks */, + 147D609C434252DB2B306015 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -271,21 +270,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 087099483AE4FBD547CD162C /* [CP] Embed Pods Frameworks */ = { + 147D609C434252DB2B306015 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -304,7 +303,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 416A4BCDEFAC86B5F7B3026B /* [CP] Check Pods Manifest.lock */ = { + 49124878560B70E4455FFAA6 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -319,14 +318,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 42EA91BC75AECD152D579EB4 /* [CP] Check Pods Manifest.lock */ = { + 5A20305488CCEE546D681D40 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -341,44 +340,44 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 8A53767D787EA8FCA422E5CB /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Run Script"; - outputPaths = ( + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 9E898933E584A570C95695E1 /* [CP] Copy Pods Resources */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + name = "Run Script"; + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ @@ -473,7 +472,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -492,26 +491,33 @@ DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D7DF891148046E4555FF3EDA /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = F8702A9996666982E6768873 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -524,12 +530,13 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 865B223993943E75694FB50F /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 27A35C6E5576827EB66F991D /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -540,12 +547,13 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 13F27597EBD6DE109B72B4B2 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = F9748A7EC2E30806F27FD9A5 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -603,7 +611,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -654,7 +662,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -675,15 +683,21 @@ DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -698,14 +712,20 @@ DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/backends/carp_webservices/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/backends/carp_webservices/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5dfe..e3773d42e 100644 --- a/backends/carp_webservices/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/backends/carp_webservices/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/backends/carp_webservices/example/ios/Runner/AppDelegate.swift b/backends/carp_webservices/example/ios/Runner/AppDelegate.swift index 70693e4a8..b63630348 100644 --- a/backends/carp_webservices/example/ios/Runner/AppDelegate.swift +++ b/backends/carp_webservices/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/backends/carp_webservices/example/ios/Runner/Info.plist b/backends/carp_webservices/example/ios/Runner/Info.plist index 6d1fe121b..0f4845c21 100644 --- a/backends/carp_webservices/example/ios/Runner/Info.plist +++ b/backends/carp_webservices/example/ios/Runner/Info.plist @@ -53,8 +53,12 @@ CFBundleURLSchemes dk.cachet.example + caws-example-app-auth + caws-example-app + NSCameraUsageDescription + CARP uses the camera to take pictures and/or videos for some tasks \ No newline at end of file diff --git a/backends/carp_webservices/example/lib/main.dart b/backends/carp_webservices/example/lib/main.dart index 53bae7daf..2ae46547f 100644 --- a/backends/carp_webservices/example/lib/main.dart +++ b/backends/carp_webservices/example/lib/main.dart @@ -1,9 +1,17 @@ -import 'package:carp_mobile_sensing/carp_mobile_sensing.dart'; +library; + +import 'dart:io'; import 'package:flutter/material.dart'; + +import 'package:carp_mobile_sensing/carp_mobile_sensing.dart'; import 'package:carp_webservices/carp_services/carp_services.dart'; import 'package:carp_webservices/carp_auth/carp_auth.dart'; import 'package:carp_core/carp_core.dart'; + import 'package:oidc/oidc.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart' as qr; + +part 'qr-scanner.dart'; void main() { CarpMobileSensing.ensureInitialized(); @@ -53,14 +61,31 @@ class HomePageState extends State { stream: CarpAuthService().manager?.userChanges(), builder: (BuildContext context, AsyncSnapshot event) { if (!event.hasData) { - return TextButton.icon( - onPressed: () async => bloc.currentUser = - await CarpAuthService().authenticate(), - icon: const Icon(Icons.login), - label: const Text( - 'LOGIN', - style: TextStyle(fontSize: 35), - ), + return Column( + children: [ + TextButton.icon( + onPressed: () { + showDialog( + context: context, + builder: (context) => QRViewExample(), + ); + }, + icon: const Icon(Icons.login), + label: const Text( + 'SCAN', + style: TextStyle(fontSize: 35), + ), + ), + TextButton.icon( + onPressed: () async => bloc.currentUser = + await CarpAuthService().authenticate(), + icon: const Icon(Icons.login), + label: const Text( + 'LOGIN', + style: TextStyle(fontSize: 35), + ), + ) + ], ); } else { return TextButton.icon( @@ -88,7 +113,7 @@ class HomePageState extends State { padding: const EdgeInsets.fromLTRB(10, 30, 10, 0), child: Text( (CarpAuthService().authenticated) - ? 'Authenticated as ${CarpAuthService().currentUser.firstName} ${CarpAuthService().currentUser.lastName}' + ? 'Authenticated as ${CarpAuthService().currentUser.username} ${CarpAuthService().currentUser.firstName} ${CarpAuthService().currentUser.lastName}' : 'Not authenticated', textAlign: TextAlign.center, ), @@ -117,8 +142,9 @@ class AppBLoC { // The authentication configuration late CarpAuthProperties authProperties = CarpAuthProperties( authURL: uri, - clientId: 'studies-app', - redirectURI: Uri.parse('carp-studies-auth://auth'), + clientId: 'caws-example-app', + redirectURI: Uri.parse('caws-example-app-auth://auth'), + anonymousRedirectURI: Uri.parse('caws-example-app:/anonymous'), // For authentication at CAWS the path is '/auth/realms/Carp' discoveryURL: uri.replace(pathSegments: [ 'auth', diff --git a/backends/carp_webservices/example/lib/qr-scanner.dart b/backends/carp_webservices/example/lib/qr-scanner.dart new file mode 100644 index 000000000..2847ae3e3 --- /dev/null +++ b/backends/carp_webservices/example/lib/qr-scanner.dart @@ -0,0 +1,136 @@ +part of 'main.dart'; + +class QRViewExample extends StatefulWidget { + const QRViewExample({super.key}); + + @override + State createState() => _QRViewExampleState(); +} + +class _QRViewExampleState extends State { + qr.Barcode? result; + qr.QRViewController? controller; + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + + // In order to get hot reload to work we need to pause the camera if the platform + // is android, or resume the camera if the platform is iOS. + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } + controller!.resumeCamera(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + Expanded(flex: 4, child: _buildQrView(context)), + Expanded( + flex: 1, + child: FittedBox( + fit: BoxFit.contain, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: const EdgeInsets.all(8), + height: 30, + child: ElevatedButton( + onPressed: () async { + await controller?.flipCamera(); + setState(() {}); + }, + child: FutureBuilder( + future: controller?.getCameraInfo(), + builder: (context, snapshot) { + if (snapshot.data != null) { + return Icon(Icons.cameraswitch); + } else { + return const Text('loading'); + } + }, + )), + ), + Container( + margin: const EdgeInsets.all(8), + height: 30, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Icon(Icons.close), + ), + ), + ], + ), + ], + ), + ), + ) + ], + ), + ), + ); + } + + Widget _buildQrView(BuildContext context) { + // For this example we check how width or tall the device is and change the scanArea and overlay accordingly. + var scanArea = (MediaQuery.of(context).size.width < 400 || + MediaQuery.of(context).size.height < 400) + ? 150.0 + : 300.0; + // To ensure the Scanner view is properly sizes after rotation + // we need to listen for Flutter SizeChanged notification and update controller + return qr.QRView( + key: qrKey, + onQRViewCreated: _onQRViewCreated, + overlay: qr.QrScannerOverlayShape( + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea), + onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), + ); + } + + void _onQRViewCreated(qr.QRViewController controller) { + setState(() { + this.controller = controller; + }); + controller.scannedDataStream.listen((scanData) async { + await controller.pauseCamera(); + if (result != null) return; + setState(() { + result = scanData; + }); + + final qrcode = scanData.code; + + if (qrcode != null && Uri.tryParse(qrcode)?.hasAbsolutePath == true) { + bloc.currentUser = + await CarpAuthService().authenticateWithMagicLink(qrcode).then((_) { + Navigator.of(context).pop(); + }); + } + }); + } + + void _onPermissionSet( + BuildContext context, qr.QRViewController ctrl, bool p) { + if (!p) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('no Permission')), + ); + } + } +} diff --git a/backends/carp_webservices/example/pubspec.yaml b/backends/carp_webservices/example/pubspec.yaml index c76d0dee7..c458209f5 100644 --- a/backends/carp_webservices/example/pubspec.yaml +++ b/backends/carp_webservices/example/pubspec.yaml @@ -4,15 +4,16 @@ homepage: https://github.com/cph-cachet/carp.sensing-flutter publish_to: none environment: - sdk: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" dependencies: flutter: sdk: flutter - carp_core: ^1.8.0 - carp_mobile_sensing: ^1.11.0 + carp_core: ^1.9.0 + carp_mobile_sensing: ^1.13.0 + qr_code_scanner_plus: ^2.0.12 # carp_core: # path: ../../../carp_core @@ -21,7 +22,7 @@ dependencies: carp_webservices: path: ../ - oidc: ^0.9.0+1 + oidc: ^0.12.0 dev_dependencies: test: any diff --git a/backends/carp_webservices/lib/carp_auth/carp_auth.dart b/backends/carp_webservices/lib/carp_auth/carp_auth.dart index 5ac955c80..bf4294d1b 100644 --- a/backends/carp_webservices/lib/carp_auth/carp_auth.dart +++ b/backends/carp_webservices/lib/carp_auth/carp_auth.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'package:carp_webservices/carp_services/carp_services.dart'; +import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:oidc/oidc.dart'; import 'package:oidc_default_store/oidc_default_store.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; part 'oauth.dart'; part 'carp_user.dart'; diff --git a/backends/carp_webservices/lib/carp_auth/carp_auth_properties.dart b/backends/carp_webservices/lib/carp_auth/carp_auth_properties.dart index 7a96a975a..673bb2b36 100644 --- a/backends/carp_webservices/lib/carp_auth/carp_auth_properties.dart +++ b/backends/carp_webservices/lib/carp_auth/carp_auth_properties.dart @@ -14,6 +14,9 @@ class CarpAuthProperties { /// Redirect URI for OAuth final Uri redirectURI; + /// Same as [redirectURI] but for anonymous authentication using qr code. + Uri? anonymousRedirectURI; + /// Redirect uri for OAuth after logout /// If not specified, the [redirectURI] is used. Uri? logoutRedirectURI; @@ -39,6 +42,7 @@ class CarpAuthProperties { required this.clientId, this.clientSecret, required this.redirectURI, + this.anonymousRedirectURI, required this.discoveryURL, this.studyDeploymentId, this.studyId, diff --git a/backends/carp_webservices/lib/carp_auth/carp_auth_service.dart b/backends/carp_webservices/lib/carp_auth/carp_auth_service.dart index edfcde254..689496204 100644 --- a/backends/carp_webservices/lib/carp_auth/carp_auth_service.dart +++ b/backends/carp_webservices/lib/carp_auth/carp_auth_service.dart @@ -93,8 +93,9 @@ class CarpAuthService { redirectUri: Uri.parse(authProperties.redirectURI.toString()), scope: ['openid', 'offline_access'], postLogoutRedirectUri: Uri.parse( - (authProperties.logoutRedirectURI ?? authProperties.redirectURI) - .toString()), + (authProperties.logoutRedirectURI ?? authProperties.redirectURI) + .toString(), + ), options: const OidcPlatformSpecificOptions( web: OidcPlatformSpecificOptions_Web( navigationMode: @@ -143,8 +144,9 @@ class CarpAuthService { _currentUser = getCurrentUserProfile(response); if (_currentUser != null) { - _currentUser! - .authenticated(OAuthToken.fromTokenResponse(response.token)); + _currentUser!.authenticated( + OAuthToken.fromTokenResponse(response.token), + ); _authEventController.add(AuthEvent.authenticated); return currentUser; } @@ -159,6 +161,82 @@ class CarpAuthService { ); } + /// Authenticate to this CARP service using an magic link URI generated by CAWS + /// as part of an anonymous access flow. + /// + /// The magic link can be found inside the .csv file downloaded from the portal. + Future authenticateWithMagicLink(String uri) async { + assert(_manager != null, 'Manager not configured. Call configure() first.'); + if (!_manager!.didInit) await initManager(); + + String? code; + String? clientId = _authProperties?.clientId; + String? redirectUri = _authProperties?.anonymousRedirectURI?.toString(); + + TokenResponse tokenResponse = await FlutterWebAuth2.authenticate( + url: uri, + callbackUrlScheme: redirectUri!.split(':/').first, + options: FlutterWebAuth2Options( + intentFlags: ephemeralIntentFlags, + preferEphemeral: true, + ), + ).then((result) async { + code = Uri.parse(result).queryParameters['code']; + if ((_currentUser == null || _currentUser!.isAuthenticated) && + code != null) { + return await FlutterAppAuth().token( + TokenRequest( + clientId!, + redirectUri, + authorizationCode: code, + discoveryUrl: _authProperties?.discoveryURL.replace( + pathSegments: [ + ...?_authProperties?.discoveryURL.pathSegments, + '.well-known', + 'openid-configuration', + ], + ).toString(), + grantType: 'authorization_code', + ), + ); + } + return Future.error("No code in redirect URI"); + }); + + _currentUser = getCurrentUserProfileFromTokenResponse(tokenResponse); + + final accessToken = tokenResponse.accessToken; + final refreshToken = tokenResponse.refreshToken; + final idToken = tokenResponse.idToken; + final scopeString = tokenResponse.tokenAdditionalParameters?['scope'] ?? + tokenResponse.tokenType; + final scope = (scopeString is String) ? scopeString.split(' ') : []; + final expiresAt = tokenResponse.accessTokenExpirationDateTime ?? + DateTime.now().add(const Duration(hours: 1)); + + if (_currentUser != null) { + _currentUser!.authenticated( + OAuthToken( + accessToken ?? '', + refreshToken ?? '', + idToken ?? '', + expiresAt, + scope, + idToken ?? '', + ), + ); + _authEventController.add(AuthEvent.authenticated); + return currentUser; + } + + // All other cases are treated as a failed attempt + _authEventController.add(AuthEvent.failed); + throw CarpServiceException( + httpStatus: HTTPStatus(401), + message: 'Authentication failed.', + ); + } + /// Authenticate to this CARP service using a [username] and [password]. /// /// The discovery URL in the [authProperties] is used to find the Identity Server. @@ -181,8 +259,9 @@ class CarpAuthService { _currentUser = getCurrentUserProfile(response); if (_currentUser != null) { - _currentUser! - .authenticated(OAuthToken.fromTokenResponse(response.token)); + _currentUser!.authenticated( + OAuthToken.fromTokenResponse(response.token), + ); _authEventController.add(AuthEvent.authenticated); return currentUser; } @@ -216,8 +295,9 @@ class CarpAuthService { _currentUser = getCurrentUserProfile(response); if (_currentUser != null) { - _currentUser! - .authenticated(OAuthToken.fromTokenResponse(response.token)); + _currentUser!.authenticated( + OAuthToken.fromTokenResponse(response.token), + ); _authEventController.add(AuthEvent.authenticated); return currentUser; } @@ -272,18 +352,54 @@ class CarpAuthService { return CarpUser.fromJWT(jwt, user.token); } + /// Gets the CARP profile of the current user from a [TokenResponse]. + /// Using the parameters in the [TokenResponse] we create an [OAuthToken] + /// to generate the CarpUser. + CarpUser? getCurrentUserProfileFromTokenResponse( + TokenResponse tokenResponse, + ) { + final accessToken = tokenResponse.accessToken; + final refreshToken = tokenResponse.refreshToken; + final idToken = tokenResponse.idToken; + final tokenType = 'bearer'; + final scopeString = tokenResponse.tokenAdditionalParameters?['scope'] ?? + tokenResponse.tokenType; + final scope = (scopeString is String) ? scopeString.split(' ') : []; + + if (accessToken == null || accessToken.isEmpty) { + return null; + } + + final jwt = JwtDecoder.decode(accessToken); + final expiresAt = tokenResponse.accessTokenExpirationDateTime ?? + DateTime.now().add(const Duration(hours: 1)); + + final oauthToken = OAuthToken( + accessToken, + refreshToken ?? '', + tokenType, + expiresAt, + scope, + idToken ?? '', + ); + + return CarpUser.fromJWTOAuth(jwt, oauthToken); + } + /// Makes sure that the [CarpApp] or [CarpUser] is configured, by throwing a /// [CarpServiceException] if they are null. /// Otherwise, returns the non-null value. T nonNullAble(T? argument) { if (argument == null && argument is CarpApp) { throw CarpServiceException( - message: - "CARP Service not initialized. Call 'CarpAuthService().configure()' first."); + message: + "CARP Service not initialized. Call 'CarpAuthService().configure()' first.", + ); } else if (argument == null && argument is CarpUser) { throw CarpServiceException( - message: - "CARP User not authenticated. Call 'CarpAuthService().authenticate()' first."); + message: + "CARP User not authenticated. Call 'CarpAuthService().authenticate()' first.", + ); } else { return argument!; } diff --git a/backends/carp_webservices/lib/carp_auth/carp_user.dart b/backends/carp_webservices/lib/carp_auth/carp_user.dart index 100d70395..bad4ecad3 100644 --- a/backends/carp_webservices/lib/carp_auth/carp_user.dart +++ b/backends/carp_webservices/lib/carp_auth/carp_user.dart @@ -65,6 +65,19 @@ class CarpUser { ); } + factory CarpUser.fromJWTOAuth(Map jwt, OAuthToken token) { + return CarpUser( + username: jwt['preferred_username'] as String, + id: jwt['sub'] as String, + firstName: jwt['given_name'] as String?, + lastName: jwt['family_name'] as String?, + email: jwt['email'] as String?, + roles: (jwt['realm_access']?['roles'] as List?) ?? [], + token: token, + ); +} + + factory CarpUser.fromJson(Map json) => _$CarpUserFromJson(json); Map toJson() => _$CarpUserToJson(this); diff --git a/backends/carp_webservices/pubspec.yaml b/backends/carp_webservices/pubspec.yaml index badca559b..1c96dfebf 100644 --- a/backends/carp_webservices/pubspec.yaml +++ b/backends/carp_webservices/pubspec.yaml @@ -1,11 +1,11 @@ name: carp_webservices -description: Flutter API for accessing the CARP web services - authentication, file management, data points, and app-specific collections of documents. -version: 3.7.0 +description: Flutter API for accessing the CARP web services, including authentication, deployments, data, files, and collections of documents. +version: 3.8.0 homepage: https://github.com/cph-cachet/carp.sensing-flutter environment: - sdk: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" platforms: android: @@ -16,25 +16,26 @@ dependencies: sdk: flutter carp_serializable: ^2.0.0 - carp_core: ^1.8.0 - carp_mobile_sensing: ^1.11.0 + carp_core: ^1.9.0 + carp_mobile_sensing: ^1.13.0 http: ^1.1.0 json_annotation: ^4.8.0 retry: ^3.1.0 meta: ^1.7.0 - url_launcher: ^6.0.9 + url_launcher: ^6.1.5 jwt_decoder: ^2.0.1 - oidc: ^0.9.0+1 - oidc_default_store: ^0.2.0+8 + oidc: ^0.12.0 + oidc_default_store: ^0.4.0 + flutter_web_auth_2: ^4.1.0 # Overriding carp libraries to use the local copy # Remove this before release of package dependency_overrides: # carp_serializable: # path: ../../carp_serializable/ - carp_core: - path: ../../carp_core/ + # carp_core: + # path: ../../carp_core/ # carp_mobile_sensing: # path: ../../carp_mobile_sensing/