Skip to content

Commit

Permalink
Merge pull request #1339 from thebiggive/DON-819-payment-step
Browse files Browse the repository at this point in the history
DON-819: Show errors blocking progress from payment step
  • Loading branch information
bdsl authored Oct 10, 2023
2 parents d3eccc0 + 5511ef4 commit 0160492
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
import {PaymentReadinessTracker} from "./PaymentReadinessTracker";

describe('PaymentReadinessTracker', () => {
let paymentGroup: {valid: boolean, controls: {}}
beforeEach(() => {
paymentGroup = {valid: true, controls: {}};
})

it('Initially says we are not ready to progress from payment step', () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);

expect(sut.readyToProgressFromPaymentStep).toBeFalse();
})

it('Lists errors according to payment group controls.', () => {
const sut = new PaymentReadinessTracker(paymentGroup);
paymentGroup.controls = {
firstName: {errors: {required: true}},
lastName: {errors: {required: true}},
emailAddress: {errors: {required: true}},
billingPostcode: {errors: {required: true}},
}

expect(sut.getErrorsBlockingProgress()).toEqual([
'Please enter your first name.',
'Please enter your last name.',
'Please enter your email address.',
'Please enter your billing postcode.',
'Please complete your payment method.',
]);
})

it('Prompts to select saved card if deselected.', () => {
const sut = new PaymentReadinessTracker(paymentGroup);
sut.selectedSavedPaymentMethod();
sut.clearSavedPaymentMethod();

expect(sut.getErrorsBlockingProgress()).toEqual([
'Please complete your new payment method, or select a saved payment method.',
]);
})

it("Allows proceeding from payment step when a saved card is selected", () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);

sut.selectedSavedPaymentMethod();
expect(sut.readyToProgressFromPaymentStep).toBeTrue();
});

it("Blocks proceeding from payment step when a saved card is selected but payments group is invalid", () => {
const paymentGroup = {valid: true};
const sut = new PaymentReadinessTracker(paymentGroup);

sut.selectedSavedPaymentMethod();
Expand All @@ -24,49 +56,49 @@ describe('PaymentReadinessTracker', () => {
});

it("Allows proceeding from payment step when donor has credit", () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);
sut.donorHasFunds();
expect(sut.readyToProgressFromPaymentStep).toBeTrue();
});

it("Allows proceeding from payment step when donation credits are prepared", () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);
sut.donationFundsPrepared(1);
expect(sut.readyToProgressFromPaymentStep).toBeTrue();
});

it("Allows proceeding from payment step when a saved card is selected ", () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);

sut.selectedSavedPaymentMethod();
sut.onUseSavedCardChange(true);
expect(sut.readyToProgressFromPaymentStep).toBeTrue();
});

it("Blocks proceeding from payment step when a saved card is selected but not to be used", () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);

sut.selectedSavedPaymentMethod();
sut.onUseSavedCardChange(false);
expect(sut.readyToProgressFromPaymentStep).toBeFalse();
});

it("Allows proceeding from payment step when a complete payment card is given", () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);

sut.onStripeCardChange({complete: true});
expect(sut.readyToProgressFromPaymentStep).toBeTrue();
})

it("Blocks proceeding fromm payment step when an incomplete payment card is given", () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);

sut.onStripeCardChange({complete: false});
expect(sut.readyToProgressFromPaymentStep).toBeFalse();
})

