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/