From 847359bb2a9f73c7be1dad11600e240b56a740b8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 29 Nov 2017 16:03:47 +0100 Subject: [PATCH] MOBILE-2253 login: Implement email signup without profile fields --- gulpfile.js | 8 +- src/app/app.scss | 7 + .../login/pages/credentials/credentials.html | 8 +- .../pages/email-signup/email-signup.html | 122 ++++++++ .../pages/email-signup/email-signup.module.ts | 35 +++ .../pages/email-signup/email-signup.scss | 2 + .../login/pages/email-signup/email-signup.ts | 268 ++++++++++++++++++ .../login/pages/site-error/site-error.html | 2 +- src/core/login/providers/helper.ts | 6 +- src/providers/utils/dom.ts | 27 +- src/providers/utils/utils.ts | 4 +- 11 files changed, 463 insertions(+), 26 deletions(-) create mode 100644 src/core/login/pages/email-signup/email-signup.html create mode 100644 src/core/login/pages/email-signup/email-signup.module.ts create mode 100644 src/core/login/pages/email-signup/email-signup.scss create mode 100644 src/core/login/pages/email-signup/email-signup.ts diff --git a/gulpfile.js b/gulpfile.js index 94ac0352c..edf9cfa8d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -84,11 +84,11 @@ function treatMergedData(data) { } addProperties(merged, data[filepath], 'mma.'+pluginName+'.'); - } else if (filepath.indexOf('core/assets/countries') === 0) { + } else if (filepath.indexOf('assets/countries') === 0) { addProperties(merged, data[filepath], 'mm.core.country-'); - } else if (filepath.indexOf('core/assets/mimetypes') === 0) { + } else if (filepath.indexOf('assets/mimetypes') === 0) { addProperties(merged, data[filepath], 'mm.core.mimetype-'); @@ -181,7 +181,9 @@ var appLangFiles = ['ar.json', 'bg.json', 'ca.json', 'cs.json', 'da.json', 'de.j lang: [ './src/lang/', './src/core/**/lang/', - './src/addons/**/lang/' + './src/addons/**/lang/', + './src/assets/countries/', + './src/assets/mimetypes/' ], config: './src/config.json', }; diff --git a/src/app/app.scss b/src/app/app.scss index 42b840fb9..34fa86dbd 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -112,3 +112,10 @@ ion-icon.icon-accessory { .mm-bold, .mm-bold .label { font-weight: bold; } + +.item .core-input-footnote { + width: 100%; + padding-top: 10px; + padding-bottom: 10px; + font-style: italic; +} diff --git a/src/core/login/pages/credentials/credentials.html b/src/core/login/pages/credentials/credentials.html index e8d899933..b52284bc4 100644 --- a/src/core/login/pages/credentials/credentials.html +++ b/src/core/login/pages/credentials/credentials.html @@ -22,14 +22,16 @@ - + + +
- +
@@ -44,7 +46,7 @@

{{ 'mm.login.firsttime' | translate }}

-

+

diff --git a/src/core/login/pages/email-signup/email-signup.html b/src/core/login/pages/email-signup/email-signup.html new file mode 100644 index 000000000..77eaa0385 --- /dev/null +++ b/src/core/login/pages/email-signup/email-signup.html @@ -0,0 +1,122 @@ + + + {{ 'mm.login.newaccount' | translate }} + + + + + + + + + + + + +
+ + + +

{{siteUrl}}

+ +

{{siteName}}

+

{{siteUrl}}

+
+ + + + {{ 'mm.login.createuserandpass' | translate }} + + + {{ 'mm.login.username' | translate }} + + + + + {{ 'mm.login.password' | translate }} + + + +

+ {{settings.passwordpolicy}} +

+ +
+ + + + {{ 'mm.login.supplyinfo' | translate }} + + + {{ 'mm.user.email' | translate }} + + + + + {{ 'mm.user.emailagain' | translate }} + + + + + {{ 'mm.user.' + nameField | translate }} + + + + + {{ 'mm.user.city' | translate }} + + + + {{ 'mm.user.country' | translate }} + + {{ 'mm.login.selectacountry' | translate }} + {{countries[key]}} + + + + + + + +
+ {{ 'mm.login.security_question' | translate }} + + {{ 'mm.login.recaptchachallengeimage' | translate }} + + + {{ 'mm.login.enterthewordsabove' | translate }} + + + + + + {{ 'mm.login.getanothercaptcha' | translate }} + +
+ + +
+ {{ 'mm.login.policyagreement' | translate }} + +

{{ 'mm.login.policyagreementclick' | translate }}

+
+ + {{ 'mm.login.policyaccept' | translate }} + + + +
+ + + + + +
+
+
+
diff --git a/src/core/login/pages/email-signup/email-signup.module.ts b/src/core/login/pages/email-signup/email-signup.module.ts new file mode 100644 index 000000000..37b1fd5b7 --- /dev/null +++ b/src/core/login/pages/email-signup/email-signup.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { CoreLoginEmailSignupPage } from './email-signup'; +import { CoreLoginModule } from '../../login.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; + +@NgModule({ + declarations: [ + CoreLoginEmailSignupPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CoreLoginModule, + IonicPageModule.forChild(CoreLoginEmailSignupPage), + TranslateModule.forChild() + ] +}) +export class CoreLoginCredentialsPageModule {} diff --git a/src/core/login/pages/email-signup/email-signup.scss b/src/core/login/pages/email-signup/email-signup.scss new file mode 100644 index 000000000..cf74cbcf4 --- /dev/null +++ b/src/core/login/pages/email-signup/email-signup.scss @@ -0,0 +1,2 @@ +page-core-login-email-signup { +} diff --git a/src/core/login/pages/email-signup/email-signup.ts b/src/core/login/pages/email-signup/email-signup.ts new file mode 100644 index 000000000..93cd7489e --- /dev/null +++ b/src/core/login/pages/email-signup/email-signup.ts @@ -0,0 +1,268 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, ViewChild } from '@angular/core'; +import { IonicPage, NavController, NavParams, Content } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreUtilsProvider } from '../../../../providers/utils/utils'; +import { CoreWSProvider } from '../../../../providers/ws'; +import { CoreLoginHelperProvider } from '../../providers/helper'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +/** + * Page to signup using email. + */ +@IonicPage() +@Component({ + selector: 'page-core-login-email-signup', + templateUrl: 'email-signup.html', +}) +export class CoreLoginEmailSignupPage { + @ViewChild(Content) content: Content; + signupForm: FormGroup; + siteUrl: string; + siteConfig: any; + siteName: string; + authInstructions: string; + settings: any; + countries: any; + countriesKeys: any[]; + categories: any[]; + settingsLoaded: boolean = false; + + // Validation errors. + usernameErrors: any; + passwordErrors: any; + emailErrors: any; + email2Errors: any; + policyErrors: any; + namefieldsErrors: any; + + constructor(private navCtrl: NavController, navParams: NavParams, private fb: FormBuilder, private wsProvider: CoreWSProvider, + private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider, + private textUtils: CoreTextUtilsProvider) { + + this.siteUrl = navParams.get('siteUrl'); + + // Create the signupForm with the basic controls. More controls will be added later. + this.signupForm = this.fb.group({ + 'username': ['', Validators.required], + 'password': ['', Validators.required], + 'email': ['', Validators.compose([Validators.required, Validators.email])], + 'email2': ['', Validators.compose([Validators.required, Validators.email])] + }); + + // Setup validation errors. + this.usernameErrors = this.loginHelper.getErrorMessages('mm.login.usernamerequired'); + this.passwordErrors = this.loginHelper.getErrorMessages('mm.login.passwordrequired'); + this.emailErrors = this.loginHelper.getErrorMessages('mm.login.missingemail'); + this.email2Errors = this.loginHelper.getErrorMessages('mm.login.missingemail', null, 'mm.login.emailnotmatch'); + this.policyErrors = this.loginHelper.getErrorMessages('mm.login.policyagree'); + } + + /** + * View loaded. + */ + ionViewDidLoad() { + // Fetch the data. + this.fetchData().finally(() => { + this.settingsLoaded = true; + }); + } + + /** + * Complete the FormGroup using the settings received from server. + */ + protected completeFormGroup() { + this.signupForm.addControl('city', this.fb.control(this.settings.defaultcity || '')); + this.signupForm.addControl('country', this.fb.control(this.settings.country || '')); + + // Add the name fields. + for (let i in this.settings.namefields) { + this.signupForm.addControl(this.settings.namefields[i], this.fb.control('', Validators.required)); + } + + if (this.settings.recaptchachallengehash && this.settings.recaptchachallengeimage) { + this.signupForm.addControl('recaptcharesponse', this.fb.control('', Validators.required)); + } + + if (this.settings.sitepolicy) { + this.signupForm.addControl('policyagreed', this.fb.control(false, Validators.requiredTrue)); + } + } + + /** + * Fetch the required data from the server- + */ + protected fetchData() : Promise { + // Get site config. + return this.sitesProvider.getSitePublicConfig(this.siteUrl).then((config) => { + this.siteConfig = config; + + if (this.treatSiteConfig(config)) { + return this.getSignupSettings(); + } + }).then(() => { + this.completeFormGroup(); + }).catch((err) => { + this.domUtils.showErrorModal(err); + }); + } + + /** + * Get signup settings from server. + */ + protected getSignupSettings() : Promise { + return this.wsProvider.callAjax('auth_email_get_signup_settings', {}, {siteUrl: this.siteUrl}).then((settings) => { + this.settings = settings; + this.categories = this.loginHelper.formatProfileFieldsForSignup(settings.profilefields); + + if (this.signupForm && this.signupForm.controls['recaptcharesponse']) { + this.signupForm.controls['recaptcharesponse'].reset(); // Reset captcha. + } + + this.namefieldsErrors = {}; + if (settings.namefields) { + settings.namefields.forEach((field) => { + this.namefieldsErrors[field] = this.loginHelper.getErrorMessages('mm.login.missing' + field); + }); + } + + return this.utils.getCountryList().then((countries) => { + this.countries = countries; + this.countriesKeys = Object.keys(countries); + }); + }); + } + + /** + * Treat the site config, checking if it's valid and extracting the data we're interested in. + * + * @param {any} siteConfig Site config to treat. + */ + protected treatSiteConfig(siteConfig) { + if (siteConfig && siteConfig.registerauth == 'email' && !this.loginHelper.isEmailSignupDisabled(siteConfig)) { + this.siteName = siteConfig.sitename; + this.authInstructions = siteConfig.authinstructions; + return true; + } else { + this.domUtils.showErrorModal( + this.translate.instant('mm.login.signupplugindisabled', {$a: this.translate.instant('mm.login.auth_email')})); + this.navCtrl.pop(); + return false; + } + } + + /** + * Pull to refresh. + * + * @param {any} refresher Refresher. + */ + refreshSettings(refresher: any) : void { + this.fetchData().finally(() => { + refresher.complete(); + }); + } + + /** + * Request another captcha. + * + * @param {boolean} ignoreError Whether to ignore errors. + */ + requestCaptcha(ignoreError?: boolean) : void { + let modal = this.domUtils.showModalLoading(); + this.getSignupSettings().catch((err) => { + if (!ignoreError && err) { + this.domUtils.showErrorModal(err); + } + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Create account. + */ + create() : void { + if (!this.signupForm.valid) { + // Form not valid. Scroll to the first element with errors. + if (!this.domUtils.scrollToInputError(this.content, document.body)) { + // Input not found, show an error modal. + this.domUtils.showErrorModal('mm.core.errorinvalidform', true); + } + } else { + let params: any = { + username: this.signupForm.value.username.trim().toLowerCase(), + password: this.signupForm.value.password, + firstname: this.textUtils.cleanTags(this.signupForm.value.firstname), + lastname: this.textUtils.cleanTags(this.signupForm.value.lastname), + email: this.signupForm.value.email.trim(), + city: this.textUtils.cleanTags(this.signupForm.value.city), + country: this.signupForm.value.country + }, + modal = this.domUtils.showModalLoading('mm.core.sending', true); + + if (this.siteConfig.launchurl) { + let service = this.sitesProvider.determineService(this.siteUrl); + params.redirect = this.loginHelper.prepareForSSOLogin(this.siteUrl, service, this.siteConfig.launchurl); + } + + if (this.settings.recaptchachallengehash && this.settings.recaptchachallengeimage) { + params.recaptchachallengehash = this.settings.recaptchachallengehash; + params.recaptcharesponse = this.signupForm.value.recaptcharesponse; + } + + // Get the data for the custom profile fields. + // @todo: Implement it once profile fields are implemented. + // $mmUserProfileFieldsDelegate.getDataForFields(fields, true, 'email', $scope.data).then(function(fieldsData) { + // params.customprofilefields = fieldsData; + + this.wsProvider.callAjax('auth_email_signup_user', params, {siteUrl: this.siteUrl}).then((result) => { + if (result.success) { + // Show alert and ho back. + let message = this.translate.instant('mm.login.emailconfirmsent', {$a: params.email}); + this.domUtils.showAlert('mm.core.success', message); + this.navCtrl.pop(); + } else { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0].message); + } else { + this.domUtils.showErrorModal('mm.login.usernotaddederror', true); + } + + // Error sending, request another capctha since the current one is probably invalid now. + this.requestCaptcha(true); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error && error.error, 'mm.login.usernotaddederror', true); + + // Error sending, request another capctha since the current one is probably invalid now. + this.requestCaptcha(true); + }).finally(() => { + modal.dismiss(); + }); + } + } + + /** + * Show authentication instructions. + */ + protected showAuthInstructions() { + this.textUtils.expandText(this.translate.instant('mm.login.instructions'), this.authInstructions, true); + } +} diff --git a/src/core/login/pages/site-error/site-error.html b/src/core/login/pages/site-error/site-error.html index 960233c44..071683560 100644 --- a/src/core/login/pages/site-error/site-error.html +++ b/src/core/login/pages/site-error/site-error.html @@ -21,7 +21,7 @@ {{ 'mm.login.contactyouradministratorissue' | translate:{$a: ''} }}

- {{issue}} +

diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 7f5aecf20..5d3faa201 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -122,6 +122,10 @@ export class CoreLoginHelperProvider { formatProfileFieldsForSignup(profileFields: any[]) : any { let categories = {}; + if (!profileFields) { + return categories; + } + profileFields.forEach((field) => { if (!field.signup) { // Not a signup field, ignore it. @@ -161,7 +165,7 @@ export class CoreLoginHelperProvider { var errors: any = {}; if (requiredMsg) { - errors.required = this.translate.instant(requiredMsg); + errors.required = errors.requiredTrue = this.translate.instant(requiredMsg); } if (emailMsg) { errors.email = this.translate.instant(emailMsg); diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index b6ec92d47..f6f2ea770 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform } from 'ionic-angular'; +import { LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreTextUtilsProvider } from './text'; import { CoreAppProvider } from '../app'; @@ -365,7 +365,7 @@ export class CoreDomUtilsProvider { } // Finally, check again. - if (element.className.indexOf(positionParentClass) != -1) { + if (element && element.className.indexOf(positionParentClass) != -1) { element = null; } } @@ -564,13 +564,14 @@ export class CoreDomUtilsProvider { /** * Scroll to a certain element inside another element. * - * @param {HTMLElement} scrollEl The element that must be scrolled. + * @param {Content|HTMLElement} scrollEl The content that must be scrolled. * @param {HTMLElement} container Element to search in. * @param {string} [selector] Selector to find the element to scroll to. If not defined, scroll to the container. * @param {string} [scrollParentClass] Parent class where to stop calculating the position. Default scroll-content. * @return {boolean} True if the element is found, false otherwise. */ - scrollToElement(scrollEl: HTMLElement, container: HTMLElement, selector?: string, scrollParentClass?: string) : boolean { + scrollToElement(scrollEl: Content|HTMLElement, container: HTMLElement, selector?: string, scrollParentClass?: string) + : boolean { let position = this.getElementXY(container, selector, scrollParentClass); if (!position) { return false; @@ -583,23 +584,17 @@ export class CoreDomUtilsProvider { /** * Search for an input with error (mm-input-error directive) and scrolls to it if found. * - * @param {HTMLElement} scrollEl The element that must be scrolled. + * @param {Content|HTMLElement} scrollEl The element that must be scrolled. * @param {HTMLElement} container Element to search in. * @param [scrollParentClass] Parent class where to stop calculating the position. Default scroll-content. * @return {boolean} True if the element is found, false otherwise. */ - scrollToInputError(scrollEl: HTMLElement, container: HTMLElement, scrollParentClass?: string) : boolean { - // @todo - return true; - // Wait an instant to make sure errors are shown and scroll to the element. - // return $timeout(function() { - // if (!scrollDelegate) { - // scrollDelegate = $ionicScrollDelegate; - // } + scrollToInputError(scrollEl: Content|HTMLElement, container: HTMLElement, scrollParentClass?: string) : boolean { + if (!scrollEl) { + return false; + } - // scrollDelegate.resize(); - // return self.scrollToElement(container, '.mm-input-has-errors', scrollDelegate, scrollParentClass); - // }, 100); + return this.scrollToElement(scrollEl, container, '.core-input-error', scrollParentClass); } /** diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 3a642ab79..ea2423c27 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -553,8 +553,8 @@ export class CoreUtilsProvider { for (let name in table) { if (name.indexOf('mm.core.country-') === 0) { - name = name.replace('mm.core.country-', ''); - countries[name] = table[name]; + let code = name.replace('mm.core.country-', ''); + countries[code] = table[name]; } }