diff --git a/android/app/src/main/assets/splash-screen.png b/android/app/src/main/assets/splash-screen.png new file mode 100644 index 0000000000..f0b042ee04 Binary files /dev/null and b/android/app/src/main/assets/splash-screen.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_new_splash_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_new_splash_foreground.xml index ece36e455d..b6ff074f5f 100644 --- a/android/app/src/main/res/drawable/ic_launcher_new_splash_foreground.xml +++ b/android/app/src/main/res/drawable/ic_launcher_new_splash_foreground.xml @@ -16,19 +16,19 @@ android:fillType="evenOdd"/> diff --git a/android/app/src/main/res/fyle_logo_square.png b/android/app/src/main/res/fyle_logo_square.png new file mode 100644 index 0000000000..119fe28ec1 Binary files /dev/null and b/android/app/src/main/res/fyle_logo_square.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.png deleted file mode 100644 index 69076eb537..0000000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..ad2642ff46 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png index ccc4a4c960..96f00618e0 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.png deleted file mode 100644 index fbb5189b69..0000000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..7213949a7e Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash_round.webp new file mode 100644 index 0000000000..2f10fcc36d Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_new_splash_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.png deleted file mode 100644 index ed8fc23e71..0000000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.png deleted file mode 100644 index 89e612ccf5..0000000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..dff53babfc Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash_round.webp new file mode 100644 index 0000000000..94dd502d5d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_new_splash_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.png deleted file mode 100644 index ddf7928b71..0000000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.webp new file mode 100644 index 0000000000..b2d893d881 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash_round.webp new file mode 100644 index 0000000000..fee5841ff0 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_new_splash_round.webp differ diff --git a/android/app/src/main/res/values/ic_launcher_new_splash_background.xml b/android/app/src/main/res/values/ic_launcher_new_splash_background.xml index 4294a955b9..cb5eb46f62 100644 --- a/android/app/src/main/res/values/ic_launcher_new_splash_background.xml +++ b/android/app/src/main/res/values/ic_launcher_new_splash_background.xml @@ -1,4 +1,4 @@ - #220033 + #FFFFFF \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index d0016f643b..a74326a208 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -18,6 +18,6 @@ \ No newline at end of file diff --git a/capacitor.config.json b/capacitor.config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/capacitor.config.json @@ -0,0 +1 @@ +{} diff --git a/fyle_logo.png b/fyle_logo.png new file mode 100644 index 0000000000..375696808a Binary files /dev/null and b/fyle_logo.png differ diff --git a/ios/App/App/Assets.xcassets/fyle_logo.png b/ios/App/App/Assets.xcassets/fyle_logo.png index 84e826c740..b68fd4852a 100644 Binary files a/ios/App/App/Assets.xcassets/fyle_logo.png and b/ios/App/App/Assets.xcassets/fyle_logo.png differ diff --git a/ios/App/App/Base.lproj/LaunchScreen.storyboard b/ios/App/App/Base.lproj/LaunchScreen.storyboard index b2b38dd969..56d6415b9d 100644 --- a/ios/App/App/Base.lproj/LaunchScreen.storyboard +++ b/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -22,7 +22,7 @@ - + diff --git a/ios/App/App/fyle_logo.png b/ios/App/App/fyle_logo.png index 84e826c740..b68fd4852a 100644 Binary files a/ios/App/App/fyle_logo.png and b/ios/App/App/fyle_logo.png differ diff --git a/ios/App/fyle_logo.png b/ios/App/fyle_logo.png index 84e826c740..b68fd4852a 100644 Binary files a/ios/App/fyle_logo.png and b/ios/App/fyle_logo.png differ diff --git a/src/app/auth/disabled/disabled.page.html b/src/app/auth/disabled/disabled.page.html index f78f04cfac..25c4b2f96c 100644 --- a/src/app/auth/disabled/disabled.page.html +++ b/src/app/auth/disabled/disabled.page.html @@ -1,25 +1,33 @@ - - - - - - - -
-
Account Disabled
- Stop -
- This account is no longer active. Please contact your company admin for details. +
+
+
+ +
+
+

Account disabled

+

+ This account is no longer active. Please contact your company admin for details. +

