Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/app/auth/components/verify/verify.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { HttpService } from '@shared/services/http/http.service';
import { ApiService } from '@shared/services/api/api.service';
import { ActivatedRoute, Router } from '@angular/router';
import { EventService } from '@shared/services/event/event.service';
import { MessageService } from '@shared/services/message/message.service';

const defaultAuthData = require('@root/test/responses/auth.verify.unverifiedEmail.success.json');

Expand Down Expand Up @@ -169,6 +170,40 @@ describe('VerifyComponent', () => {
req.flush(verifyPhoneResponse);
});

it('reports a phone code that fails to send on arrival', async () => {
const unverifiedPhoneData = require('@root/test/responses/auth.verify.unverifiedPhone.success.json');
await init(unverifiedPhoneData, { sendSms: true });

const showErrorSpy = spyOn(TestBed.inject(MessageService), 'showError');
const request = httpMock.expectOne((candidate) =>
candidate.url.includes('resendTextCreatedAccount'),
);
request.flush({ isSuccessful: false, isSystemUp: true, Results: [] });
await new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});

expect(showErrorSpy).toHaveBeenCalled();
});

it('reports a failed send that is not a response object', async () => {
const unverifiedPhoneData = require('@root/test/responses/auth.verify.unverifiedPhone.success.json');
await init(unverifiedPhoneData, { sendSms: true });

const showErrorSpy = spyOn(TestBed.inject(MessageService), 'showError');
const request = httpMock.expectOne((candidate) =>
candidate.url.includes('resendTextCreatedAccount'),
);
request.error(new ProgressEvent('network down'));
await new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});

expect(showErrorSpy.calls.mostRecent().args[0].message).toBe(
'error.generic.internal',
);
});

it('should show CAPTCHA before verifying phone', async () => {
const unverifiedPhoneData = require('@root/test/responses/auth.verify.unverifiedPhone.success.json');
await init(unverifiedPhoneData, { sendSms: true });
Expand Down
16 changes: 15 additions & 1 deletion src/app/auth/components/verify/verify.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ export class VerifyComponent implements OnInit {

if (this.canSendCodes('phone')) {
if (queryParams.sendSms) {
this.accountService.resendPhoneVerification();
this.accountService
.resendPhoneVerification()
.catch((response: unknown) => {
this.showCodeSendError(response);
});
}
}
}
Expand Down Expand Up @@ -233,6 +237,16 @@ export class VerifyComponent implements OnInit {
});
}

private showCodeSendError(response: unknown): void {
this.message.showError({
message:
response instanceof AuthResponse
? response.getMessage()
: 'error.generic.internal',
translate: true,
});
}

public resolveCaptcha(response: string): void {
this.captchaPassed = true;
if (this.canSendCodes('phone')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
</div>
</div>
<div class="settings-group">
<p class="settings-group-note">
Permanent only supports US and Canada numbers.
</p>
<div class="settings-item">
<label
>Mobile Phone
Expand All @@ -50,6 +53,8 @@
[displayValue]="account.primaryPhone"
[noScroll]="true"
[emptyMessage]="'Mobile Phone Number'"
(editValueChange)="onPhoneNumberTyped($event)"
(editingChange)="onPhoneNumberEditingChange($event)"
(doneEditing)="onSaveProfileInfo('primaryPhone', $event)"
></pr-inline-value-edit>
</div>
Expand All @@ -73,4 +78,12 @@
}
</div>
</div>
@if (phoneNumberWarning) {
<div class="settings-item settings-item-note">
<span class="phone-number-warning">
<i class="material-icons">warning</i>
{{ phoneNumberWarning }}
</span>
</div>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,30 @@ p {
form {
@include tabbedDialogPanelForm;
}

.settings-item-note {
font-size: $font-size-sm;
padding-bottom: $grid-unit * 0.5;

@include desktop {
padding-left: 12rem;
}
}

.settings-group-note {
font-size: $font-size-sm;
color: $gray-dark;
padding: 0 0 $grid-unit * 0.5;
margin: 0;
}

.phone-number-warning {
color: $PR-yellow-700;
display: flex;
align-items: center;
gap: $grid-unit * 0.25;

i {
font-size: $mat-icon-sm;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,147 @@ describe('AccountSettingsComponent', () => {
}
});

it('warns about a number that arrived from the backend, without any editing', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

expect(instance.account).toBe(TestBed.inject(AccountService).getAccount());

instance.account.primaryPhone = '+44 20 7946 0958';
instance.account.phoneStatus = 'status.auth.unverified';
fixture.detectChanges();

expect(
ngMocks.find('.phone-number-warning').nativeElement.textContent,
).toContain('US and Canada');
});

it('renders no warning for a stored number the provider can reach', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

instance.account.primaryPhone = '+1 (202) 555-0147';
instance.account.phoneStatus = 'status.auth.unverified';
fixture.detectChanges();

expect(ngMocks.findAll('.phone-number-warning').length).toBe(0);
});

it('keeps the standing guidance separate from the warning', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

instance.account.primaryPhone = '416 555 0147';
instance.account.phoneStatus = 'status.auth.unverified';
fixture.detectChanges();

expect(
ngMocks.find('.settings-group-note').nativeElement.textContent,
).toContain('Permanent only supports US and Canada numbers.');

expect(ngMocks.findAll('.phone-number-warning').length).toBe(0);
});

it('warns in the same place once a typed number is long enough to judge', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

instance.account.phoneStatus = 'status.auth.unverified';
instance.onPhoneNumberEditingChange(true);
instance.onPhoneNumberTyped('+40 748');
fixture.detectChanges();

expect(ngMocks.findAll('.phone-number-warning').length).toBe(0);

instance.onPhoneNumberTyped('+40748498404');
fixture.detectChanges();

expect(
ngMocks.find('.phone-number-warning').nativeElement.textContent,
).toContain('US and Canada');
});

