Skip to content

Commit

Permalink
DON-1112: Add user-friendly validation for amount errors on regular g…
Browse files Browse the repository at this point in the history
…iving form
  • Loading branch information
bdsl committed Jan 10, 2025
1 parent 29c3dd2 commit bb8a8c7
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 7 deletions.
13 changes: 11 additions & 2 deletions src/app/regular-giving/regular-giving.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,22 @@
</div>
<p>This amount will be taken from your account today and once every month in future</p>

@if (amountErrorMessage) {
<div
class="error"
aria-live="polite"
>
{{ this.amountErrorMessage }}
</div>
}

<div style="text-align: center">
<button style="width: 40%;"
type="button"
class="continue b-w-100 b-rt-0"
mat-raised-button
color="primary"
(click)="next()"
(click)="this.selectStep(1)"
>Continue
</button>
</div>
Expand Down Expand Up @@ -115,7 +124,7 @@
class="continue b-w-100 b-rt-0"
mat-raised-button
color="primary"
(click)="next()"
(click)="this.selectStep(2)"
>Continue
</button>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/app/regular-giving/regular-giving.component.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@use '@angular/material' as mat;
@import '../../abstract';

// code below duplicated from donation-start-container.component.scss
Expand Down Expand Up @@ -130,3 +131,8 @@ table#personal-details, table#paymentMethods {
vertical-align: bottom;
}
}

.error, .stripeError {
color: mat.m2-get-color-from-palette($donate-warn);
margin: 1rem 0;
}
67 changes: 62 additions & 5 deletions src/app/regular-giving/regular-giving.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {Campaign} from "../campaign.model";
import {ComponentsModule} from "@biggive/components-angular";
Expand All @@ -24,6 +24,11 @@ import {ConfirmationToken, StripeElements, StripePaymentElement} from "@stripe/s
import {DonationService, StripeCustomerSession} from "../donation.service";
import {MatProgressSpinner} from "@angular/material/progress-spinner";

// for now min & max are hard-coded, will change to be based on a field on
// the campaign.
const maxAmount = 500;
const minAmount = 1;

@Component({
selector: 'app-regular-giving',
standalone: true,
Expand All @@ -41,7 +46,7 @@ import {MatProgressSpinner} from "@angular/material/progress-spinner";
templateUrl: './regular-giving.component.html',
styleUrl: './regular-giving.component.scss'
})
export class RegularGivingComponent implements OnInit {
export class RegularGivingComponent implements OnInit, AfterViewInit {
protected campaign: Campaign;
mandateForm: FormGroup;
@ViewChild('stepper') private stepper: MatStepper;
Expand All @@ -60,6 +65,8 @@ export class RegularGivingComponent implements OnInit {
private stripeCustomerSession: StripeCustomerSession | undefined;
protected submitting: boolean = false;

protected amountErrorMessage: string | undefined;

constructor(
private route: ActivatedRoute,
private formBuilder: FormBuilder,
Expand Down Expand Up @@ -97,9 +104,8 @@ export class RegularGivingComponent implements OnInit {
this.mandateForm = this.formBuilder.group({
donationAmount: ['', [
requiredNotBlankValidator,
getCurrencyMinValidator(1), // for now min & max are hard-coded, will change to be based on a field on
// the campaign.
getCurrencyMaxValidator(500),
getCurrencyMinValidator(minAmount),
getCurrencyMaxValidator(maxAmount),
Validators.pattern('^\\s*[£$]?[0-9]+?(\\.00)?\\s*$'),
]],
billingPostcode: [this.donorAccount.billingPostCode,
Expand All @@ -120,6 +126,23 @@ export class RegularGivingComponent implements OnInit {
.catch(console.error);
}

ngAfterViewInit() {
// It seems the stepper doesn't provide a nice way to let us intercept each request to change step. Monkey-patching
// the select function which is called when the user clicks a step heading, to let us check that all previous
// steps have been completed correctly, and then either proceed to the chosen step or display an error message.

// Based on https://stackoverflow.com/a/62787311/2526181 - I'm not 100% sure why it's wrapped in setTimeout but
// maybe returning immediately from ngAfterViewInit improves performance.

setTimeout(() => {
this.stepper.steps.forEach((step, stepIndex) => {
step.select = () => {
this.selectStep(stepIndex);
};
});
});
}

async interceptSubmitAndProceedInstead(event: Event) {
event.preventDefault();
await this.next();
Expand Down Expand Up @@ -246,4 +269,38 @@ export class RegularGivingComponent implements OnInit {
this.stripePaymentElement.on('change', () => {}); // @todo-regular-giving: implement card change handler
}
}

protected selectStep(stepIndex: number) {

const donationAmountErrors = this.mandateForm.controls.donationAmount!.errors;

if (donationAmountErrors) {
console.error(donationAmountErrors);
for (const [key] of Object.entries(donationAmountErrors)) {
switch (key) {
case 'required':
this.amountErrorMessage = "Please enter your monthly donation amount";
break;
case 'pattern':
this.amountErrorMessage = `Please enter a whole number of £ without commas`;
break;
case 'max':
case 'min':
this.amountErrorMessage = `Please select an amount between £${minAmount} and £${maxAmount}`;
break;
default:
this.amountErrorMessage = 'Unexpected donation amount error';
console.error({donationAmountErrors});
break;
}
}
this.toast.showError(this.amountErrorMessage!);

return;
} else {
this.amountErrorMessage = undefined;
}

this.stepper.selected = this.stepper.steps.get(stepIndex);
}
}

0 comments on commit bb8a8c7

Please sign in to comment.