+
+
+
+ +
+ + Back to sign in +
+
- - - - - Incorrect Account? - Try Signing in Again - - - diff --git a/src/app/auth/disabled/disabled.page.scss b/src/app/auth/disabled/disabled.page.scss index 5fdb92d550..4b402c85cd 100644 --- a/src/app/auth/disabled/disabled.page.scss +++ b/src/app/auth/disabled/disabled.page.scss @@ -1,93 +1,80 @@ -$disabled-header: #220033; -$body-header: #000; -$body-subheader: #4a4a4a; -$password-icon: #b9beba; -$secondary-cta-border: #e0e0e0; +@import '../../../theme/colors.scss'; -.disabled { - &--header { - min-height: 140px; - } - - &--header-container { - min-height: 140px; - background-color: $disabled-header; - } +.disabled-user { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; - &--header-logo-container { - text-align: center; + &__content-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 2; } - &--header-logo { - max-width: 100px; + &__arrow-icon { + fill: $pure-white; + margin-right: 8px; } - &--body { - padding: 24px; - text-align: center; + &__cta-text { + font-size: 14px; + font-weight: 500; } - &--body-field { - width: 100%; + &__cta { + margin: 0 20px 24px 20px; } - &--body-header { - font-size: 24px; - margin-top: 16px; - margin-bottom: 8px; - color: $body-header; - font-weight: 700; + &__cta-content { + display: flex; + align-items: center; + justify-content: center; } - &--image { - max-width: 125px; - padding: 12px; + &__error-icon-container { + width: 60px; + height: 60px; + border-radius: 8px; + background: $pale-pink; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; } - &--subtext { - font-size: 16px; - color: $body-subheader; - margin-top: 8px; - margin-bottom: 24px; + &__error-icon { + width: 45px; + height: 45px; } - &--primary-cta { - .mat-button-base { - width: 100%; - font-weight: 700; - min-height: 47px; - } + &__text { + display: flex; + align-items: center; + gap: 8px; + flex-direction: column; } - &--password-visibility-icon { - color: $password-icon; + &__header { + color: $black; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: normal; + height: 26px; + margin: 0; } - &--secondary-cta { - .mat-button-base { - width: 100%; - font-weight: 700; - min-height: 47px; - letter-spacing: 1.6px; - border: 1px solid $secondary-cta-border; - } - margin-top: 24px; - } - - &--redirect { - margin: 16px; + &__content { + color: $black-light; text-align: center; - font-size: 16px; - a { - text-decoration: none; - } - } - - &--edit-email { - text-align: end; - } - - &--greyed { - color: grey; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 1.25; + height: 36px; + width: 274px; } } diff --git a/src/app/auth/new-password/new-password.module.ts b/src/app/auth/new-password/new-password.module.ts index ebe48b4b6e..452791efbb 100644 --- a/src/app/auth/new-password/new-password.module.ts +++ b/src/app/auth/new-password/new-password.module.ts @@ -17,6 +17,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { PopupComponent } from './popup/popup.component'; +import { SharedModule } from 'src/app/shared/shared.module'; @NgModule({ imports: [ @@ -30,6 +31,7 @@ import { PopupComponent } from './popup/popup.component'; ReactiveFormsModule, MatIconModule, MatButtonModule, + SharedModule, ], declarations: [NewPasswordPage, PopupComponent], }) diff --git a/src/app/auth/new-password/new-password.page.html b/src/app/auth/new-password/new-password.page.html index ad72afd5e7..b5798a0961 100644 --- a/src/app/auth/new-password/new-password.page.html +++ b/src/app/auth/new-password/new-password.page.html @@ -1,133 +1,116 @@ - - - - - - - - -
-
Reset password
-
+ +
+
+
- - - - - Is between 12 to 32 characters -
-
- - - - - Contains atleast 1 uppercase character -
-
- - - - - Contains atleast 1 lowercase character -
-
- - - - - Contains atleast 1 number -
-
- - - - - Contains atleast 1 special character +
Reset password
+
+
+
+
+
New password
+
+ +
+ +
+
+ +
+
+ Password cannot be empty +
+
+ Please enter a valid password. +
+
+
+
+
Confirm new password
+
+ +
+ +
+
+
+
+ Password cannot be empty +
+
+ Passwords do not match +
+
+
+
-
Please enter a new password
-
- - - - {{hide ? 'visibility_off' : 'visibility'}} - - -
-
- + Reset password + +
+ + Back to sign in +
- - - - - Back to Sign in - - - diff --git a/src/app/auth/new-password/new-password.page.scss b/src/app/auth/new-password/new-password.page.scss index e1afee8662..5df595b64e 100644 --- a/src/app/auth/new-password/new-password.page.scss +++ b/src/app/auth/new-password/new-password.page.scss @@ -1,84 +1,114 @@ -$new-password-header: #220033; -$form-header: #000; -$form-subheader: #4a4a4a; -$password-icon: #b9beba; -$secondary-cta-border: #e0e0e0; -$flash-yellow: #fde081; -$error: #f00; -$offline-title: #4a4a4a; -$offline-sub-title: #ababab; +@import '../../../theme/colors.scss'; .new-password { - &--header { - min-height: 140px; + display: flex; + flex-direction: column; + padding: 24px 20px 44px 20px; + margin-top: 18px; + height: 100%; + justify-content: space-between; + + &__password-container { + display: flex; } - &--header-container { - min-height: 140px; - background-color: $new-password-header; + &__input-container { + margin-bottom: 24px; } - &--header-logo-container { - text-align: center; + &__error { + color: $red; + font-size: 12px; + display: flex; + align-items: center; } - &--header-logo { - max-width: 100px; + &__text { + border-bottom: 1px solid $grey-lighter; + + &__invalid { + border-bottom: 1px solid $red; + } } - &--form { - padding: 24px; + &__mandatory { + color: $brand-primary; + display: inline-block; + font-size: 14px; + font-weight: 400; } - &--form-field { - width: 100%; + &__text-label { + margin: 0 8px 0 0; + max-width: 90%; + min-width: 120px; + font-size: 12px; + color: $black-light; + line-height: 16px; + white-space: nowrap; + font-weight: 400; } - &--form-header { - font-size: 24px; - margin-top: 16px; - margin-bottom: 8px; - color: $form-header; - font-weight: 700; + &__text-input { + border: 0; + font-size: 14px; + font-weight: 400; + height: 18px; + line-height: 18px; + color: $blue-black; + width: 100%; + margin: 6px 0; + padding: 0; } - &--form-subheader { - font-size: 16px; - color: $form-subheader; - margin-top: 8px; - margin-bottom: 24px; + &__password-icon { + width: 20px; + height: 20px; + fill: $black-light; } - &--primary-cta { - margin-top: 12px; - .mat-button-base { - width: 100%; - font-weight: 700; - min-height: 47px; - } + &__password-icon-container { + display: flex; + align-items: center; } - &--password-rules { - padding: 14px 0; + &__form-header { + font-size: 20px; + position: relative; + margin-bottom: 24px; + color: $black; + font-weight: 500; } - &--save { + &__save { width: 100%; &__disabled { opacity: 0.2; } } - &--validation { - vertical-align: middle; - font-size: 16px; + &__back-icon { + margin-bottom: 16px; + width: 28px; + height: 28px; + } - &__correct { - color: green; - } + &__cta-text { + font-size: 14px; + font-weight: 500; + } - &__incorrect { - color: red; - } + &__arrow-icon { + fill: $pure-white; + margin-right: 6px; + } + + &__cta-secondary { + display: flex; + padding-top: 18px; + align-items: center; + justify-content: center; + color: $blue-black; + flex-direction: row; } } diff --git a/src/app/auth/new-password/new-password.page.spec.ts b/src/app/auth/new-password/new-password.page.spec.ts index f5db061720..c4da1499ba 100644 --- a/src/app/auth/new-password/new-password.page.spec.ts +++ b/src/app/auth/new-password/new-password.page.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; -import { IonicModule, PopoverController } from '@ionic/angular'; +import { IonicModule } from '@ionic/angular'; import { NewPasswordPage } from './new-password.page'; import { AuthService } from 'src/app/core/services/auth.service'; @@ -9,14 +9,16 @@ import { TrackingService } from 'src/app/core/services/tracking.service'; import { DeviceService } from 'src/app/core/services/device.service'; import { LoginInfoService } from 'src/app/core/services/login-info.service'; import { FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { of } from 'rxjs'; import { apiEouRes } from 'src/app/core/mock-data/extended-org-user.data'; import { extendedDeviceInfoMockData } from 'src/app/core/mock-data/extended-device-info.data'; import { RouterTestingModule } from '@angular/router/testing'; import { getElementBySelector } from 'src/app/core/dom-helpers'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { PopupComponent } from './popup/popup.component'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; describe('NewPasswordPage', () => { let component: NewPasswordPage; @@ -24,19 +26,23 @@ describe('NewPasswordPage', () => { let authService: jasmine.SpyObj; let routerAuthService: jasmine.SpyObj; let loaderService: jasmine.SpyObj; - let popoverController: jasmine.SpyObj; let trackingService: jasmine.SpyObj; let deviceService: jasmine.SpyObj; let loginInfoService: jasmine.SpyObj; + let router: jasmine.SpyObj; + let matSnackBar: jasmine.SpyObj; + let snackbarPropertiesService: jasmine.SpyObj; beforeEach(waitForAsync(() => { const authServiceSpy = jasmine.createSpyObj('AuthService', ['refreshEou']); const routerAuthServiceSpy = jasmine.createSpyObj('RouterAuthService', ['resetPassword']); const loaderServiceSpy = jasmine.createSpyObj('LoaderService', ['showLoader', 'hideLoader']); - const popoverControllerSpy = jasmine.createSpyObj('PopoverController', ['create']); const trackingServiceSpy = jasmine.createSpyObj('TrackingService', ['onSignin', 'resetPassword', 'eventTrack']); const deviceServiceSpy = jasmine.createSpyObj('DeviceService', ['getDeviceInfo']); const loginInfoServiceSpy = jasmine.createSpyObj('LoginInfoService', ['addLoginInfo']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + const matSnackBarSpy = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']); + const snackbarPropertiesServiceSpy = jasmine.createSpyObj('SnackbarPropertiesService', ['setSnackbarProperties']); TestBed.configureTestingModule({ declarations: [NewPasswordPage], @@ -46,10 +52,12 @@ describe('NewPasswordPage', () => { { provide: AuthService, useValue: authServiceSpy }, { provide: RouterAuthService, useValue: routerAuthServiceSpy }, { provide: LoaderService, useValue: loaderServiceSpy }, - { provide: PopoverController, useValue: popoverControllerSpy }, { provide: TrackingService, useValue: trackingServiceSpy }, { provide: DeviceService, useValue: deviceServiceSpy }, { provide: LoginInfoService, useValue: loginInfoServiceSpy }, + { provide: MatSnackBar, useValue: matSnackBarSpy }, + { provide: SnackbarPropertiesService, useValue: snackbarPropertiesServiceSpy }, + { provide: Router, useValue: routerSpy }, { provide: ActivatedRoute, useValue: { @@ -67,10 +75,12 @@ describe('NewPasswordPage', () => { authService = TestBed.inject(AuthService) as jasmine.SpyObj; routerAuthService = TestBed.inject(RouterAuthService) as jasmine.SpyObj; loaderService = TestBed.inject(LoaderService) as jasmine.SpyObj; - popoverController = TestBed.inject(PopoverController) as jasmine.SpyObj; trackingService = TestBed.inject(TrackingService) as jasmine.SpyObj; deviceService = TestBed.inject(DeviceService) as jasmine.SpyObj; loginInfoService = TestBed.inject(LoginInfoService) as jasmine.SpyObj; + router = TestBed.inject(Router) as jasmine.SpyObj; + matSnackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj; + snackbarPropertiesService = TestBed.inject(SnackbarPropertiesService) as jasmine.SpyObj; fixture.detectChanges(); })); @@ -78,133 +88,23 @@ describe('NewPasswordPage', () => { expect(component).toBeTruthy(); }); - describe('ngOnInit():', () => { - it('should initialize the form and observables', () => { - component.ngOnInit(); - - expect(component.fg).toBeDefined(); - expect(component.lengthValidationDisplay$).toBeDefined(); - expect(component.uppercaseValidationDisplay$).toBeDefined(); - expect(component.numberValidationDisplay$).toBeDefined(); - expect(component.specialCharValidationDisplay$).toBeDefined(); - expect(component.lowercaseValidationDisplay$).toBeDefined(); - }); - - it('should validate password length of 12 characters', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="lengthValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('123456789012'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should validate password length of 32 characters', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="lengthValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('1234567890123456789012345678901'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate password length of less 12 characters', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="lengthValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('12345'); - expect(closeIcon).toBeDefined(); - }); - - it('should not validate password length of more 32 characters', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="lengthValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('12345678901234567890123456789012'); - expect(closeIcon).toBeDefined(); - }); - - it('should validate the presence of an uppercase letter in password', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="uppercaseValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithUpperCase'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate the absence of an uppercase letter in password', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="uppercaseValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('passwordwithoutuppercase'); - expect(closeIcon).toBeDefined(); - }); - - it('should validate the presence of a number in password', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="numberValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithNumber123'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate the absence of a number in password', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="numberValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithoutNumber'); - expect(closeIcon).toBeDefined(); - }); - - it('should validate the presence of a special character in password', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="specialcharValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWith@Special#Char'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate the absence of a special character in password', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="specialcharValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithoutSpecialChar'); - expect(closeIcon).toBeDefined(); - }); - - it('should validate the presence of a lowercase letter in password', () => { - const checkmarkIcon = getElementBySelector(fixture, '[data-testid="lowercaseValidation_correct"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PasswordWithLowerCase'); - expect(checkmarkIcon).toBeDefined(); - }); - - it('should not validate the absence of a lowercase letter in password', () => { - const closeIcon = getElementBySelector(fixture, '[data-testid="lowercaseValidation_incorrect"]'); - const passwordControl = component.fg.controls.password as FormControl; - - passwordControl.setValue('PASSWORDWITHOUTLOWERCASE'); - expect(closeIcon).toBeDefined(); - }); - }); - describe('changePassword', () => { const passwordValue = 'DummyPassword@123'; const refreshToken = 'token123'; const resetPasswordRes = { ...apiEouRes, refresh_token: refreshToken }; it('should change the password and show success message on success', fakeAsync(() => { + const message = 'Password changed successfully'; spyOn(component, 'trackLoginInfo'); routerAuthService.resetPassword.and.returnValue(of(resetPasswordRes)); authService.refreshEou.and.returnValue(of(apiEouRes)); - const popoverSpy = jasmine.createSpyObj('HTMLIonPopoverElement', ['present']); - popoverController.create.and.returnValue(popoverSpy); deviceService.getDeviceInfo.and.returnValue(of(extendedDeviceInfoMockData)); loaderService.showLoader.and.resolveTo(); loaderService.hideLoader.and.resolveTo(); component.fg.controls.password.setValue(passwordValue); fixture.detectChanges(); - const newPasswordButton = getElementBySelector(fixture, '#new-password--btn-sign-in') as HTMLButtonElement; + const newPasswordButton = getElementBySelector(fixture, '.btn-primary') as HTMLButtonElement; newPasswordButton.click(); tick(500); @@ -215,27 +115,23 @@ describe('NewPasswordPage', () => { expect(trackingService.onSignin).toHaveBeenCalledOnceWith('ajain@fyle.in'); expect(trackingService.resetPassword).toHaveBeenCalledTimes(1); expect(component.trackLoginInfo).toHaveBeenCalledTimes(1); - expect(popoverController.create).toHaveBeenCalledOnceWith({ - component: PopupComponent, - componentProps: { - header: 'Password changed successfully', - route: ['/', 'auth', 'switch_org'], - }, - cssClass: 'dialog-popover', + expect(snackbarPropertiesService.setSnackbarProperties).toHaveBeenCalledOnceWith('success', { message }); + expect(matSnackBar.openFromComponent).toHaveBeenCalledOnceWith(ToastMessageComponent, { + ...snackbarPropertiesService.setSnackbarProperties('success', { message }), + panelClass: ['msb-success'], }); })); it('should show error message on failure', fakeAsync(() => { + const message = 'Something went wrong. Please try after some time.'; spyOn(component, 'trackLoginInfo'); routerAuthService.resetPassword.and.rejectWith(); - const popoverSpy = jasmine.createSpyObj('HTMLIonPopoverElement', ['present']); - popoverController.create.and.returnValue(popoverSpy); loaderService.showLoader.and.resolveTo(); loaderService.hideLoader.and.resolveTo(); component.fg.controls.password.setValue(passwordValue); fixture.detectChanges(); - const newPasswordButton = getElementBySelector(fixture, '#new-password--btn-sign-in') as HTMLButtonElement; + const newPasswordButton = getElementBySelector(fixture, '.btn-primary') as HTMLButtonElement; newPasswordButton.click(); tick(500); @@ -246,13 +142,9 @@ describe('NewPasswordPage', () => { expect(trackingService.onSignin).not.toHaveBeenCalled(); expect(trackingService.resetPassword).not.toHaveBeenCalled(); expect(component.trackLoginInfo).not.toHaveBeenCalled(); - expect(popoverController.create).toHaveBeenCalledOnceWith({ - component: PopupComponent, - componentProps: { - header: 'Setting new password failed. Please try again later.', - route: ['/', 'auth', 'sign_in'], - }, - cssClass: 'dialog-popover', + expect(matSnackBar.openFromComponent).toHaveBeenCalledOnceWith(ToastMessageComponent, { + ...snackbarPropertiesService.setSnackbarProperties('failure', { message }), + panelClass: ['msb-failure'], }); })); }); @@ -270,4 +162,81 @@ describe('NewPasswordPage', () => { expect(trackingService.eventTrack).toHaveBeenCalledOnceWith('Added Login Info', { label: '5.50.0' }); expect(loginInfoService.addLoginInfo).toHaveBeenCalledOnceWith('5.50.0', mockDate); })); + + it('redirectToSignIn(): should navigate to the sign-in page', () => { + component.redirectToSignIn(); + // @ts-ignore + expect(component.router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'sign_in']); // Should navigate to the correct route + }); + + describe('checkPasswordValidity():', () => { + it('should return null when isPasswordValid is true', () => { + component.isPasswordValid = true; + + const result = component.checkPasswordValidity(); + + expect(result).toBeNull(); // No errors + }); + + it('should return an error object when isPasswordValid is false', () => { + component.isPasswordValid = false; + + const result = component.checkPasswordValidity(); + + expect(result).toEqual({ invalidPassword: true }); // Error object + }); + }); + + describe('validatePasswordEquality():', () => { + it('should return null when password and confirmPassword match', () => { + component.fg.controls.password.setValue('StrongPassword@123'); + component.fg.controls.confirmPassword.setValue('StrongPassword@123'); + + const result = component.validatePasswordEquality(); + + expect(result).toBeNull(); // No errors + }); + + it('should return an error object when password and confirmPassword do not match', () => { + component.fg.controls.password.setValue('StrongPassword@123'); + component.fg.controls.confirmPassword.setValue('DifferentPassword@123'); + + const result = component.validatePasswordEquality(); + + expect(result).toEqual({ passwordMismatch: true }); + }); + + it('should return null when password or confirmPassword is empty', () => { + component.fg.controls.password.setValue(''); + component.fg.controls.confirmPassword.setValue(''); + + const result = component.validatePasswordEquality(); + + expect(result).toBeNull(); + }); + }); + + describe('onPasswordValid():', () => { + it('should set isPasswordValid to true when called with true', () => { + component.onPasswordValid(true); + expect(component.isPasswordValid).toBeTrue(); + }); + + it('should set isPasswordValid to false when called with false', () => { + component.onPasswordValid(false); + expect(component.isPasswordValid).toBeFalse(); + }); + }); + + describe('setPasswordTooltip():', () => { + it('should set showPasswordTooltip to true when called with true', () => { + component.setPasswordTooltip(true); + expect(component.showPasswordTooltip).toBeTrue(); + }); + + it('should set showPasswordTooltip to false when called with false', () => { + component.setPasswordTooltip(false); + expect(component.showPasswordTooltip).toBeFalse(); + }); + }); }); diff --git a/src/app/auth/new-password/new-password.page.ts b/src/app/auth/new-password/new-password.page.ts index 0d5295e798..57530edce8 100644 --- a/src/app/auth/new-password/new-password.page.ts +++ b/src/app/auth/new-password/new-password.page.ts @@ -1,16 +1,17 @@ import { Component, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { finalize, map, switchMap, tap } from 'rxjs/operators'; +import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; +import { finalize, switchMap, tap } from 'rxjs/operators'; import { from, Observable } from 'rxjs'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { LoaderService } from 'src/app/core/services/loader.service'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; import { AuthService } from 'src/app/core/services/auth.service'; -import { PopoverController } from '@ionic/angular'; -import { PopupComponent } from './popup/popup.component'; import { TrackingService } from '../../core/services/tracking.service'; import { DeviceService } from '../../core/services/device.service'; import { LoginInfoService } from '../../core/services/login-info.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; @Component({ selector: 'app-new-password', @@ -30,7 +31,15 @@ export class NewPasswordPage implements OnInit { lowercaseValidationDisplay$: Observable; - hide = false; + isPasswordValid = false; + + hide = true; + + hideConfirmPassword = true; + + showPasswordTooltip = false; + + isLoading = false; constructor( private fb: FormBuilder, @@ -38,54 +47,27 @@ export class NewPasswordPage implements OnInit { private loaderService: LoaderService, private routerAuthService: RouterAuthService, private authService: AuthService, - private popoverController: PopoverController, private trackingService: TrackingService, private deviceService: DeviceService, - private loginInfoService: LoginInfoService + private loginInfoService: LoginInfoService, + private router: Router, + private matSnackBar: MatSnackBar, + private snackbarPropertiesService: SnackbarPropertiesService ) {} - ngOnInit() { + ngOnInit(): void { this.fg = this.fb.group({ - password: [ - '', - Validators.compose([ - Validators.required, - Validators.minLength(12), - Validators.maxLength(32), - Validators.pattern(/[A-Z]/), - Validators.pattern(/[a-z]/), - Validators.pattern(/[0-9]/), - Validators.pattern(/[!@#$%^&*()+\-:;<=>{}|~?]/), - ]), - ], + password: ['', [Validators.required, this.checkPasswordValidity]], + confirmPassword: ['', [Validators.required, this.validatePasswordEquality]], }); - - this.lengthValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => password && password.length >= 12 && password.length <= 32) - ); - - this.uppercaseValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => /[A-Z]/.test(password)) - ); - - this.numberValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => /[0-9]/.test(password)) - ); - this.specialCharValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => /[!@#$%^&*()+\-:;<=>{}|~?]/.test(password)) - ); - - this.lowercaseValidationDisplay$ = this.fg.controls.password.valueChanges.pipe( - map((password) => /[a-z]/.test(password)) - ); } - changePassword() { - const refreshToken = this.activatedRoute.snapshot.params.refreshToken; - + changePassword(): void { + const refreshToken = this.activatedRoute.snapshot.params.refreshToken as string; + this.isLoading = true; from(this.loaderService.showLoader()) .pipe( - switchMap(() => this.routerAuthService.resetPassword(refreshToken, this.fg.controls.password.value)), + switchMap(() => this.routerAuthService.resetPassword(refreshToken, this.fg.controls.password.value as string)), switchMap(() => this.authService.refreshEou()), tap(async (eou) => { const email = eou.us.email; @@ -93,39 +75,65 @@ export class NewPasswordPage implements OnInit { this.trackingService.resetPassword(); await this.trackLoginInfo(); }), - finalize(() => from(this.loaderService.hideLoader())) + finalize(() => { + this.isLoading = false; + return from(this.loaderService.hideLoader()); + }) ) .subscribe( - async () => { - const popup = await this.popoverController.create({ - component: PopupComponent, - componentProps: { - header: 'Password changed successfully', - route: ['/', 'auth', 'switch_org'], - }, - cssClass: 'dialog-popover', + () => { + const toastMessageData = { + message: 'Password changed successfully', + }; + + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarPropertiesService.setSnackbarProperties('success', toastMessageData), + panelClass: ['msb-success'], }); - - await popup.present(); + this.router.navigate(['/', 'auth', 'switch_org']); }, - async () => { - const popup = await this.popoverController.create({ - component: PopupComponent, - componentProps: { - header: 'Setting new password failed. Please try again later.', - route: ['/', 'auth', 'sign_in'], - }, - cssClass: 'dialog-popover', + () => { + const toastMessageData = { + message: 'Something went wrong. Please try after some time.', + }; + + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarPropertiesService.setSnackbarProperties('failure', toastMessageData), + panelClass: ['msb-failure'], }); - - await popup.present(); + this.router.navigate(['/', 'auth', 'sign_in']); } ); } - async trackLoginInfo() { + async trackLoginInfo(): Promise { const deviceInfo = await this.deviceService.getDeviceInfo().toPromise(); this.trackingService.eventTrack('Added Login Info', { label: deviceInfo.appVersion }); await this.loginInfoService.addLoginInfo(deviceInfo.appVersion, new Date()); } + + onPasswordValid(isValid: boolean): void { + this.isPasswordValid = isValid; + this.fg.controls.password.updateValueAndValidity(); + this.fg.controls.confirmPassword.updateValueAndValidity(); + } + + redirectToSignIn(): void { + this.router.navigate(['/', 'auth', 'sign_in']); + } + + setPasswordTooltip(value: boolean): void { + this.showPasswordTooltip = value; + } + + checkPasswordValidity = (): ValidationErrors => (this.isPasswordValid ? null : { invalidPassword: true }); + + validatePasswordEquality = (): ValidationErrors => { + if (!this.fg) { + return null; + } + const password = this.fg.controls.password.value as string; + const confirmPassword = this.fg.controls.confirmPassword.value as string; + return password === confirmPassword ? null : { passwordMismatch: true }; + }; } diff --git a/src/app/auth/pending-verification/pending-verification.page.html b/src/app/auth/pending-verification/pending-verification.page.html index 0e31f3bb36..317f9d0cad 100644 --- a/src/app/auth/pending-verification/pending-verification.page.html +++ b/src/app/auth/pending-verification/pending-verification.page.html @@ -1,26 +1,96 @@ - - - + +
+
+
+
+ +
+
+
The invitation has expired
+
+ Enter your registered email address to receive a new invitation and set up your account on Fyle. +
+
+
+
Registered email
+ +
+ Please enter a valid email. +
+
+
+
+ + Send invite + +
+ + Back to Sign In +
+
+
+
+
+
+ +
+
+

