From 85e6446f4b086d43a5f2ddaac99a9f809e2fb8a7 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 15 Feb 2024 15:32:40 +0100 Subject: [PATCH] MOBILE-2768 policy: Support accepting all types of policies --- scripts/langindex.json | 7 + src/core/classes/sites/authenticated-site.ts | 2 +- .../input-errors/core-input-errors.html | 5 +- src/core/features/login/login.module.ts | 4 - .../policy/components/components.module.ts | 31 ++ .../components/policy-modal/policy-modal.html | 24 ++ .../components/policy-modal/policy-modal.ts | 37 ++ src/core/features/policy/lang.json | 9 +- .../policy/pages/site-policy/site-policy.html | 113 +++++- .../policy/pages/site-policy/site-policy.scss | 40 +- .../policy/pages/site-policy/site-policy.ts | 358 ++++++++++++++++-- .../features/policy/policy-lazy.module.ts | 2 + src/core/features/policy/policy.module.ts | 15 +- src/core/features/policy/services/policy.ts | 42 +- 14 files changed, 628 insertions(+), 61 deletions(-) create mode 100644 src/core/features/policy/components/components.module.ts create mode 100644 src/core/features/policy/components/policy-modal/policy-modal.html create mode 100644 src/core/features/policy/components/policy-modal/policy-modal.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index f0697bec7..c43f2fe20 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2282,12 +2282,19 @@ "core.phone": "moodle", "core.pictureof": "moodle", "core.play": "local_moodlemobileapp", + "core.policy.agreepolicies": "tool_policy", + "core.policy.backtotop": "tool_policy", + "core.policy.consentpagetitle": "tool_policy", "core.policy.havereadandagreepolicy": "local_moodlemobileapp", + "core.policy.idontagree": "tool_policy", + "core.policy.mustagreetocontinue": "tool_policy", "core.policy.policyacceptmandatory": "local_moodlemobileapp", "core.policy.policyagree": "moodle", "core.policy.policyagreement": "moodle", "core.policy.policyagreementclick": "moodle", + "core.policy.refertofullpolicytext": "tool_policy", "core.policy.sitepolicynotagreederror": "local_moodlemobileapp", + "core.policy.steppolicies": "tool_policy", "core.previous": "moodle", "core.proceed": "moodle", "core.publicprofile": "moodle", diff --git a/src/core/classes/sites/authenticated-site.ts b/src/core/classes/sites/authenticated-site.ts index 7db3ad6a9..7769d479f 100644 --- a/src/core/classes/sites/authenticated-site.ts +++ b/src/core/classes/sites/authenticated-site.ts @@ -671,7 +671,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { this.triggerSiteEvent(CoreEvents.SITE_POLICY_NOT_AGREED, {}); error.message = Translate.instant('core.policy.sitepolicynotagreederror'); - throw new CoreWSError(error); + throw new CoreSilentError(error); } else if (error.errorcode === 'dmlwriteexception' && CoreTextUtils.hasUnicodeData(data)) { if (!this.cleanUnicode) { // Try again cleaning unicode. diff --git a/src/core/components/input-errors/core-input-errors.html b/src/core/components/input-errors/core-input-errors.html index 89de85000..2f7da442d 100644 --- a/src/core/components/input-errors/core-input-errors.html +++ b/src/core/components/input-errors/core-input-errors.html @@ -2,7 +2,10 @@
- {{ errorMessages[error] | translate }} + + {{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }} diff --git a/src/core/features/login/login.module.ts b/src/core/features/login/login.module.ts index 4a1e6244d..7d15b2175 100644 --- a/src/core/features/login/login.module.ts +++ b/src/core/features/login/login.module.ts @@ -62,10 +62,6 @@ const appRoutes: Routes = [ CoreLoginHelper.passwordChangeForced(data.siteId); }); - CoreEvents.on(CoreEvents.SITE_POLICY_NOT_AGREED, (data) => { - CoreLoginHelper.sitePolicyNotAgreed(data.siteId); - }); - await CoreLoginHelper.initialize(); }, }, diff --git a/src/core/features/policy/components/components.module.ts b/src/core/features/policy/components/components.module.ts new file mode 100644 index 000000000..21c3b118b --- /dev/null +++ b/src/core/features/policy/components/components.module.ts @@ -0,0 +1,31 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { CorePolicyViewPolicyModalComponent } from './policy-modal/policy-modal'; + +@NgModule({ + declarations: [ + CorePolicyViewPolicyModalComponent, + ], + imports: [ + CoreSharedModule, + ], + exports: [ + CorePolicyViewPolicyModalComponent, + ], +}) +export class CorePolicyComponentsModule {} diff --git a/src/core/features/policy/components/policy-modal/policy-modal.html b/src/core/features/policy/components/policy-modal/policy-modal.html new file mode 100644 index 000000000..b555e7f34 --- /dev/null +++ b/src/core/features/policy/components/policy-modal/policy-modal.html @@ -0,0 +1,24 @@ + + + +