it('stays quiet while a reachable number is being typed', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

instance.account.phoneStatus = 'status.auth.unverified';
instance.onPhoneNumberEditingChange(true);

['2', '202', '(202) 555', '(202) 555-014', '(202) 555-0147'].forEach(
(partial) => {
instance.onPhoneNumberTyped(partial);

expect(instance.phoneNumberWarning).withContext(partial).toBeNull();
},
);
});

it('falls back to the stored number once editing ends', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

instance.account.primaryPhone = '+44 20 7946 0958';
instance.account.phoneStatus = 'status.auth.unverified';

instance.onPhoneNumberEditingChange(true);
instance.onPhoneNumberTyped('(202) 555-0147');

expect(instance.phoneNumberWarning).toBeNull();

instance.onPhoneNumberEditingChange(false);

expect(instance.phoneNumberWarning).toContain('US and Canada');
});

it('does not warn about a number that has already verified', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

instance.account.primaryPhone = '+44 20 7946 0958';
instance.account.phoneStatus = 'status.auth.verified';

expect(instance.phoneNumberWarning).toBeNull();

instance.account.phoneStatus = 'status.auth.unverified';

expect(instance.phoneNumberWarning).toContain('US and Canada');
});

it('does not warn about a number that was cleared', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

instance.account.primaryPhone = '';

expect(instance.phoneNumberWarning).toBeNull();

instance.onPhoneNumberEditingChange(true);
instance.onPhoneNumberTyped('');

expect(instance.phoneNumberWarning).toBeNull();
});

it('still saves a number the provider cannot reach', async () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;

const accountUpdateSpy = spyOn(
TestBed.inject(ApiService).account,
'update',
).and.resolveTo(new AccountVO({}));
const errorMessageSpy = spyOn(TestBed.inject(MessageService), 'showError');

await instance.onSaveProfileInfo('primaryPhone', '+44 20 7946 0958');
await new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});

expect(accountUpdateSpy).toHaveBeenCalled();
expect(errorMessageSpy).not.toHaveBeenCalled();
expect(instance.account.primaryPhone).toBe('+44 20 7946 0958');
});

it('should disable "Verify Phone Number" button if primaryPhone is empty', () => {
const fixture = MockRender(AccountSettingsComponent);
const instance = fixture.point.componentInstance;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, signal } from '@angular/core';
import { AccountService } from '@shared/services/account/account.service';
import { AccountVO } from '@models';
import { ApiService } from '@shared/services/api/api.service';
Expand All @@ -8,6 +8,11 @@ import { FormInputSelectOption } from '@shared/components/form-input/form-input.
import { Router, ActivatedRoute } from '@angular/router';
import { EventService } from '@shared/services/event/event.service';
import { savePropertyOnAccount } from '@shared/services/account/account.service.helpers';
import {
UNSUPPORTED_PHONE_COUNTRY_NOTICE,
storedPhoneNumberNeedsCountryWarning,
typedPhoneNumberNeedsCountryWarning,
} from '@shared/utilities/phone-number';

@Component({
selector: 'pr-account-settings',
Expand All @@ -21,6 +26,8 @@ export class AccountSettingsComponent implements OnInit {
public states: FormInputSelectOption[];
public waiting = false;

private readonly phoneNumberBeingEdited = signal<string | null>(null);

constructor(
private accountService: AccountService,
private prConstants: PrConstantsService,
Expand Down Expand Up @@ -50,6 +57,30 @@ export class AccountSettingsComponent implements OnInit {
});
}

public get phoneNumberWarning(): string | null {
if (this.account.phoneStatus === 'status.auth.verified') {
return null;
}

const beingEdited = this.phoneNumberBeingEdited();
const needsWarning =
beingEdited === null
? storedPhoneNumberNeedsCountryWarning(this.account.primaryPhone)
: typedPhoneNumberNeedsCountryWarning(beingEdited);

return needsWarning ? UNSUPPORTED_PHONE_COUNTRY_NOTICE : null;
}

onPhoneNumberTyped(value: string) {
this.phoneNumberBeingEdited.set(value ?? '');
}

onPhoneNumberEditingChange(isEditing: boolean) {
if (!isEditing) {
this.phoneNumberBeingEdited.set(null);
}
}

async onSaveProfileInfo(prop: keyof AccountVO, value: string) {
savePropertyOnAccount(
this.account,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
name="text"
class="form-control"
[class.is-editing]="isEditing"
[(ngModel)]="editValue"
[ngModel]="editValue"
(ngModelChange)="onEditValueChange($event)"
#input
#model="ngModel"
[required]="required"
Expand Down
Loading
Loading