Invitation link sent

+

+ A new invitation link has been sent to your registered email address. Check your inbox to continue setting + up your account. +

+
+
+
+ +
+ + Back to sign in +
+
+
+
+
+
diff --git a/src/app/auth/pending-verification/pending-verification.page.scss b/src/app/auth/pending-verification/pending-verification.page.scss new file mode 100644 index 0000000000..18f0553f15 --- /dev/null +++ b/src/app/auth/pending-verification/pending-verification.page.scss @@ -0,0 +1,203 @@ +@import '../../../theme/colors.scss'; + +.pending-verification { + height: 100%; + margin-top: 18px; + padding-top: 28px; + padding-bottom: 20px; + + &__send-invite { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + padding: 0 20px 24px 20px; + } + + &__form-container { + display: flex; + flex-direction: column; + padding-top: 24px; + justify-content: flex-start; + height: 100%; + } + + &__error-icon { + width: 45px; + height: 45px; + fill: $red; + } + + &__error-icon-container { + width: 40px; + height: 40px; + border-radius: 8px; + background: $pale-pink; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + padding: 10px; + } + + &__page-header { + font-size: 20px; + margin: 0 0 10px; + position: relative; + margin-bottom: 8px; + font-weight: 500; + } + + &__disabled { + opacity: 0.2; + } + + &__sub-header { + font-size: 14px; + margin: 0 0 10px; + position: relative; + margin-bottom: 32px; + color: $black-light; + line-height: 1.28; + } + + &__error-message { + color: $red; + font-size: 12px; + display: flex; + align-items: center; + } + + &__input-container { + &__label { + margin: 0 8px 0 0; + font-size: 12px; + color: $black-light; + line-height: 1.3; + font-weight: 400; + } + + &__input { + border: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.3; + color: $blue-black; + width: 100%; + padding: 6px 0; + border-bottom: 1px solid $grey-lighter; + } + + &__input:focus { + border-bottom: 1px solid $blue-black; + } + } + + &__content-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 2; + } + + &__arrow-icon { + fill: $pure-white; + margin-right: 6px; + } + + &__cta-text { + font-size: 14px; + font-weight: 500; + } + + &__cta-content { + display: flex; + align-items: center; + justify-content: center; + } + + &__success-message { + height: 100%; + display: flex; + flex-direction: column; + } + + &__success-icon-container { + width: 60px; + height: 60px; + border-radius: 8px; + background: $success-bg; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + } + + &__success-icon { + width: 45px; + height: 45px; + fill: $green; + } + + &__text { + display: flex; + align-items: center; + gap: 8px; + flex-direction: column; + } + + &__header { + color: $black; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: normal; + height: 26px; + margin: 0; + } + + &__resend-text { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + color: $black-light; + font-weight: 500; + gap: 2px; + + &__resend-link { + color: $brand-primary; + } + + &__spinner-icon { + color: $brand-primary; + height: 12px; + } + } + + &__content { + color: $black-light; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 1.25; + height: 36px; + width: 274px; + + &__reset-email { + font-weight: 500; + } + } + &__cta-secondary { + display: flex; + padding-top: 18px; + flex-direction: column; + justify-content: center; + align-items: center; + color: $blue-black; + display: flex; + flex-direction: row; + } +} diff --git a/src/app/auth/pending-verification/pending-verification.page.spec.ts b/src/app/auth/pending-verification/pending-verification.page.spec.ts index 17271dc667..604f6b7f00 100644 --- a/src/app/auth/pending-verification/pending-verification.page.spec.ts +++ b/src/app/auth/pending-verification/pending-verification.page.spec.ts @@ -1,48 +1,76 @@ import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { PendingVerificationPage } from './pending-verification.page'; -import { PageState } from 'src/app/core/models/page-state.enum'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; import { of, throwError } from 'rxjs'; import { authResData1 } from 'src/app/core/mock-data/auth-reponse.data'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; +import { HttpErrorResponse } from '@angular/common/http'; +import { getElementRef } from 'src/app/core/dom-helpers'; describe('PendingVerificationPage', () => { let component: PendingVerificationPage; let fixture: ComponentFixture; let router: jasmine.SpyObj; - let activatedRoute: jasmine.SpyObj; let routerAuthService: jasmine.SpyObj; + let matSnackBar: jasmine.SpyObj; + let snackbarPropertiesService: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; + let formBuilder: jasmine.SpyObj; + let fb: FormBuilder; beforeEach(waitForAsync(() => { const routerSpy = jasmine.createSpyObj('Router', ['navigate']); const routerAuthServiceSpy = jasmine.createSpyObj('RouterAuthService', ['resendVerificationLink']); - + const matSnackBarSpy = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']); + const snackbarPropertiesServiceSpy = jasmine.createSpyObj('SnackbarPropertiesService', ['setSnackbarProperties']); TestBed.configureTestingModule({ declarations: [PendingVerificationPage], - imports: [IonicModule.forRoot()], + imports: [IonicModule.forRoot(), RouterTestingModule, RouterModule, FormsModule, ReactiveFormsModule], providers: [ - { provide: Router, useValue: routerSpy }, - { - provide: ActivatedRoute, - useValue: { snapshot: { params: { orgId: 'orNVthTo2Zyo' } } }, - }, + FormBuilder, { provide: RouterAuthService, useValue: routerAuthServiceSpy, }, + { + provide: Router, + useValue: routerSpy, + }, + { + provide: MatSnackBar, + useValue: matSnackBarSpy, + }, + { + provide: SnackbarPropertiesService, + useValue: snackbarPropertiesServiceSpy, + }, + { + provide: ActivatedRoute, + useValue: { snapshot: { params: { email: 'aastha.b@fyle.in' } } }, + }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(PendingVerificationPage); component = fixture.componentInstance; router = TestBed.inject(Router) as jasmine.SpyObj; - activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; routerAuthService = TestBed.inject(RouterAuthService) as jasmine.SpyObj; + matSnackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj; + snackbarPropertiesService = TestBed.inject(SnackbarPropertiesService) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + fb = TestBed.inject(FormBuilder); + activatedRoute.snapshot.params.orgId = 'orNVthTo2Zyo'; + component.fg = fb.group({ + email: ['', Validators.compose([Validators.required, Validators.pattern('\\S+@\\S+\\.\\S{2,}')])], + }); fixture.detectChanges(); })); @@ -50,33 +78,6 @@ describe('PendingVerificationPage', () => { expect(component).toBeTruthy(); }); - it('should set hasTokenExpired to true if snapshot.params.hasTokenExpired is defined and true on ionViewWillEnter()', () => { - activatedRoute.snapshot.params.hasTokenExpired = true; - component.ionViewWillEnter(); - fixture.detectChanges(); - const pageTitle = fixture.debugElement.query(By.css('app-send-email')).nativeElement.title; - expect(component.hasTokenExpired).toBe(true); - expect(pageTitle).toEqual('Verification link expired'); - }); - - it('should set hasTokenExpired to false if snapshot.params.hasTokenExpired is defined and false on ionViewWillEnter()', () => { - activatedRoute.snapshot.params.hasTokenExpired = false; - component.ionViewWillEnter(); - fixture.detectChanges(); - const pageTitle = fixture.debugElement.query(By.css('app-send-email')).nativeElement.title; - expect(component.hasTokenExpired).toBe(false); - expect(pageTitle).toEqual('Please verify your email'); - }); - - it('should set hasTokenExpired to false and currentPageState to notSent if snapshot.params.hasTokenExpired is not defined on ionViewWillEnter()', () => { - component.ionViewWillEnter(); - fixture.detectChanges(); - const pageTitle = fixture.debugElement.query(By.css('app-send-email')).nativeElement.title; - expect(component.hasTokenExpired).toBe(false); - expect(component.currentPageState).toEqual(PageState.notSent); - expect(pageTitle).toEqual('Please verify your email'); - }); - it('resendVerificationLink(): should call routerAuthService and set PageState to success if API is successful', fakeAsync(() => { const data = { cluster_domain: authResData1.cluster_domain, @@ -86,36 +87,80 @@ describe('PendingVerificationPage', () => { tick(1000); expect(routerAuthService.resendVerificationLink).toHaveBeenCalledOnceWith('ajain@fyle.in', 'orNVthTo2Zyo'); expect(component.isLoading).toBeFalse(); - expect(component.currentPageState).toEqual(PageState.success); })); it('resendVerificationLink(): should call routerAuthService and call handleError if API is unsuccessful', fakeAsync(() => { - const error = new Error('An Error Occured'); + const error = { status: 500 } as HttpErrorResponse; routerAuthService.resendVerificationLink.and.returnValue(throwError(() => error)); spyOn(component, 'handleError'); component.resendVerificationLink('ajain@fyle.in'); tick(1000); expect(routerAuthService.resendVerificationLink).toHaveBeenCalledOnceWith('ajain@fyle.in', 'orNVthTo2Zyo'); expect(component.isLoading).toBeTrue(); - expect(component.currentPageState).not.toEqual(PageState.success); expect(component.handleError).toHaveBeenCalledOnceWith(error); })); - it('handleError(); should navigate to auth/disabled if status code is 422', () => { - const error = { - status: 422, - }; - component.handleError(error); - expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'disabled']); - expect(component.currentPageState).not.toEqual(PageState.failure); + describe('handleError():', () => { + it('handleError(); should navigate to auth/disabled if status code is 422', () => { + const error = { + status: 422, + } as HttpErrorResponse; + component.handleError(error); + expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'disabled']); + }); + + it('should display error message on other errors', () => { + const error = { status: 401 } as HttpErrorResponse; + const props = { + panelClass: ['msb-failure'], + }; + + matSnackBar.openFromComponent.and.callThrough(); + + component.handleError(error); + expect(matSnackBar.openFromComponent).toHaveBeenCalledOnceWith(ToastMessageComponent, { + ...props, + panelClass: ['msb-failure'], + }); + expect(snackbarPropertiesService.setSnackbarProperties).toHaveBeenCalledTimes(1); + }); }); - it('handleError(); should set pagestate to failure if status code', () => { - const error = { - status: 422, - }; - component.handleError(error); - expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'disabled']); - expect(component.currentPageState).not.toEqual(PageState.failure); + it('onGotoSignInClick(): should navigate to sign-in page', () => { + component.onGotoSignInClick(); + expect(router.navigate).toHaveBeenCalledWith(['/', 'auth', 'sign_in']); + }); + + describe('template', () => { + it('should render the form for entering email', () => { + component.isInvitationLinkSent = false; + fixture.detectChanges(); + + const formElement = fixture.debugElement.query(By.css('.pending-verification__form-container')); + expect(formElement).toBeTruthy(); + }); + + it('should display validation error for invalid email input', () => { + component.isInvitationLinkSent = false; + const emailControl = component.fg.controls.email; + emailControl.setValue('invalid-email'); + emailControl.markAsTouched(); + fixture.detectChanges(); + + const errorElement = getElementRef(fixture, '.pending-verification__error-message'); + expect(errorElement.nativeElement.textContent).toContain('Please enter a valid email.'); + }); + + it('should call resendVerificationLink with correct email when button is clicked', () => { + component.isInvitationLinkSent = false; + spyOn(component, 'resendVerificationLink'); + component.fg.controls.email.setValue('test@example.com'); + fixture.detectChanges(); + + const buttonElement = fixture.debugElement.query(By.css('ion-button')); + buttonElement.triggerEventHandler('click', null); + + expect(component.resendVerificationLink).toHaveBeenCalledWith('test@example.com'); + }); }); }); diff --git a/src/app/auth/pending-verification/pending-verification.page.ts b/src/app/auth/pending-verification/pending-verification.page.ts index 814941636a..cf54a1723b 100644 --- a/src/app/auth/pending-verification/pending-verification.page.ts +++ b/src/app/auth/pending-verification/pending-verification.page.ts @@ -1,51 +1,71 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { tap } from 'rxjs/operators'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; -import { PageState } from 'src/app/core/models/page-state.enum'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-pending-verification', templateUrl: './pending-verification.page.html', + styleUrls: ['./pending-verification.page.scss'], }) -export class PendingVerificationPage implements OnInit { - currentPageState: PageState; - +export class PendingVerificationPage { isLoading = false; - hasTokenExpired = false; + isInvitationLinkSent = false; + + fg: FormGroup; constructor( + private formBuilder: FormBuilder, private routerAuthService: RouterAuthService, private router: Router, - private activatedRoute: ActivatedRoute + private activatedRoute: ActivatedRoute, + private matSnackBar: MatSnackBar, + private snackbarProperties: SnackbarPropertiesService ) {} - ngOnInit() {} - - ionViewWillEnter() { - this.hasTokenExpired = this.activatedRoute.snapshot.params.hasTokenExpired || false; - this.currentPageState = PageState.notSent; + ionViewWillEnter(): void { + this.fg = this.formBuilder.group({ + email: ['', Validators.compose([Validators.required, Validators.pattern('\\S+@\\S+\\.\\S{2,}')])], + }); } - resendVerificationLink(email: string) { + resendVerificationLink(email: string): void { this.isLoading = true; - const orgId = this.activatedRoute.snapshot.params.orgId; + const orgId = this.activatedRoute.snapshot.params.orgId as string; this.routerAuthService .resendVerificationLink(email, orgId) .pipe(tap(() => (this.isLoading = false))) .subscribe({ - next: () => (this.currentPageState = PageState.success), - error: (err) => this.handleError(err), + next: () => { + this.isInvitationLinkSent = true; + }, + error: (err: HttpErrorResponse) => this.handleError(err), }); } - handleError(err: any) { + handleError(err: HttpErrorResponse): void { if (err.status === 422) { this.router.navigate(['/', 'auth', 'disabled']); } else { - this.currentPageState = PageState.failure; + const toastMessageData = { + message: 'Something went wrong. Please try after some time.', + }; + + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarProperties.setSnackbarProperties('failure', toastMessageData), + panelClass: ['msb-failure'], + }); } } + + onGotoSignInClick(): void { + this.router.navigate(['/', 'auth', 'sign_in']); + } } diff --git a/src/app/auth/reset-password/reset-password.page.html b/src/app/auth/reset-password/reset-password.page.html index e7ff210805..fb032a0b3e 100644 --- a/src/app/auth/reset-password/reset-password.page.html +++ b/src/app/auth/reset-password/reset-password.page.html @@ -1,11 +1,94 @@ - + +
+
+
+ +
+
Forgot password
+
+ Please enter your registered email address to receive instructions for resetting your password +
+
+
+
Registered email
+ +
+ Enter an email address. +
+
+
+
+ + Request reset link + +
+
+
+
+
+ +
+
+

Check your email

+

+ We've sent password recovery instructions to + {{resetEmail}} +

+
+
+ Didn’t receive the email? + Resend + +
+
+
+ +
+ + Back to sign in +
+
+
+
+
+
diff --git a/src/app/auth/reset-password/reset-password.page.scss b/src/app/auth/reset-password/reset-password.page.scss new file mode 100644 index 0000000000..1e73b237ee --- /dev/null +++ b/src/app/auth/reset-password/reset-password.page.scss @@ -0,0 +1,174 @@ +@import '../../../theme/colors.scss'; + +.forgot-password { + height: 100%; + margin-top: 18px; + padding-bottom: 20px; + + &__form-container { + display: flex; + flex-direction: column; + padding: 24px 20px; + justify-content: space-between; + height: 100%; + } + + &__page-header { + font-size: 20px; + margin: 0 0 10px; + position: relative; + margin-bottom: 6px; + font-weight: 500; + } + + &__disabled { + opacity: 0.2; + } + + &__sub-header { + font-size: 14px; + margin: 0 0 10px; + position: relative; + margin-bottom: 24px; + color: $black-light; + } + + &__error-message { + color: $red; + display: flex; + align-items: center; + } + + &__input-container { + &__label { + margin: 0 8px 0 0; + font-size: 12px; + color: $black-light; + line-height: 1.3; + font-weight: 400; + } + + &__input { + border: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.3; + color: $blue-black; + width: 100%; + padding: 6px 0; + border-bottom: 1px solid $grey-lighter; + } + + &__input:focus { + border-bottom: 1px solid $blue-black; + } + } + + &__content-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 2; + } + + &__arrow-icon { + fill: $pure-white; + margin-right: 8px; + } + + &__cta-text { + font-size: 14px; + font-weight: 500; + } + + &__cta { + margin: 0 20px 24px 20px; + } + + &__cta-content { + display: flex; + align-items: center; + justify-content: center; + } + + &__success-message { + height: 100%; + display: flex; + flex-direction: column; + } + + &__success-icon-container { + width: 60px; + height: 60px; + border-radius: 8px; + background: $success-bg; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + } + + &__success-icon { + width: 45px; + height: 45px; + fill: $green; + } + + &__text { + display: flex; + align-items: center; + gap: 8px; + flex-direction: column; + } + + &__header { + color: $black; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: normal; + height: 26px; + margin: 0; + } + + &__resend-text { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + color: $black-light; + font-weight: 500; + gap: 2px; + + &__resend-link { + color: $brand-primary; + } + + &__spinner-icon { + color: $brand-primary; + height: 12px; + } + } + + &__content { + color: $black-light; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 1.25; + height: 36px; + width: 274px; + + &__reset-email { + font-weight: 500; + } + } + + &__back-icon { + margin-bottom: 16px; + width: 28px; + height: 28px; + } +} diff --git a/src/app/auth/reset-password/reset-password.page.spec.ts b/src/app/auth/reset-password/reset-password.page.spec.ts index 55a30ea298..a1d25fbc7d 100644 --- a/src/app/auth/reset-password/reset-password.page.spec.ts +++ b/src/app/auth/reset-password/reset-password.page.spec.ts @@ -1,54 +1,40 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IonicModule } from '@ionic/angular'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { ResetPasswordPage } from './reset-password.page'; +import { ReactiveFormsModule, FormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { IonicModule } from '@ionic/angular'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; -import { Router, RouterModule } from '@angular/router'; -import { Location } from '@angular/common'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; import { PageState } from 'src/app/core/models/page-state.enum'; +import { getElementRef } from 'src/app/core/dom-helpers'; +import { DebugElement } from '@angular/core'; import { of, throwError } from 'rxjs'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; describe('ResetPasswordPage', () => { let component: ResetPasswordPage; let fixture: ComponentFixture; let router: jasmine.SpyObj; let routerAuthService: jasmine.SpyObj; - let location: jasmine.SpyObj; - - @Component({ - selector: 'app-send-email', - template: '
', - }) - class MockSendEmailComponent { - @Input() title: string; - - @Input() content: string; - - @Input() subcontent: string; - - @Input() ctaText: string; - - @Input() successTitle: string; - - @Input() successContent: string; - - @Input() sendEmailPageState: PageState; - - @Input() isLoading: boolean; - - @Output() sendEmail = new EventEmitter(); - } + let matSnackBar: jasmine.SpyObj; + let snackbarPropertiesService: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; + let formBuilder: jasmine.SpyObj; + let fb: FormBuilder; beforeEach(waitForAsync(() => { const routerSpy = jasmine.createSpyObj('Router', ['navigate']); - const locationSpy = jasmine.createSpyObj('Location', ['path']); const routerAuthServiceSpy = jasmine.createSpyObj('RouterAuthService', ['sendResetPassword']); + const matSnackBarSpy = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']); + const snackbarPropertiesServiceSpy = jasmine.createSpyObj('SnackbarPropertiesService', ['setSnackbarProperties']); TestBed.configureTestingModule({ - declarations: [ResetPasswordPage, MockSendEmailComponent], + declarations: [ResetPasswordPage], imports: [IonicModule.forRoot(), RouterTestingModule, RouterModule, FormsModule, ReactiveFormsModule], providers: [ + FormBuilder, { provide: RouterAuthService, useValue: routerAuthServiceSpy, @@ -58,8 +44,16 @@ describe('ResetPasswordPage', () => { useValue: routerSpy, }, { - provide: Location, - useValue: locationSpy, + provide: MatSnackBar, + useValue: matSnackBarSpy, + }, + { + provide: SnackbarPropertiesService, + useValue: snackbarPropertiesServiceSpy, + }, + { + provide: ActivatedRoute, + useValue: { snapshot: { params: { email: 'aastha.b@fyle.in' } } }, }, ], }).compileComponents(); @@ -67,12 +61,18 @@ describe('ResetPasswordPage', () => { fixture = TestBed.createComponent(ResetPasswordPage); component = fixture.componentInstance; router = TestBed.inject(Router) as jasmine.SpyObj; - location = TestBed.inject(Location) as jasmine.SpyObj; routerAuthService = TestBed.inject(RouterAuthService) as jasmine.SpyObj; + matSnackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj; + snackbarPropertiesService = TestBed.inject(SnackbarPropertiesService) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + fb = TestBed.inject(FormBuilder); + component.fg = fb.group({ + email: [Validators.compose([Validators.required, Validators.pattern('\\S+@\\S+\\.\\S{2,}')])], + }); fixture.detectChanges(); })); - it('should create', () => { + it('should create the component', () => { expect(component).toBeTruthy(); }); @@ -81,15 +81,89 @@ describe('ResetPasswordPage', () => { expect(component.currentPageState).toEqual(PageState.notSent); }); + describe('template', () => { + it('should render the form in "notSent" state', () => { + component.currentPageState = component.PageState.notSent; + fixture.detectChanges(); + + const formElement = fixture.debugElement.query(By.css('.forgot-password__form-container')); + expect(formElement).toBeTruthy(); + }); + + it('should render the success message in "success" state', () => { + component.currentPageState = component.PageState.success; + component.resetEmail = 'test@example.com'; + fixture.detectChanges(); + + const successMessageElement = fixture.debugElement.query(By.css('.forgot-password__success-message')); + expect(successMessageElement).toBeTruthy(); + + const emailElement = fixture.debugElement.query(By.css('.forgot-password__content__reset-email')); + expect(emailElement.nativeElement.textContent).toContain('test@example.com'); + }); + + it('should display validation error for invalid email input', () => { + component.currentPageState = PageState.notSent; + const emailControl = component.fg.controls.email; + emailControl.setValue('invalid-email'); + emailControl.markAsTouched(); + fixture.detectChanges(); + + const errorElement = getElementRef(fixture, '.forgot-password__error-message'); + expect(errorElement.nativeElement.textContent).toContain(' Enter an email address. '); + }); + + it('should call sendResetLink with correct email when button is clicked', () => { + component.currentPageState = PageState.notSent; + spyOn(component, 'sendResetLink'); + component.fg.controls.email.setValue('test@example.com'); + fixture.detectChanges(); + + const buttonElement = fixture.debugElement.query(By.css('ion-button')); + buttonElement.triggerEventHandler('click', null); + + expect(component.sendResetLink).toHaveBeenCalledWith('test@example.com'); + }); + + it('should display resend link if email is not sent', () => { + component.isEmailSentOnce = false; + component.isLoading = false; + component.currentPageState = component.PageState.success; + component.resetEmail = 'test@example.com'; + fixture.detectChanges(); + + const resendLink = fixture.debugElement.query(By.css('.forgot-password__resend-text__resend-link')); + expect(resendLink).toBeTruthy(); + }); + + it('should hide resend link and show spinner when loading', () => { + component.isEmailSentOnce = false; + component.isLoading = true; + component.currentPageState = component.PageState.success; + fixture.detectChanges(); + + const resendLink = getElementRef(fixture, '.forgot-password__resend-text__resend-link') as DebugElement; + expect(resendLink).toBeFalsy(); + + const spinner = getElementRef(fixture, 'ion-spinner') as DebugElement; + expect(spinner).toBeTruthy(); + }); + }); + describe('sendResetLink():', () => { - it('should send reset password link, change loading and page state', () => { + beforeEach(() => { + component.currentPageState = PageState.success; + }); + + it('should send reset password link, change loading and page state to success', fakeAsync(() => { routerAuthService.sendResetPassword.and.returnValue(of({})); const email = 'jay.b@fyle.in'; component.sendResetLink(email); - expect(component.isLoading).toEqual(false); + tick(); + expect(component.isLoading).toBeFalse(); expect(component.currentPageState).toEqual(PageState.success); - }); + })); it('should send reset password link, change loading and page state', () => { routerAuthService.sendResetPassword.and.returnValue(throwError(() => new Error('Error message'))); @@ -97,28 +171,43 @@ describe('ResetPasswordPage', () => { const email = 'jay.b@fyle.in'; component.sendResetLink(email); - expect(component.isLoading).toEqual(true); + expect(component.isLoading).toBeFalse(); expect(component.handleError).toHaveBeenCalledTimes(1); }); }); - describe('handleError():', () => { - it('should navigate to disabled auth', () => { - component.handleError({ - status: 422, - message: 'Error message', - }); + describe('handleError(): ', () => { + it('should navigate to disabled page on 422 error', () => { + const error = { status: 422 }; + component.handleError(error); - expect(router.navigate).toHaveBeenCalledOnceWith(['/', 'auth', 'disabled']); + expect(router.navigate).toHaveBeenCalledWith(['/', 'auth', 'disabled']); }); - it('should change page state if auth not disabled', () => { - component.handleError({ - status: 400, - message: 'Error message', - }); + it('should display error message on other errors', () => { + const error = { status: 401 }; + const props = { + panelClass: ['msb-failure'], + }; + + matSnackBar.openFromComponent.and.callThrough(); - expect(component.currentPageState).toEqual(PageState.failure); + component.handleError(error); + expect(matSnackBar.openFromComponent).toHaveBeenCalledOnceWith(ToastMessageComponent, { + ...props, + panelClass: ['msb-failure'], + }); + expect(snackbarPropertiesService.setSnackbarProperties).toHaveBeenCalledTimes(1); }); }); + + it('onGotoSignInClick(): should navigate to sign-in page', () => { + component.onGotoSignInClick(); + expect(router.navigate).toHaveBeenCalledWith([ + '/', + 'auth', + 'sign_in', + { email: component.fg.controls.email.value }, + ]); + }); }); diff --git a/src/app/auth/reset-password/reset-password.page.ts b/src/app/auth/reset-password/reset-password.page.ts index 09d288f93d..e9bac076e7 100644 --- a/src/app/auth/reset-password/reset-password.page.ts +++ b/src/app/auth/reset-password/reset-password.page.ts @@ -1,43 +1,96 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { tap } from 'rxjs/operators'; +import { Component } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { finalize } from 'rxjs/operators'; import { RouterAuthService } from 'src/app/core/services/router-auth.service'; import { PageState } from 'src/app/core/models/page-state.enum'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service'; +import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component'; @Component({ selector: 'app-reset-password', templateUrl: './reset-password.page.html', + styleUrls: ['./reset-password.page.scss'], }) -export class ResetPasswordPage implements OnInit { +export class ResetPasswordPage { currentPageState: PageState; isLoading = false; - constructor(private routerAuthService: RouterAuthService, private router: Router) {} + fg: FormGroup; - ionViewWillEnter() { + resetEmail: string; + + isEmailSentOnce: boolean; + + PageState: typeof PageState = PageState; + + constructor( + private formBuilder: FormBuilder, + private routerAuthService: RouterAuthService, + private router: Router, + private activatedRoute: ActivatedRoute, + private matSnackBar: MatSnackBar, + private snackbarProperties: SnackbarPropertiesService + ) {} + + ionViewWillEnter(): void { this.currentPageState = PageState.notSent; + const email = (this.activatedRoute.snapshot.params.email as string) || ''; + this.fg = this.formBuilder.group({ + email: [email, Validators.compose([Validators.required, Validators.pattern('\\S+@\\S+\\.\\S{2,}')])], + }); } - ngOnInit() {} - - sendResetLink(email: string) { + sendResetLink(email: string): void { this.isLoading = true; + this.resetEmail = email; + + if (this.currentPageState === PageState.success) { + this.isEmailSentOnce = true; + } this.routerAuthService .sendResetPassword(email) - .pipe(tap(() => (this.isLoading = false))) + .pipe( + finalize(() => { + this.isLoading = false; + }) + ) .subscribe({ - next: () => (this.currentPageState = PageState.success), - error: (err) => this.handleError(err), + next: () => { + this.currentPageState = PageState.success; + if (this.isEmailSentOnce) { + const toastMessageData = { + message: ' Password recovery email sent successfully.', + }; + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarProperties.setSnackbarProperties('success', toastMessageData), + panelClass: ['msb-success'], + }); + } + }, + error: (err: { status: number }) => this.handleError(err), }); } - handleError(err) { + handleError(err: { status: number }): void { if (err.status === 422) { this.router.navigate(['/', 'auth', 'disabled']); } else { - this.currentPageState = PageState.failure; + const toastMessageData = { + message: 'Something went wrong. Please try after some time.', + }; + + this.matSnackBar.openFromComponent(ToastMessageComponent, { + ...this.snackbarProperties.setSnackbarProperties('failure', toastMessageData), + panelClass: ['msb-failure'], + }); } } + + onGotoSignInClick(): void { + this.router.navigate(['/', 'auth', 'sign_in', { email: this.fg.controls.email.value as string }]); + } } diff --git a/src/app/auth/sign-in/error/error.component.html b/src/app/auth/sign-in/error/error.component.html index d7d3157e3d..e3d0d09cf6 100644 --- a/src/app/auth/sign-in/error/error.component.html +++ b/src/app/auth/sign-in/error/error.component.html @@ -1,11 +1,18 @@
-
- {{ header }} +
+ +
+ {{ header }} +
-
+
- This email address will be temporarily locked after 5 unsuccessful login attempts. Try - resetting your + This email address will be temporarily locked after 5 unsuccessful login attempts. Would you like to try + resetting your password?
@@ -31,7 +38,7 @@
Your organization has restricted Fyle access to its corporate network.
-
- +
+ Try again
diff --git a/src/app/auth/sign-in/error/error.component.scss b/src/app/auth/sign-in/error/error.component.scss index bab19843e8..496b825bc9 100644 --- a/src/app/auth/sign-in/error/error.component.scss +++ b/src/app/auth/sign-in/error/error.component.scss @@ -1,29 +1,39 @@ -$details-color: #ababab; +@import '../../../../theme/colors.scss'; .error-internal { - &--header { + &__header { font-size: 20px; padding: 16px; - font-weight: 700; - } - - &--details { - font-size: 16px; - padding: 0 16px; font-weight: 500; - color: $details-color; - } + display: flex; + flex-direction: row; + border-bottom: 1px solid $grey-lighter; + gap: 12px; - &--primary-cta { - padding: 16px; - .mat-button-base { + &__header-text { + align-items: center; + justify-content: center; width: 100%; - font-weight: 700; - min-height: 47px; + display: flex; + flex-direction: row; } } - &--redirect { + &__close-icon { + align-self: flex-start; + } + + &__primary-cta { + padding: 0 16px 16px; + } + + &__details { + font-size: 14px; + padding: 20px 16px; + color: $black; + } + + &__redirect { text-decoration: none; } } diff --git a/src/app/auth/sign-in/error/error.component.spec.ts b/src/app/auth/sign-in/error/error.component.spec.ts index 2771080c02..f92b180191 100644 --- a/src/app/auth/sign-in/error/error.component.spec.ts +++ b/src/app/auth/sign-in/error/error.component.spec.ts @@ -35,11 +35,11 @@ describe('ErrorComponent', () => { }); it('should have a default header', () => { - expect(component.header).toEqual('Account does not Exist'); + expect(component.header).toEqual('Account does not exist'); }); - it('tryAgainClicked(): should dismiss the popover on try again button click', async () => { - const tryAgainBtn = getElementBySelector(fixture, '.error-internal--primary-cta button') as HTMLButtonElement; + it('closePopover(): should dismiss the popover on try again button click', async () => { + const tryAgainBtn = getElementBySelector(fixture, '.error-internal__primary-cta ion-button') as HTMLButtonElement; click(tryAgainBtn); fixture.detectChanges(); await fixture.whenStable(); @@ -59,10 +59,10 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 401 and data is present', () => { component.error = { status: 401, data: { message: 'Invalid email or password' } }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); - const resetLink = getElementBySelector(fixture, '.error-internal--redirect'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); + const resetLink = getElementBySelector(fixture, '.error-internal__redirect'); expect(getTextContent(errorMessage)).toContain( - 'This email address will be temporarily locked after 5 unsuccessful login attempts. Try resetting your password?' + 'This email address will be temporarily locked after 5 unsuccessful login attempts. Would you like to try resetting your password?' ); expect(resetLink).toBeTruthy(); }); @@ -70,7 +70,7 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 400', () => { component.error = { status: 400 }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); expect(getTextContent(errorMessage)).toContain( 'Your account is not verified. Please request a verification link, if required' ); @@ -79,7 +79,7 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 500', () => { component.error = { status: 500 }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); const supportLink = getElementBySelector(fixture, 'a'); expect(getTextContent(errorMessage)).toContain( 'Please retry in a while. Send us a note to support@fylehq.com if the problem persists.' @@ -90,7 +90,7 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 433', () => { component.error = { status: 433 }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); expect(getTextContent(errorMessage)).toContain( 'This email address is locked temporarily, as there are too many unsuccessful login attempts recently. Please retry later.' ); @@ -99,7 +99,7 @@ describe('ErrorComponent', () => { it('should display the correct error message for status 401 and no data or message is present', () => { component.error = { status: 401 }; fixture.detectChanges(); - const errorMessage = getElementBySelector(fixture, '.error-internal--details'); + const errorMessage = getElementBySelector(fixture, '.error-internal__details'); expect(getTextContent(errorMessage)).toContain( 'Your organization has restricted Fyle access to its corporate network.' ); diff --git a/src/app/auth/sign-in/error/error.component.ts b/src/app/auth/sign-in/error/error.component.ts index a4a2775bda..bdbd7fc782 100644 --- a/src/app/auth/sign-in/error/error.component.ts +++ b/src/app/auth/sign-in/error/error.component.ts @@ -1,27 +1,24 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { PopoverController } from '@ionic/angular'; import { Router } from '@angular/router'; -import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-error', templateUrl: './error.component.html', styleUrls: ['./error.component.scss'], }) -export class ErrorComponent implements OnInit { - @Input() header = 'Account does not Exist'; +export class ErrorComponent { + @Input() header = 'Account does not exist'; @Input() error; constructor(private popoverController: PopoverController, private router: Router) {} - ngOnInit() {} - - async tryAgainClicked() { + async closePopover(): Promise { await this.popoverController.dismiss(); } - async routeTo(route: string[]) { + async routeTo(route: string[]): Promise { this.router.navigate(route); await this.popoverController.dismiss(); } diff --git a/src/app/auth/sign-in/sign-in-page-state.enum.ts b/src/app/auth/sign-in/sign-in-page-state.enum.ts new file mode 100644 index 0000000000..ac16d4b294 --- /dev/null +++ b/src/app/auth/sign-in/sign-in-page-state.enum.ts @@ -0,0 +1,5 @@ +export enum SignInPageState { + SELECT_SIGN_IN_METHOD, // Google Sign In and normal sign in redirection from here + ENTER_EMAIL, // user can enter email and proceed to next step, SSO flow begins after this step + ENTER_PASSWORD, // user can enter their password for login here +} diff --git a/src/app/auth/sign-in/sign-in.page.html b/src/app/auth/sign-in/sign-in.page.html index e03001cd91..0fbb779586 100644 --- a/src/app/auth/sign-in/sign-in.page.html +++ b/src/app/auth/sign-in/sign-in.page.html @@ -1,125 +1,160 @@ -