{{ policy.name }}

+
+ + + + +
+
+ + + + + + + + + + + + diff --git a/src/core/features/policy/components/policy-modal/policy-modal.ts b/src/core/features/policy/components/policy-modal/policy-modal.ts new file mode 100644 index 000000000..d9f9a6da2 --- /dev/null +++ b/src/core/features/policy/components/policy-modal/policy-modal.ts @@ -0,0 +1,37 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Input } from '@angular/core'; +import { CorePolicySitePolicy } from '@features/policy/services/policy'; +import { ModalController } from '@singletons'; + +/** + * Modal to view a policy. + */ +@Component({ + selector: 'core-policy-view-policy-modal', + templateUrl: 'policy-modal.html', +}) +export class CorePolicyViewPolicyModalComponent { + + @Input() policy?: CorePolicySitePolicy; + + /** + * Close modal. + */ + closeModal(): void { + ModalController.dismiss(); + } + +} diff --git a/src/core/features/policy/lang.json b/src/core/features/policy/lang.json index 12b54abe9..fd53e1f17 100644 --- a/src/core/features/policy/lang.json +++ b/src/core/features/policy/lang.json @@ -1,8 +1,15 @@ { + "agreepolicies": "Please agree to the following policies", + "backtotop": "Back to top", + "consentpagetitle": "Consent", "havereadandagreepolicy": "I have read and agree to the {{policyname}}", + "idontagree": "No thanks, I decline {{$a}}", + "mustagreetocontinue": "Before continuing you need to acknowledge all these policies.", "policyacceptmandatory": "I understand and agree to the mandatory site policies", "policyagree": "You must agree to this policy to continue using this site. Do you agree?", "policyagreement": "Site policy agreement", "policyagreementclick": "Link to site policy agreement", - "sitepolicynotagreederror": "Site policy not agreed." + "refertofullpolicytext": "Please refer to the full {{$a}} if you would like to review the text.", + "sitepolicynotagreederror": "Site policy not agreed.", + "steppolicies": "Policy {{$a.numpolicy}} out of {{$a.totalpolicies}}" } diff --git a/src/core/features/policy/pages/site-policy/site-policy.html b/src/core/features/policy/pages/site-policy/site-policy.html index 160a2b81b..8054a867e 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.html +++ b/src/core/features/policy/pages/site-policy/site-policy.html @@ -17,13 +17,22 @@ -
- + + -

{{ 'core.policy.policyagreement' | translate }}

+

{{ 'core.policy.steppolicies' | translate:{ $a:stepData } }}

+
+
+ + + +

{{ title }}

+

{{ subTitle }}


+ + @@ -48,18 +57,104 @@
- +

{{ 'core.policy.havereadandagreepolicy' | translate:{ policyname:'core.policy.policyagreement' | translate } }} - - +

+

{{ 'core.policy.policyacceptmandatory' | translate }} - +

- +
+ + + + + + + + + + + + + + + + + + + + +

{{ policy.name }}

+
+ +
+
+
+ + +

+
+
+ + +
+ + + +

{{ 'core.policy.steppolicies' | translate:{ $a:stepData } }}

+
+ + +
+ + + + + + +
+ {{ 'core.continue' | translate }} - + + {{ 'core.next' | translate }} + +
+ + + + +

+ {{ 'core.policy.havereadandagreepolicy' | translate:{ policyname:policy.name } }} +

+
+ +
+ + + +

{{ 'core.policy.havereadandagreepolicy' | translate:{ policyname:policy.name } }}

+
+
+ + +

{{ 'core.policy.idontagree' | translate:{ $a:policy.name } }}