it("Blocks proceeding from payment step when a payment method is selected then cleared", () => {
const sut = new PaymentReadinessTracker({valid: true});
const sut = new PaymentReadinessTracker(paymentGroup);

sut.selectedSavedPaymentMethod();
sut.clearSavedPaymentMethod();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,101 @@ export class PaymentReadinessTracker {
*/
private donorFunds: boolean = false;


/**
* Does the logged in donor have a saved card, even if they don't want to use it?
*/
private hasASavedCard: boolean = false;
constructor(
/**
* Payment group from the Material form. If Angular Material has a validation error then we're not going to be ready
* to proceed.
*/
private paymentGroup: { valid: boolean },
private paymentGroup: {
controls: {
[key: string]: {
errors: {
[key: string]: unknown;
} | null
}},

valid: boolean
}
) {
}

get readyToProgressFromPaymentStep(): boolean {
return this.getErrorsBlockingProgress().length === 0;
}

/**
* Returns an array of messages explaining why the donor cannot immediately progress past the payment step. Will
* be displayed to them so they can fix.
*/
public getErrorsBlockingProgress(): string[] {
const usingSavedCard = !!this.selectedSavedMethod && this.useSavedCard;
const atLeastOneWayOfPayingIsReady = this.donorFunds || usingSavedCard || this.paymentElementIsComplete;
const formHasNoValidationErrors = this.paymentGroup.valid;

return formHasNoValidationErrors && atLeastOneWayOfPayingIsReady
let paymentErrors: string[] = [];
if (!atLeastOneWayOfPayingIsReady) {
paymentErrors = [
this.hasASavedCard ?
"Please complete your new payment method, or select a saved payment method." :
"Please complete your payment method."
];
}

return [...this.humanReadableFormValidationErrors(), ...paymentErrors];
}

private humanReadableFormValidationErrors() {
const fieldNames = {
firstName: 'first name',
lastName: 'last name',
emailAddress: 'email address',
billingPostcode: 'billing postcode',
}

const errors = this.getFormValidationErrors().map((error) => {
const key = error.key as keyof typeof fieldNames;

const fieldName = fieldNames[key];

switch (error.error) {
case 'required':
return `Please enter your ${fieldName}.`;
case 'pattern':
return `Sorry, your ${fieldName} is not recognised - please enter a valid ${fieldName}.`;
default:
console.error(error);
return `Sorry, there is an error with the ${key} field.`;
}
});

if (errors.length === 0 && ! this.paymentGroup.valid ) {
// hopefully will never happen in prod.
console.error("Unexpected payment group error");
return ['Please check all fields in the payment section and correct any errors.'];
}

return errors;
}

getFormValidationErrors(): { key: string; error: string }[] {
const errors: {key: string, error: string}[] = [];

Object.entries(this.paymentGroup.controls).forEach(([key, control]) => {
const errorsFromThisControl = Object.entries(control.errors ?? []).filter(Boolean);

errorsFromThisControl.forEach(([errorType]) => errors.push({key: key, error: errorType}))
});

return errors;
}

selectedSavedPaymentMethod() {
this.selectedSavedMethod = true;
this.useSavedCard = true;
this.hasASavedCard = true;
}

donorHasFunds() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,18 +434,25 @@ <h3 class="b-rt-0 b-m-0 b-bold"><span class="span-hr"></span>How does Big Give u

</div>

<p *ngIf="stripeError" class="error" aria-live="assertive">
<p *ngIf="stripeError" class="error" aria-live="polite">
{{ stripeError }}
</p>

<p class="error" *ngIf="paymentStepErrors && don819FlagEnabled">
<!-- No need for aria-live on this because the errors will have already been in an aria-live toast pop.
They are here in addition to all the donor to review them in detail.
-->
{{paymentStepErrors}}
</p>

<div style="text-align: center">
<button
type="button"
class="continue b-w-100 b-rt-0"
mat-raised-button
color="primary"
[disabled]="! readyToProgressFromPaymentStep"
(click)="next()"
[disabled]="! readyToProgressFromPaymentStep && ! don819FlagEnabled"
(click)="continueFromPaymentStep()"
>Continue</button>
</div>
</mat-step>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
private stripeElements: StripeElements | undefined;
private selectedPaymentMethodType: string | undefined;
private paymentReadinessTracker: PaymentReadinessTracker;
public paymentStepErrors: string = "";

constructor(
public cardIconsService: CardIconsService,
Expand Down Expand Up @@ -2166,4 +2167,14 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
this.tipAmountField?.setValue(this.tipValue);
}

continueFromPaymentStep() {
if (! this.readyToProgressFromPaymentStep) {
this.paymentStepErrors = this.paymentReadinessTracker.getErrorsBlockingProgress().join(" ");
this.showErrorToast(this.paymentStepErrors);
return;
} else {
this.paymentStepErrors = "";
this.next()
}
}
}
2 changes: 1 addition & 1 deletion src/app/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {EnvironmentID} from "../environments/environment.interface";

export const flagsForEnvironment = (environmentId: EnvironmentID) => {
return {
don819FlagEnabled: (environmentId === 'development'),
don819FlagEnabled: (environmentId === 'development' || environmentId == 'staging'),
};
}

Expand Down

0 comments on commit 0160492

Please sign in to comment.