diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c15e186f8..ddae7d656 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v2 - name: Cache - uses: actions/cache@v2.0.0 + uses: actions/cache@v4 with: # Cache gradle directories path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f54cf93d8..1b2e6b112 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v2 - name: Cache - uses: actions/cache@v2.0.0 + uses: actions/cache@v4 with: # Cache gradle directories path: | diff --git a/android/app/build.gradle b/android/app/build.gradle index ca580d75a..12498070d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "org.phidatalab.radar_armt" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 592 - versionName "3.3.3-alpha" + versionCode 593 + versionName "3.3.4-alpha" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 0878601f7..7537a286c 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -9,21 +9,22 @@ /* Begin PBXBuildFile section */ 2DCC4441ECB6AE53462C7302 /* Pods_RADAR_Active_RMT.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3FBB3316FEBE50D0F3BCBA4C /* Pods_RADAR_Active_RMT.framework */; }; 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; - 4C4FF1572BEBFDF000B0D335 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C4FF1562BEBFDF000B0D335 /* GoogleService-Info.plist */; }; + 4CAE23452D50FE9900E2E2EC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4CAE23442D50FE9900E2E2EC /* GoogleService-Info.plist */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + F5307CDA0B504AB59720284B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3EEFC32190254FBBA57F3349 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 0066ED161F09C0B9FCA0716A /* Pods-RADAR Active RMT.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RADAR Active RMT.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RADAR Active RMT/Pods-RADAR Active RMT.debug.xcconfig"; sourceTree = ""; }; 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; 3FBB3316FEBE50D0F3BCBA4C /* Pods_RADAR_Active_RMT.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RADAR_Active_RMT.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4C4FF1562BEBFDF000B0D335 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../GoogleService-Info.plist"; sourceTree = ""; }; 4C4FF1592BEC015000B0D335 /* RADAR Questionnaire.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "RADAR Questionnaire.entitlements"; sourceTree = ""; }; + 4CAE23442D50FE9900E2E2EC /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../../../Downloads/GoogleService-Info.plist"; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; 504EC3041FED79650016851F /* RADAR Active RMT.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "RADAR Active RMT.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -37,6 +38,7 @@ A4424794DED16B074B2B4CA9 /* Pods-RADAR Active RMT.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RADAR Active RMT.release.xcconfig"; path = "Pods/Target Support Files/Pods-RADAR Active RMT/Pods-RADAR Active RMT.release.xcconfig"; sourceTree = ""; }; AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; + 3EEFC32190254FBBA57F3349 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; name = "PrivacyInfo.xcprivacy"; path = "PrivacyInfo.xcprivacy"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,6 +69,7 @@ 504EC3051FED79650016851F /* Products */, 7F8756D8B27F46E3366F6CEA /* Pods */, 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, + 3EEFC32190254FBBA57F3349 /* Resources */, ); sourceTree = ""; }; @@ -81,10 +84,10 @@ 504EC3061FED79650016851F /* App */ = { isa = PBXGroup; children = ( + 4CAE23442D50FE9900E2E2EC /* GoogleService-Info.plist */, 50379B222058CBB4000EE86E /* capacitor.config.json */, 504EC3071FED79650016851F /* AppDelegate.swift */, 504EC30B1FED79650016851F /* Main.storyboard */, - 4C4FF1562BEBFDF000B0D335 /* GoogleService-Info.plist */, 504EC30E1FED79650016851F /* Assets.xcassets */, 504EC3101FED79650016851F /* LaunchScreen.storyboard */, 504EC3131FED79650016851F /* Info.plist */, @@ -107,6 +110,15 @@ name = Pods; sourceTree = ""; }; + 68FC8506249F495CBBFC6201 /* Resources */ = { + isa = PBXGroup; + children = ( + 3EEFC32190254FBBA57F3349 /* PrivacyInfo.xcprivacy */, + ); + name = Resources; + path = undefined; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -135,8 +147,8 @@ 504EC2FC1FED79650016851F /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0920; + LastSwiftUpdateCheck = 920; + LastUpgradeCheck = 920; TargetAttributes = { 504EC3031FED79650016851F = { CreatedOnToolsVersion = 9.2; @@ -174,7 +186,8 @@ 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, 504EC30D1FED79650016851F /* Main.storyboard in Resources */, 2FAD9763203C412B000D30F8 /* config.xml in Resources */, - 4C4FF1572BEBFDF000B0D335 /* GoogleService-Info.plist in Resources */, + 4CAE23452D50FE9900E2E2EC /* GoogleService-Info.plist in Resources */, + F5307CDA0B504AB59720284B /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -366,7 +379,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 3.3.3; + MARKETING_VERSION = 3.3.4; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "org.phidatalab.radar-armt"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -388,7 +401,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 3.3.3; + MARKETING_VERSION = 3.3.4; PRODUCT_BUNDLE_IDENTIFIER = "org.phidatalab.radar-armt"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 1510729da..968b3ce9f 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.3.3 + 3.3.4 CFBundleURLTypes diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 070102ca4..149bd5227 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - Capacitor (5.7.0): + - Capacitor (5.7.4): - CapacitorCordova - CapacitorApp (5.0.7): - Capacitor @@ -9,7 +9,7 @@ PODS: - Capacitor - CapacitorCommunityKeepAwake (4.0.0): - Capacitor - - CapacitorCordova (5.7.0) + - CapacitorCordova (5.7.4) - CapacitorDevice (5.0.7): - Capacitor - CapacitorDialog (5.0.7): @@ -49,7 +49,7 @@ PODS: - Capacitor - CapacitorVoiceRecorder (6.0.3): - Capacitor - - CordovaPlugins (5.7.0): + - CordovaPlugins (5.7.4): - CapacitorCordova - Firebase/CoreOnly (10.21.0): - FirebaseCore (= 10.21.0) @@ -304,12 +304,12 @@ EXTERNAL SOURCES: :path: "../../node_modules/@perfood/capacitor-healthkit" SPEC CHECKSUMS: - Capacitor: fc155ee2ee45a2093d716f13cf5aa5a865e2d85a + Capacitor: 4fe9adf012caceb4c71ffea2f1f4d005cdcbeea7 CapacitorApp: 17fecd0e6cb23feafac7eb0939417389038b0979 CapacitorAppLauncher: 7b2705481a74cbe322441bd374f56194de59ab1a CapacitorBrowser: a6deae9e5bf87f62b62a753cff7992c5def9e771 CapacitorCommunityKeepAwake: f3442e3e9b666dd0162522fbd053da1fe81ede20 - CapacitorCordova: e825fce1a2e14e4b5730641c7e098dccf74397b7 + CapacitorCordova: a6e87fccc0307dee7aec1560ec9398485f2b0ce7 CapacitorDevice: fc91bdb484dc0e70755e9b621cd557afe642613a CapacitorDialog: ea1beaccb7f825191aa18b72cba8981b881b3b2d CapacitorFilesystem: 9f3e3c7fea2fff12f46dd5b07a2914f2103e4cfc @@ -325,7 +325,7 @@ SPEC CHECKSUMS: CapacitorStatusBar: f390fbb49b82ffb754ea4b3cf71dc8b048baf3e7 CapacitorTextZoom: 69814a62b1d0f23a899cca06ea9838c38de187ff CapacitorVoiceRecorder: d44c1da901cc3918eb9c92c24834d8dc90f071f4 - CordovaPlugins: a707edf6d79ba00bd300f341fe9e1ada032d211f + CordovaPlugins: a17fcaee819862ac7aca195699992de19cf191d4 Firebase: 4453b799f72f625384dc23f412d3be92b0e3b2a0 FirebaseABTesting: 40774deef367dcc7b736b6c26dd59ce0fab42f41 FirebaseAnalytics: d275f288881d4417f780115dd52c05fa9752d530 diff --git a/ios/App/PrivacyInfo.xcprivacy b/ios/App/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..ed9f35761 --- /dev/null +++ b/ios/App/PrivacyInfo.xcprivacy @@ -0,0 +1,29 @@ + + + + + NSPrivacyTracking + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + NSPrivacyCollectedDataTypes + + + \ No newline at end of file diff --git a/package.json b/package.json index c318a5f07..ef22902ff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "radar-questionnaire", "description": "An application that collects active data for research.", - "version": "3.3.3", + "version": "3.3.4", "author": "RADAR Base", "homepage": "http://www.radar-base.org/", "scripts": { @@ -53,16 +53,16 @@ "@capacitor-firebase/messaging": "^5.0.0", "@capacitor-firebase/remote-config": "^5.0.0", "@capacitor-mlkit/barcode-scanning": "^5.4.0", - "@capacitor/android": "^5.0.3", + "@capacitor/android": "5.7.4", "@capacitor/app": "5.0.7", "@capacitor/app-launcher": "^5.0.7", "@capacitor/browser": "^5.2.0", - "@capacitor/core": "5.7.0", + "@capacitor/core": "5.7.4", "@capacitor/device": "^5.0.2", "@capacitor/dialog": "^5.0.2", "@capacitor/filesystem": "^5.0.2", "@capacitor/haptics": "5.0.7", - "@capacitor/ios": "^5.0.3", + "@capacitor/ios": "5.7.4", "@capacitor/keyboard": "^5.0.8", "@capacitor/local-notifications": "^5.0.2", "@capacitor/splash-screen": "^5.0.2", @@ -117,7 +117,7 @@ "@angular/compiler-cli": "~14.3.0", "@angular/language-service": "~14.3.0", "@angular/platform-server": "~14.3.0", - "@capacitor/cli": "5.7.0", + "@capacitor/cli": "5.7.4", "@ionic/angular-toolkit": "^7.0.0", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "~2.0.3", @@ -174,7 +174,7 @@ "loader-utils": "2.0.4", "nth-check": "2.0.1", "webpack": "5.76.0", - "@capacitor/core": "5.7.0", + "@capacitor/core": "5.7.4", "undici": "6.6.1" }, "ionic_enable_lint": false diff --git a/src/app/core/services/config/config.service.ts b/src/app/core/services/config/config.service.ts index a7a76fc24..153c3b5c4 100755 --- a/src/app/core/services/config/config.service.ts +++ b/src/app/core/services/config/config.service.ts @@ -59,7 +59,8 @@ export class ConfigService { this.hasAppVersionChanged(), this.hasTimezoneChanged(), this.hasNotificationsExpired(), - this.hasNotificationMessagingTypeChanged() + this.hasNotificationMessagingTypeChanged(), + this.hasParticipantAttributesChanged() ]) .then( ([ @@ -67,8 +68,11 @@ export class ConfigService { newAppVersion, newTimezone, newNotifications, - newMessagingType + newMessagingType, + newAttributes ]) => { + if (newAttributes) + return this.fetchConfigState(true) if (newProtocol && newAppVersion && newTimezone) this.subjectConfig .getEnrolmentDate() @@ -96,6 +100,22 @@ export class ConfigService { }) } + hasParticipantAttributesChanged() { + return Promise.all([ + this.subjectConfig.getParticipantAttributes(), + this.subjectConfig.pullSubjectInformation(), + ]) + .then(([attributes, user]) => { + const newAttributes = user.attributes + const hasChanged = JSON.stringify(attributes) !== JSON.stringify(user.attributes) + if (hasChanged) { + this.subjectConfig.setParticipantAttributes(newAttributes) + this.analytics.setUserProperties(newAttributes) + } + return hasChanged + }) + } + hasProtocolChanged(force?) { return Promise.all([ this.appConfig.getScheduleHashUrl(), diff --git a/src/app/core/services/config/protocol.service.ts b/src/app/core/services/config/protocol.service.ts index 32e5fcaac..6f23722f2 100644 --- a/src/app/core/services/config/protocol.service.ts +++ b/src/app/core/services/config/protocol.service.ts @@ -36,7 +36,7 @@ export class ProtocolService { private logger: LogService, private analytics: AnalyticsService, private util: Utility - ) {} + ) { } pull(): Promise { return Promise.all([this.getProjectTree(), this.getParticipantAttributes()]) @@ -176,20 +176,13 @@ export class ProtocolService { .then(cfg => Promise.all([ this.config.getParticipantAttributes(), - this.config.pullSubjectInformation(), this.getParticipantAttributeOrder(cfg) ]) ) - .then(([attributes, user, order]) => { - const newAttributes = - JSON.stringify(attributes) == JSON.stringify(user.attributes) - ? attributes - : user.attributes - this.config.setParticipantAttributes(newAttributes) - this.analytics.setUserProperties(newAttributes) + .then(([attributes, order]) => { return sortObject( - newAttributes, - this.formatAttributeOrder(newAttributes, order) + attributes, + this.formatAttributeOrder(attributes, order) ) }) } @@ -201,8 +194,8 @@ export class ProtocolService { const orderWithoutNull = {} Object.keys(attributes).map( k => - (orderWithoutNull[k] = - order[k] != null ? order[k] : this.DEFAULT_ATTRIBUTE_ORDER) + (orderWithoutNull[k] = + order[k] != null ? order[k] : this.DEFAULT_ATTRIBUTE_ORDER) ) return orderWithoutNull } diff --git a/src/app/core/services/kafka/converters/converter-factory.service..ts b/src/app/core/services/kafka/converters/converter-factory.service..ts index 938594b08..f933da405 100644 --- a/src/app/core/services/kafka/converters/converter-factory.service..ts +++ b/src/app/core/services/kafka/converters/converter-factory.service..ts @@ -18,9 +18,9 @@ export class ConverterFactoryService { private completionLogConverter: CompletionLogConverterService, private timzoneConverter: TimezoneConverterService, private keyConverter: KeyConverterService - ) {} + ) { } - init() {} + init() { } getConverter(type) { switch (this.classify(type)) { @@ -45,4 +45,13 @@ export class ConverterFactoryService { if (type.includes(SchemaType.HEALTHKIT)) return SchemaType.HEALTHKIT else return type } + + reset() { + this.healthkitConverter.reset() + this.assessmentConverter.reset() + this.completionLogConverter.reset() + this.timzoneConverter.reset() + this.appEventConverter.reset() + this.assessmentConverter.reset() + } } diff --git a/src/app/core/services/kafka/converters/converter.service.ts b/src/app/core/services/kafka/converters/converter.service.ts index ca4239d77..f5aeda51a 100644 --- a/src/app/core/services/kafka/converters/converter.service.ts +++ b/src/app/core/services/kafka/converters/converter.service.ts @@ -132,4 +132,10 @@ export abstract class ConverterService { return false } } + + reset() { + this.BASE_URI = null + this.specifications = null + this.schemas = {} + } } diff --git a/src/app/core/services/kafka/schema.service.ts b/src/app/core/services/kafka/schema.service.ts index 7593e6ec7..aba8788a5 100644 --- a/src/app/core/services/kafka/schema.service.ts +++ b/src/app/core/services/kafka/schema.service.ts @@ -13,7 +13,7 @@ export class SchemaService { constructor( private converterFactory: ConverterFactoryService, private subjectConfig: SubjectConfigService - ) {} + ) { } getKafkaObjectKey() { return this.subjectConfig @@ -59,4 +59,8 @@ export class SchemaService { }) ) } + + reset() { + return this.converterFactory.reset() + } } diff --git a/src/app/core/services/storage/storage.service.ts b/src/app/core/services/storage/storage.service.ts index 5df193514..16ef64fc5 100755 --- a/src/app/core/services/storage/storage.service.ts +++ b/src/app/core/services/storage/storage.service.ts @@ -21,14 +21,14 @@ export class StorageService { ) { this.keyUpdates = new Subject() this.platform.ready().then(() => { - this.prepare().then(() => + this.getStorageState().then(() => this.logger.log('Global configuration', this.global) ) }) } - getStorageState() { - return this.storage.ready() + getStorageState(): Promise { + return this.storage.ready().then(() => this.prepare()) } set(key: StorageKeys, value: any): Promise { @@ -107,8 +107,8 @@ export class StorageService { const errMsg = error.message ? error.message : error.status - ? `${error.status} - ${error.statusText}` - : 'error' + ? `${error.status} - ${error.statusText}` + : 'error' return observableThrowError(errMsg) } } diff --git a/src/app/core/services/token/token.service.ts b/src/app/core/services/token/token.service.ts index 28d193ce5..fc98326e0 100644 --- a/src/app/core/services/token/token.service.ts +++ b/src/app/core/services/token/token.service.ts @@ -103,7 +103,12 @@ export class TokenService { .post(uri, refreshBody, { headers: headers }) .toPromise() }) - .then(res => this.setTokens(res).then(() => res)) + .then((res: any) => { + if (!res.iat) + res.iat = this.jwtHelper.decodeToken(res.access_token)['iat'] + this.setTokens(res) + return res + }) } refresh(): Promise { diff --git a/src/app/pages/auth/components/qr-form/qr-form.component.ts b/src/app/pages/auth/components/qr-form/qr-form.component.ts index bed8f5db4..b5b836a7a 100755 --- a/src/app/pages/auth/components/qr-form/qr-form.component.ts +++ b/src/app/pages/auth/components/qr-form/qr-form.component.ts @@ -15,9 +15,10 @@ export class QRFormComponent { @Output() data: EventEmitter = new EventEmitter() - constructor(private usage: UsageService) {} + constructor(private usage: UsageService) { } async scanQRHandler() { + this.loading = true document.querySelector('body').classList.add('scanner-active') // Check camera permission // This is just a simple example, check out the better checks below @@ -29,7 +30,6 @@ export class QRFormComponent { async result => { await listener.remove() this.data.emit(result.barcode.rawValue) - // Removes the class after the scan (workaround for the camera not closing) document.querySelector('body').classList.remove('scanner-active') } diff --git a/src/app/pages/auth/containers/enrolment-page.component.html b/src/app/pages/auth/containers/enrolment-page.component.html index 754473952..e99ede398 100755 --- a/src/app/pages/auth/containers/enrolment-page.component.html +++ b/src/app/pages/auth/containers/enrolment-page.component.html @@ -22,6 +22,11 @@ +
+
+ {{ outcomeStatus }} +
+
{{ 'BTN_START' | translate }} diff --git a/src/app/pages/auth/containers/enrolment-page.component.scss b/src/app/pages/auth/containers/enrolment-page.component.scss index f39694adc..0c172d9d0 100755 --- a/src/app/pages/auth/containers/enrolment-page.component.scss +++ b/src/app/pages/auth/containers/enrolment-page.component.scss @@ -55,6 +55,15 @@ text-align: center; } +.initial-status { + margin-top: 48px; + padding: auto; + flex: 1 0 100%; + width: 100%; + height: auto; + text-align: center; +} + #outcome { padding: 5px; background-color: var(--cl-danger-40); diff --git a/src/app/pages/auth/containers/enrolment-page.component.ts b/src/app/pages/auth/containers/enrolment-page.component.ts index f010e6161..4e278e9e4 100755 --- a/src/app/pages/auth/containers/enrolment-page.component.ts +++ b/src/app/pages/auth/containers/enrolment-page.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, ViewChild } from '@angular/core' +import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core' import { App } from '@capacitor/app' import { Browser } from '@capacitor/browser' import { Device } from '@capacitor/device' @@ -51,8 +51,10 @@ export class EnrolmentPageComponent { private localization: LocalizationService, private alertService: AlertService, private usage: UsageService, - private logger: LogService + private logger: LogService, + private cdr: ChangeDetectorRef ) { + this.initializeDeepLinking() this.init() } @@ -63,7 +65,6 @@ export class EnrolmentPageComponent { let lang = this.languagesSelectable.find(a => a.value == tag) this.language = lang ? lang : this.language this.localization.setLanguage(this.language) - this.initializeDeepLinking() } ionViewDidEnter() { @@ -91,18 +92,16 @@ export class EnrolmentPageComponent { // Attempt to slide to the next slide with a delay for stability setTimeout(() => { - this.slides.nativeElement.swiper - .slideTo(nextIndex, 500) - .then(() => { - // Disable sliding after moving to the next slide - this.slides.nativeElement.swiper.allowSlideNext = false - this.slides.nativeElement.swiper.allowSlidePrev = false - }) - .catch(error => { - console.warn('Slide transition failed:', error) - // Retry the slide transition if it fails - this.retrySlideTransition(nextIndex) - }) + try { + this.slides.nativeElement.swiper.slideTo(nextIndex, 500) + // Disable sliding after moving to the next slide + this.slides.nativeElement.swiper.allowSlideNext = false + this.slides.nativeElement.swiper.allowSlidePrev = false + } catch (error) { + console.warn('Slide transition failed:', error) + // Retry the slide transition if it fails + this.retrySlideTransition(nextIndex) + } }, 100) // Adjust delay as necessary } else { console.warn('Swiper instance not ready, retrying...') @@ -120,9 +119,8 @@ export class EnrolmentPageComponent { const slideIndex = this.findSlideIndexById(id) if (slideIndex !== -1) { this.slides.nativeElement.swiper.allowSlideNext = true - this.slides.nativeElement.swiper - .slideTo(slideIndex, 500) - .then(() => (this.slides.nativeElement.swiper.allowSlideNext = false)) + this.slides.nativeElement.swiper.slideTo(slideIndex, 500) + this.slides.nativeElement.swiper.allowSlideNext = false } } @@ -145,16 +143,10 @@ export class EnrolmentPageComponent { this.slides.nativeElement.swiper ) { this.slides.nativeElement.swiper.update() // Ensure swiper is updated - this.slides.nativeElement.swiper - .slideTo(targetIndex, 500) - .then(() => { - // Disable sliding after moving to the target slide - this.slides.nativeElement.swiper.allowSlideNext = false - this.slides.nativeElement.swiper.allowSlidePrev = false - }) - .catch(error => - console.warn('Retry failed for slide transition:', error) - ) + this.slides.nativeElement.swiper.slideTo(targetIndex, 500) + // Disable sliding after moving to the target slide + this.slides.nativeElement.swiper.allowSlideNext = false + this.slides.nativeElement.swiper.allowSlidePrev = false } } @@ -165,6 +157,7 @@ export class EnrolmentPageComponent { authenticate(authObj) { this.loading.next(true) + this.cdr.detectChanges() this.clearStatus() return this.auth .authenticate(authObj) @@ -173,14 +166,18 @@ export class EnrolmentPageComponent { }) .then(() => this.handleAuthenticationSuccess()) .catch(e => this.handleAuthenticationError(e)) - .then(() => this.loading.next(false)) + .then(() => { + this.loading.next(false) + this.cdr.detectChanges() + }) } handleAuthenticationSuccess() { return this.auth.initSubjectInformation().then(() => { this.usage.sendGeneralEvent(EnrolmentEventType.SUCCESS) this.removeSlideById('qr-registration-choice') - return this.removeSlideById('qr-registration') + this.removeSlideById('qr-registration') + return this.goToSlideById('privacy-policy') }) } @@ -193,11 +190,13 @@ export class EnrolmentPageComponent { this.logger.error('Failed to log in', e) this.showStatus() this.outcomeStatus = - e.error && e.error.message - ? e.error.message - : e.status - ? e.statusText + ' (' + e.status + ')' - : e + e.error && e.error.error_description + ? e.error.error_description + : e.error && e.error.message + ? e.error.message + : e.status + ? e.statusText + ' (' + e.status + ')' + : e this.usage.sendGeneralEvent( e.status == 409 ? EnrolmentEventType.ERROR : EnrolmentEventType.FAIL, false, @@ -212,7 +211,10 @@ export class EnrolmentPageComponent { } showStatus() { - setTimeout(() => (this.showOutcomeStatus = true), 500) + setTimeout(() => { + (this.showOutcomeStatus = true) + this.cdr.detectChanges() + }, 500) } navigateToSplash() { diff --git a/src/app/pages/home/containers/home-page.component.html b/src/app/pages/home/containers/home-page.component.html index 597a43879..58d7c9a73 100755 --- a/src/app/pages/home/containers/home-page.component.html +++ b/src/app/pages/home/containers/home-page.component.html @@ -75,7 +75,7 @@
isTaskCalendarTaskNameShown: Promise + isTaskInfoShown: Promise currentDate: number APP_CREDITS = '© RADAR-Base' @@ -112,6 +113,7 @@ export class HomePageComponent implements OnInit, OnDestroy { this.title = this.tasksService.getPlatformInstanceName() this.isTaskCalendarTaskNameShown = this.tasksService.getIsTaskCalendarTaskNameShown() + this.isTaskInfoShown = this.tasksService.getIsTaskInfoShown() this.onDemandIcon = this.tasksService.getOnDemandAssessmentIcon() this.showMiscTasksButton = this.getShowMiscTasksButton() } diff --git a/src/app/pages/home/services/tasks.service.ts b/src/app/pages/home/services/tasks.service.ts index 644b9645b..f3f13e9a8 100644 --- a/src/app/pages/home/services/tasks.service.ts +++ b/src/app/pages/home/services/tasks.service.ts @@ -5,7 +5,8 @@ import { DefaultAppCreditsTitle, DefaultOnDemandAssessmentIcon, DefaultPlatformInstance, - DefaultShowTaskCalendarName + DefaultShowTaskCalendarName, + DefaultShowTaskInfo } from '../../../../assets/data/defaultConfig' import { QuestionnaireService } from '../../../core/services/config/questionnaire.service' import { RemoteConfigService } from '../../../core/services/config/remote-config.service' @@ -65,7 +66,7 @@ export class TasksService { .getTasksForDate(new Date(), AssessmentType.SCHEDULED) .then(tasks => tasks.filter( - t => !this.isTaskExpired(t) || this.wasTaskCompletedToday(t) + t => !this.isTaskExpired(t) || this.wasTaskCompletedToday(t) || this.wasTaskValidToday(t) ) ) } @@ -127,6 +128,10 @@ export class TasksService { return task.completed && this.isToday(task.timeCompleted) } + wasTaskValidToday(task) { + return this.isToday(task.timestamp) + } + /** * This function Retrieves the most current next task from a list of tasks. * @param tasks : list of tasks to retrieve the next task from. @@ -197,4 +202,16 @@ export class TasksService { ) .then(res => JSON.parse(res)) } + + getIsTaskInfoShown() { + return this.remoteConfig + .read() + .then(config => + config.getOrDefault( + ConfigKeys.SHOW_TASK_INFO, + DefaultShowTaskInfo + ) + ) + .then(res => JSON.parse(res)) + } } diff --git a/src/app/pages/questions/components/question/matrix-radio-input/matrix-radio-input.component.html b/src/app/pages/questions/components/question/matrix-radio-input/matrix-radio-input.component.html index ebf425530..265868034 100755 --- a/src/app/pages/questions/components/question/matrix-radio-input/matrix-radio-input.component.html +++ b/src/app/pages/questions/components/question/matrix-radio-input/matrix-radio-input.component.html @@ -1,4 +1,4 @@ - + @@ -8,7 +8,6 @@ diff --git a/src/app/pages/questions/components/question/matrix-radio-input/matrix-radio-input.component.ts b/src/app/pages/questions/components/question/matrix-radio-input/matrix-radio-input.component.ts index 9d682c580..3453ed8ef 100755 --- a/src/app/pages/questions/components/question/matrix-radio-input/matrix-radio-input.component.ts +++ b/src/app/pages/questions/components/question/matrix-radio-input/matrix-radio-input.component.ts @@ -48,6 +48,6 @@ export class MatrixRadioInputComponent implements OnInit, OnChanges { } onInputChange(event) { - this.valueChange.emit(event) + this.valueChange.emit(event.detail.value) } } diff --git a/src/app/pages/questions/components/question/question.component.ts b/src/app/pages/questions/components/question/question.component.ts index 505151fdf..816343d26 100755 --- a/src/app/pages/questions/components/question/question.component.ts +++ b/src/app/pages/questions/components/question/question.component.ts @@ -41,9 +41,10 @@ export class QuestionComponent implements OnInit, OnChanges { task: Task @Input() isSectionHeaderHidden: boolean - // isNextAutomatic: automatically slide to next upon answer @Input() - isNextAutomatic: boolean + isNextAutomatic: boolean // Automatically slide to next upon answer + @Input() + isMatrix = false @Output() answer: EventEmitter = new EventEmitter() @Output() @@ -59,7 +60,6 @@ export class QuestionComponent implements OnInit, OnChanges { keyboardScrollPadding = 200 keyboardInputOffset = 0 inputHeight = 0 - isMatrix = false isAutoHeight = false showScrollButton = false defaultYesNoResponse: Response[] = [ @@ -80,10 +80,6 @@ export class QuestionComponent implements OnInit, OnChanges { QuestionType.audio, QuestionType.descriptive ]) - MATRIX_INPUT_SET: Set = new Set([ - QuestionType.matrix_radio, - QuestionType.health - ]) // Input set where height is set to auto AUTO_HEIGHT_INPUT_SET: Set = new Set([ @@ -111,7 +107,6 @@ export class QuestionComponent implements OnInit, OnChanges { this.isFieldLabelHidden = this.HIDE_FIELD_LABEL_SET.has( this.question.field_type ) - this.isMatrix = this.MATRIX_INPUT_SET.has(this.question.field_type) this.isAutoHeight = this.isMatrix || this.AUTO_HEIGHT_INPUT_SET.has(this.question.field_type) setTimeout(() => { @@ -205,7 +200,7 @@ export class QuestionComponent implements OnInit, OnChanges { return ( this.SCROLLBAR_VISIBLE_SET.has(this.question.field_type) && this.inputEl.nativeElement.scrollHeight > - this.inputEl.nativeElement.clientHeight + this.inputEl.nativeElement.clientHeight ) } @@ -215,7 +210,7 @@ export class QuestionComponent implements OnInit, OnChanges { this.showScrollButton && event && event.target.scrollTop >= - (event.target.scrollHeight - event.target.clientHeight) * 0.1 + (event.target.scrollHeight - event.target.clientHeight) * 0.1 ) { this.showScrollButton = false } diff --git a/src/app/pages/questions/components/question/text-input/text-input.component.ts b/src/app/pages/questions/components/question/text-input/text-input.component.ts index 278a20706..481c13751 100644 --- a/src/app/pages/questions/components/question/text-input/text-input.component.ts +++ b/src/app/pages/questions/components/question/text-input/text-input.component.ts @@ -53,13 +53,13 @@ export class TextInputComponent implements OnInit { ampm: 'AM/PM' } textValue = '' - value = {} + DEFAULT_DATE_FORMAT = 'DD/MM/YYYY' constructor( private localization: LocalizationService, public modalCtrl: ModalController - ) {} + ) { } ngOnInit() { if (this.type.length) { @@ -80,36 +80,33 @@ export class TextInputComponent implements OnInit { } initDates() { - const moment = this.localization.moment(Date.now()) - const locale = moment.localeData() - const formatL = locale.longDateFormat('L') + const momentInstance = this.localization.moment(Date.now()) // Use a local instance this.datePickerObj = { - // User the user's locale format as output format - dateFormat: formatL, + dateFormat: this.DEFAULT_DATE_FORMAT, btnProperties: { - expand: 'block', // Default 'block' - fill: 'outline', // Default 'solid' - size: 'small', // Default 'default' - disabled: '', // Default false - strong: 'true', // Default false - color: 'secondary' // Default '' + expand: 'block', + fill: 'outline', + size: 'small', + disabled: '', + strong: 'true', + color: 'secondary' }, closeOnSelect: 'true' } - const month = locale.monthsShort() + const month = moment.monthsShort() const day = this.addLeadingZero(Array.from(Array(32).keys()).slice(1, 32)) const year = Array.from(Array(31).keys()).map(d => String(d + 2000)) this.datePickerValues = { day, month, year } this.defaultDatePickerValue = { - day: moment.format('DD'), - month: moment.format('MMM'), - year: moment.format('YYYY') + day: momentInstance.format('DD'), + month: momentInstance.format('MMM'), + year: momentInstance.format('YYYY') } this.emitAnswer(this.defaultDatePickerValue) } initTime() { - const moment = this.localization.moment(Date.now()) + const momentInstance = this.localization.moment(Date.now()) const hour = this.addLeadingZero(Array.from(Array(13).keys()).slice(1, 13)) const minute = this.addLeadingZero(Array.from(Array(60).keys())) const second = minute @@ -117,10 +114,10 @@ export class TextInputComponent implements OnInit { this.timePickerValues = { hour, minute, ampm } if (this.showSeconds) this.timePickerValues = { hour, minute, second, ampm } this.defaultTimePickerValue = { - hour: moment.format('hh'), - minute: moment.format('mm'), - second: this.showSeconds ? moment.format('ss') : '00', - ampm: moment.format('A') + hour: momentInstance.format('hh'), + minute: momentInstance.format('mm'), + second: this.showSeconds ? momentInstance.format('ss') : '00', + ampm: momentInstance.format('A') } } @@ -136,7 +133,7 @@ export class TextInputComponent implements OnInit { } datePickerObj: any = {} - selectedDate: string = this.localization.moment(Date.now()).format('L') + selectedDate: string = this.localization.moment(Date.now()).format(this.DEFAULT_DATE_FORMAT) async openDatePicker() { const datePickerModal = await this.modalCtrl.create({ @@ -150,11 +147,9 @@ export class TextInputComponent implements OnInit { await datePickerModal.present() datePickerModal.onDidDismiss().then(data => { - let date = moment(data.data.date) - date = date.isValid() ? date : this.localization.moment(this.selectedDate) - this.selectedDate = date.format('L') + let date = moment(data.data.date, this.DEFAULT_DATE_FORMAT) + this.selectedDate = date.isValid() ? date.format(this.DEFAULT_DATE_FORMAT) : this.selectedDate - // Transfer local date format all to US format to easily parse the data this.defaultDatePickerValue = { year: date.format('YYYY'), month: date.format('M'), diff --git a/src/app/pages/questions/containers/questions-page.component.html b/src/app/pages/questions/containers/questions-page.component.html index 29bc87966..2637e1694 100755 --- a/src/app/pages/questions/containers/questions-page.component.html +++ b/src/app/pages/questions/containers/questions-page.component.html @@ -31,6 +31,7 @@ [task]="task" [currentIndex]="currentQuestionGroupId" [isSectionHeaderHidden]="j != currentQuestionIndices[0]" + [isMatrix]="item.value.length > 1" (answer)="onAnswer($event)" (nextAction)="nextAction($event)" > diff --git a/src/app/pages/questions/containers/questions-page.component.ts b/src/app/pages/questions/containers/questions-page.component.ts index 601ee311f..394c57e25 100644 --- a/src/app/pages/questions/containers/questions-page.component.ts +++ b/src/app/pages/questions/containers/questions-page.component.ts @@ -70,6 +70,13 @@ export class QuestionsPageComponent implements OnInit { ]) MATRIX_FIELD_NAME = 'matrix' HEALTH_FIELD_NAME = 'health' + MATRIX_INPUT_SET: Set = new Set([ + QuestionType.matrix_radio, + QuestionType.healthkit, + QuestionType.slider, + QuestionType.yesno + ]) + backButtonListener: Subscription showProgressCount: Promise @@ -151,8 +158,7 @@ export class QuestionsPageComponent implements OnInit { const groupedQuestions = new Map() questions.forEach(q => { const key = - q.field_type.includes(this.MATRIX_FIELD_NAME) || - q.field_type.includes(this.HEALTH_FIELD_NAME) + this.MATRIX_INPUT_SET.has(q.field_type) && q.matrix_group_name ? q.matrix_group_name : q.field_name const entry = groupedQuestions.get(key) ? groupedQuestions.get(key) : [] @@ -281,7 +287,7 @@ export class QuestionsPageComponent implements OnInit { const currentQs = this.getCurrentQuestions() if (!currentQs) return this.isRightButtonDisabled = - !this.questionsService.isAnyAnswered(currentQs) && + !this.questionsService.areAllAnswered(currentQs) && !this.questionsService.getIsAnyNextEnabled(currentQs) this.isLeftButtonDisabled = this.questionsService.getIsAnyPreviousEnabled(currentQs) diff --git a/src/app/pages/questions/services/questions.service.ts b/src/app/pages/questions/services/questions.service.ts index 9b5c3d4a2..07d128427 100644 --- a/src/app/pages/questions/services/questions.service.ts +++ b/src/app/pages/questions/services/questions.service.ts @@ -158,8 +158,8 @@ export class QuestionsService { return this.answerService.check(id) } - isAnyAnswered(questions: Question[]) { - return questions.some(q => this.isAnswered(q)) + areAllAnswered(questions: Question[]) { + return questions.every(q => this.isAnswered(q)) } getNextQuestion(groupedQuestions, currentQuestionId): QuestionPosition { diff --git a/src/app/shared/enums/config.ts b/src/app/shared/enums/config.ts index 8e61a578b..7ef9c5209 100644 --- a/src/app/shared/enums/config.ts +++ b/src/app/shared/enums/config.ts @@ -39,8 +39,8 @@ export class ConfigKeys { static TOPIC_CACHE_TIMEOUT = new ConfigKeys('topic_cache_timeout') static SHOW_TASK_CALENDAR_NAME = new ConfigKeys('show_task_calendar_name') - static SHOW_TASK_PROGRESS_COUNT = new ConfigKeys('show_task_progress_count') + static SHOW_TASK_INFO = new ConfigKeys('show_task_info') static AUDIO_SAMPLING_RATE = new ConfigKeys('audio_sampling_rate') diff --git a/src/app/shared/models/question.ts b/src/app/shared/models/question.ts index 30f4227fd..830d69a60 100755 --- a/src/app/shared/models/question.ts +++ b/src/app/shared/models/question.ts @@ -59,6 +59,7 @@ export class QuestionType { static descriptive = 'descriptive' static matrix_radio = 'matrix-radio' static health = 'health' + static healthkit = 'healthkit' } export interface Response { @@ -98,4 +99,4 @@ export interface QuestionPosition { export enum WebInputType { NHS = 'nhs' -} \ No newline at end of file +} diff --git a/src/assets/data/defaultConfig.ts b/src/assets/data/defaultConfig.ts index c21b28d50..3fa082972 100755 --- a/src/assets/data/defaultConfig.ts +++ b/src/assets/data/defaultConfig.ts @@ -63,8 +63,8 @@ export const DefaultAppCreditsBody = JSON.stringify( ) export const DefaultShowTaskCalendarName = 'false' - export const DefaultShowTaskProgressCount = 'false' +export const DefaultShowTaskInfo = 'true' // DEFAULT URI