+
+ +
+
+
diff --git a/src/core/features/policy/pages/site-policy/site-policy.scss b/src/core/features/policy/pages/site-policy/site-policy.scss index 7192dca7b..9d5a17fa8 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.scss +++ b/src/core/features/policy/pages/site-policy/site-policy.scss @@ -6,6 +6,10 @@ margin: 0 16px; } + h2 { + font-size: 1.25rem; + } + form { display: flex; flex-direction: column; @@ -20,6 +24,17 @@ --inner-border-width: 0; } + .core-site-policy-top-bar, .core-site-policy-bottom-bar { + font-size: 0.875rem; + } + + .core-site-policy-top-bar ion-label { + margin-bottom: 0; + } + .core-site-policy-bottom-bar ion-label { + margin-top: 0; + } + .core-site-policy-link { p { text-decoration: underline; @@ -38,8 +53,29 @@ flex-grow: 1; } - ion-button[type="submit"] { - margin-bottom: 12px;; + .core-site-policy-content ion-label { + margin-top: 0; } + + .core-site-policy-go-top-button { + --border-radius: 50%; + --box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12); + margin-bottom: 8px; + } + + .core-site-policy-buttons { + text-align: center; + + ion-button { + margin-left: 0; + margin-right: 0; + margin-bottom: 12px; + } + } + + .item ::ng-deep ion-label p span { + color: var(--core-link-color); + } + } } diff --git a/src/core/features/policy/pages/site-policy/site-policy.ts b/src/core/features/policy/pages/site-policy/site-policy.ts index 55c057238..afe259d68 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.ts +++ b/src/core/features/policy/pages/site-policy/site-policy.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { CoreSites } from '@services/sites'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; @@ -23,8 +23,14 @@ import { CoreNavigator } from '@services/navigator'; import { CoreEvents } from '@singletons/events'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; -import { CorePolicy } from '@features/policy/services/policy'; +import { CorePolicy, CorePolicyAgreementStyle, CorePolicySitePolicy } from '@features/policy/services/policy'; import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { CoreUrlUtils } from '@services/utils/url'; +import { IonContent } from '@ionic/angular'; +import { CoreScreen } from '@services/screen'; +import { Subscription } from 'rxjs'; +import { CoreDom } from '@singletons/dom'; +import { CorePolicyViewPolicyModalComponent } from '@features/policy/components/policy-modal/policy-modal'; /** * Page to accept a site policy. @@ -34,18 +40,38 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; templateUrl: 'site-policy.html', styleUrls: ['site-policy.scss'], }) -export class CorePolicySitePolicyPage implements OnInit { +export class CorePolicySitePolicyPage implements OnInit, OnDestroy { + + @ViewChild(IonContent) content?: IonContent; siteName?: string; isManageAcceptancesAvailable = false; + policyLoaded?: boolean; + policiesForm?: FormGroup; isPoliciesURL = false; + title = ''; + subTitle?: string; + hasScroll = false; + isTablet = false; + + // Variables for accepting policies using a URL. sitePoliciesURL?: string; showInline?: boolean; - policyLoaded?: boolean; - policyForm?: FormGroup; + + // Variables for accepting policies one by one. + currentPolicy?: SitePolicy; + pendingPolicies?: SitePolicy[]; + agreeInOwnPage = false; + numPolicy = 1; + showConsentForm = false; + stepData?: {numpolicy: number; totalpolicies: number}; + policiesErrors = { required: Translate.instant('core.policy.mustagreetocontinue') }; protected siteId?: string; protected currentSite!: CoreSite; + protected layoutSubscription?: Subscription; + + constructor(protected elementRef: ElementRef, protected changeDetector: ChangeDetectorRef) {} /** * @inheritdoc @@ -73,26 +99,23 @@ export class CorePolicySitePolicyPage implements OnInit { return; } + this.isTablet = CoreScreen.isTablet; + this.layoutSubscription = CoreScreen.layoutObservable.subscribe(() => { + this.isTablet = CoreScreen.isTablet; + }); + this.isManageAcceptancesAvailable = await CorePolicy.isManageAcceptancesAvailable(this.siteId); this.isPoliciesURL = this.isManageAcceptancesAvailable ? (await this.currentSite.getConfig('sitepolicyhandler')) !== 'tool_policy' : true; // Site doesn't support managing acceptances, just display it as a URL. if (this.isPoliciesURL) { - this.initFormForPoliciesURL(); - await this.fetchSitePoliciesURL(); - } else { - // TODO - } - CoreAnalytics.logEvent({ - type: CoreAnalyticsEventType.VIEW_ITEM, - ws: 'auth_email_get_signup_settings', - name: Translate.instant('core.policy.policyagreement'), - data: { category: 'policy' }, - url: '/user/policy.php', - }); + this.initFormForPoliciesURL(); + } else { + await this.fetchNextPoliciesToAccept(); + } } /** @@ -101,6 +124,9 @@ export class CorePolicySitePolicyPage implements OnInit { * @returns Promise resolved when done. */ protected async fetchSitePoliciesURL(): Promise { + this.title = Translate.instant('core.policy.policyagreement'); + this.subTitle = undefined; + try { this.sitePoliciesURL = await CorePolicy.getSitePoliciesURL(this.siteId); } catch (error) { @@ -122,13 +148,103 @@ export class CorePolicySitePolicyPage implements OnInit { } finally { this.policyLoaded = true; } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'auth_email_get_signup_settings', + name: Translate.instant('core.policy.policyagreement'), + data: { category: 'policy' }, + url: '/user/policy.php', + }); + } + + /** + * Fetch the next site policies to accept. + * + * @returns Promise resolved when done. + */ + protected async fetchNextPoliciesToAccept(): Promise { + try { + this.scrollTop(); + + const pendingPolicies = await CorePolicy.getNextPendingPolicies({ + readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, + siteId: this.siteId, + }); + + // Add some calculated data. + this.pendingPolicies = pendingPolicies.map((policy: SitePolicy) => { + policy.referToFullPolicyText = Translate.instant('core.policy.refertofullpolicytext', { + $a: `${policy.name}`, + }); + + return policy; + }); + + const policy = this.pendingPolicies[0]; + if (!policy) { + // No more policies to accept. + await this.finishAcceptingPolicies(); + + return; + } + + this.initFormForPendingPolicies(); + + this.agreeInOwnPage = policy.agreementstyle === CorePolicyAgreementStyle.OwnPage; + this.showConsentForm = false; + this.numPolicy = 1; + this.setCurrentPolicy(policy); + this.policyLoaded = true; + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error getting site policy.'); + this.cancel(); + } + } + + /** + * Log in analytics viewing a certain policy. + */ + protected logAnalyticsPolicyView(): void { + if (!this.currentPolicy) { + return; + } + + const analyticsParams: Record = { + versionid: this.currentPolicy.versionid, + }; + if (!this.agreeInOwnPage) { + analyticsParams.numpolicy = this.numPolicy; + analyticsParams.totalpolicies = this.pendingPolicies?.length ?? this.numPolicy; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'tool_policy_get_user_acceptances', + name: this.currentPolicy.name, + data: analyticsParams, + url: CoreUrlUtils.addParamsToUrl('/admin/tool/policy/view.php', analyticsParams), + }); + } + + /** + * Log in analytics viewing the consent form. + */ + protected logAnalyticsConsentFormView(): void { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'tool_policy_get_user_acceptances', + name: Translate.instant('core.policy.consentpagetitle'), + data: {}, + url: CoreUrlUtils.addParamsToUrl('/admin/tool/policy/index.php'), + }); } /** * Init the form to accept the policies using a URL. */ protected initFormForPoliciesURL(): void { - this.policyForm = new FormGroup({ + this.policiesForm = new FormGroup({ agreepolicy: new FormControl(false, { validators: Validators.requiredTrue, nonNullable: true, @@ -136,6 +252,26 @@ export class CorePolicySitePolicyPage implements OnInit { }); } + /** + * Init the form to accept the current pending policies. + */ + protected initFormForPendingPolicies(): void { + this.policiesForm = new FormGroup({}); + + this.pendingPolicies?.forEach(policy => { + if (policy.optional) { + this.policiesForm?.addControl('agreepolicy' + policy.versionid, new FormControl(undefined, { + validators: Validators.required, + })); + } else { + this.policiesForm?.addControl('agreepolicy' + policy.versionid, new FormControl(false, { + validators: Validators.requiredTrue, + nonNullable: true, + })); + } + }); + } + /** * Cancel. * @@ -147,6 +283,68 @@ export class CorePolicySitePolicyPage implements OnInit { await CoreNavigator.navigate('/login/sites', { reset: true }); } + /** + * Load next policy. + * + * @param event Event. + */ + nextPolicy(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + + if (!this.pendingPolicies) { + return; + } + + this.scrollTop(); + + if (this.numPolicy < this.pendingPolicies.length) { + this.numPolicy++; + this.setCurrentPolicy(this.pendingPolicies[this.numPolicy - 1]); + + return; + } + + // All policies seen, display the consent form. + this.currentPolicy = undefined; + this.stepData = undefined; + this.showConsentForm = true; + this.title = Translate.instant('core.policy.consentpagetitle'); + this.subTitle = Translate.instant('core.policy.agreepolicies'); + + this.logAnalyticsConsentFormView(); + } + + /** + * Set current policy. + */ + protected setCurrentPolicy(policy?: CorePolicySitePolicy): void { + if (!policy) { + return; + } + + this.hasScroll = false; + this.currentPolicy = policy; + this.title = policy.name || ''; + this.subTitle = undefined; + this.stepData = !this.agreeInOwnPage ? + { numpolicy: this.numPolicy, totalpolicies: this.pendingPolicies?.length ?? this.numPolicy } : + undefined; + + this.logAnalyticsPolicyView(); + } + + /** + * Check if the content has scroll. + */ + protected async checkScroll(): Promise { + await CoreUtils.wait(400); + + const scrollElement = await this.content?.getScrollElement(); + + this.hasScroll = !!scrollElement && scrollElement.scrollHeight > scrollElement.clientHeight; + } + /** * Submit the acceptances to one or several policies. * @@ -157,27 +355,131 @@ export class CorePolicySitePolicyPage implements OnInit { event.preventDefault(); event.stopPropagation(); - if (!this.policyForm?.valid) { + if (!this.policiesForm?.valid) { + for (const name in this.policiesForm?.controls) { + this.policiesForm.controls[name].markAsDirty(); + } + this.changeDetector.detectChanges(); + + // Scroll to the first element with errors. + const errorFound = await CoreDom.scrollToInputError( + this.elementRef.nativeElement, + ); + + if (!errorFound) { + // Input not found, show an error modal. + CoreDomUtils.showErrorModal('core.policy.mustagreetocontinue', true); + } + return; } const modal = await CoreDomUtils.showModalLoading('core.sending', true); try { + if (!this.isPoliciesURL) { + await this.acceptPendingPolicies(); + + return; + } + await CorePolicy.acceptMandatorySitePolicies(this.siteId); - // Success accepting, go to site initial page. - // Invalidate cache since some WS don't return error if site policy is not accepted. - await CoreUtils.ignoreErrors(this.currentSite.invalidateWsCache()); - - CoreEvents.trigger(CoreEvents.SITE_POLICY_AGREED, {}, this.siteId); - - await CoreNavigator.navigateToSiteHome(); + await this.finishAcceptingPolicies(); } catch (error) { - CoreDomUtils.showErrorModalDefault(error, 'Error accepting site policy.'); + CoreDomUtils.showErrorModalDefault(error, 'Error accepting site policies.'); } finally { modal.dismiss(); } } + /** + * Accept current pending policies. + */ + protected async acceptPendingPolicies(): Promise { + if (!this.pendingPolicies) { + return; + } + + const acceptances: Record = {}; + + this.pendingPolicies?.forEach(policy => { + const control = this.policiesForm?.controls['agreepolicy' + policy.versionid]; + if (!control) { + return; + } + + if (policy.optional) { + if (control.value === null || control.value === undefined) { + // Not answered, this code shouldn't be reached. Display error. + CoreDomUtils.showErrorModal('core.policy.mustagreetocontinue', true); + + return; + } + + acceptances[policy.versionid] = control.value; + } else { + if (!control.value) { + // Not answered, this code shouldn't be reached. Display error. + CoreDomUtils.showErrorModal('core.policy.mustagreetocontinue', true); + + return; + } + + acceptances[policy.versionid] = 1; + } + }); + + await CorePolicy.setUserAcceptances(acceptances, this.siteId); + + await this.fetchNextPoliciesToAccept(); + } + + /** + * All mandatory policies have been accepted, go to site initial page. + */ + protected async finishAcceptingPolicies(): Promise { + // Invalidate cache since some WS don't return error if site policy is not accepted. + await CoreUtils.ignoreErrors(this.currentSite.invalidateWsCache()); + + CoreEvents.trigger(CoreEvents.SITE_POLICY_AGREED, {}, this.siteId); + + await CoreNavigator.navigateToSiteHome(); + } + + /** + * Scroll to top. + * + * @param event Event. + */ + scrollTop(event?: Event): void { + event?.preventDefault(); + event?.stopPropagation(); + + this.content?.scrollToTop(400); + } + + /** + * View the full policy. + * + * @param policy Policy. + */ + viewFullPolicy(policy: CorePolicySitePolicy): void { + CoreDomUtils.openModal({ + component: CorePolicyViewPolicyModalComponent, + componentProps: { policy }, + }); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.layoutSubscription?.unsubscribe(); + } + } + +type SitePolicy = CorePolicySitePolicy & { + referToFullPolicyText?: string; +}; diff --git a/src/core/features/policy/policy-lazy.module.ts b/src/core/features/policy/policy-lazy.module.ts index fd9bca426..eb49beae2 100644 --- a/src/core/features/policy/policy-lazy.module.ts +++ b/src/core/features/policy/policy-lazy.module.ts @@ -18,6 +18,7 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { CorePolicySitePolicyPage } from '@features/policy/pages/site-policy/site-policy'; import { SITE_POLICY_PAGE_NAME } from './constants'; +import { CorePolicyComponentsModule } from './components/components.module'; const routes: Routes = [ { @@ -30,6 +31,7 @@ const routes: Routes = [ imports: [ CoreSharedModule, RouterModule.forChild(routes), + CorePolicyComponentsModule, ], declarations: [ CorePolicySitePolicyPage, diff --git a/src/core/features/policy/policy.module.ts b/src/core/features/policy/policy.module.ts index b8c79184d..54ace343d 100644 --- a/src/core/features/policy/policy.module.ts +++ b/src/core/features/policy/policy.module.ts @@ -12,26 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { Routes } from '@angular/router'; import { AppRoutingModule } from '@/app/app-routing.module'; import { CoreEvents } from '@singletons/events'; import { POLICY_PAGE_NAME } from './constants'; -/** - * Get policy services. - * - * @returns Policy services. - */ -export async function getPolicyServices(): Promise[]> { - const { CorePolicyService } = await import('@features/policy/services/policy'); - - return [ - CorePolicyService, - ]; -} - const routes: Routes = [ { path: POLICY_PAGE_NAME, diff --git a/src/core/features/policy/services/policy.ts b/src/core/features/policy/services/policy.ts index d67b71aca..c5caa910e 100644 --- a/src/core/features/policy/services/policy.ts +++ b/src/core/features/policy/services/policy.ts @@ -89,6 +89,38 @@ export class CorePolicyService { return sitePolicy; } + /** + * Get the next policies to accept. + * + * @param options Options + * @returns Next pending policies + */ + async getNextPendingPolicies(options: CoreSitesCommonWSOptions = {}): Promise { + const policies = await this.getUserAcceptances(options); + + const pendingPolicies: CorePolicySitePolicy[] = []; + + for (const i in policies) { + const policy = policies[i]; + const hasAccepted = policy.acceptance?.status === 1; + const hasDeclined = policy.acceptance?.status === 0; + + if (hasAccepted || (hasDeclined && policy.optional === 1)) { + // Policy already answered, ignore. + continue; + } + + if (policy.agreementstyle === CorePolicyAgreementStyle.OwnPage) { + // Policy needs to be accepted on its own page, it's the next policy to accept. + return [policy]; + } + + pendingPolicies.push(policy); + } + + return pendingPolicies; + } + /** * Get user acceptances. * @@ -177,7 +209,7 @@ export class CorePolicyService { })), }; - const response = await site.write('tool_policy_get_user_acceptances', data); + const response = await site.write('tool_policy_set_acceptances_status', data); if (response.warnings?.length) { throw new CoreWSError(response.warnings[0]); } @@ -260,3 +292,11 @@ type CorePolicySetAcceptancesWSResponse = { policyagreed: number; // Whether the user has provided acceptance to all current site policies. 1 if yes, 0 if not. warnings?: CoreWSExternalWarning[]; }; + +/** + * Agreement style. + */ +export enum CorePolicyAgreementStyle { + ConsentPage = 0, // Policy to be accepted together with others on the consent page. + OwnPage = 1, // Policy to be accepted on its own page before reaching the consent page. +}