Skip to content

Commit

Permalink
Merge pull request #1821 from thebiggive/DON-1112-regular-giving-vali…
Browse files Browse the repository at this point in the history
…dation

DON-1112: Add user-friendly validation for amount errors on regular g…
  • Loading branch information
bdsl authored Jan 13, 2025
2 parents 637de53 + e3aa4c1 commit 1cf214c
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ import {IdentityService} from '../../identity.service';
import {ConversionTrackingService} from '../../conversionTracking.service';
import {PageMetaService} from '../../page-meta.service';
import {Person} from '../../person.model';
import {PostcodeService} from '../../postcode.service';
import {billingPostcodeRegExp, postcodeFormatHelpRegExp, postcodeRegExp, PostcodeService} from '../../postcode.service';
import {retryStrategy} from '../../observable-retry';
import {StripeService} from '../../stripe.service';
import {getStripeFriendlyError, StripeService} from '../../stripe.service';
import {getCurrencyMaxValidator} from '../../validators/currency-max';
import {getCurrencyMinValidator} from '../../validators/currency-min';
import {EMAIL_REGEXP} from '../../validators/patterns';
Expand Down Expand Up @@ -186,17 +186,6 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon

tipPercentage = 15;
tipValue: number | undefined;
/**
* Used just to take raw input and put together an all-caps, spaced UK postcode, assuming the
* input was valid (even if differently formatted). Loosely based on https://stackoverflow.com/a/10701634/2803757
* with an additional tweak to allow (and trim) surrounding spaces.
*/
private postcodeFormatHelpRegExp = new RegExp('^\\s*([A-Z]{1,2}\\d{1,2}[A-Z]?)\\s*(\\d[A-Z]{2})\\s*$');
// Based on the simplified pattern suggestions in https://stackoverflow.com/a/51885364/2803757
private postcodeRegExp = new RegExp('^([A-Z][A-HJ-Y]?\\d[A-Z\\d]? \\d[A-Z]{2}|GIR 0A{2})$');

// Intentionally looser to support most countries' formats.
private billingPostcodeRegExp = new RegExp('^[0-9a-zA-Z -]{2,8}$');

private idCaptchaCode?: string;
private stripeResponseErrorCode?: string; // stores error codes returned by Stripe after callout
Expand Down Expand Up @@ -777,7 +766,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
this.paymentReadinessTracker.onStripeCardChange(state);

if (state.error) {
this.stripeError = this.getStripeFriendlyError(state.error, 'card_change');
this.stripeError = getStripeFriendlyError(state.error, 'card_change');
this.toast.showError(this.stripeError);
this.stripeResponseErrorCode = state.error.code;
} else {
Expand Down Expand Up @@ -1439,70 +1428,17 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon

private handleStripeError(
error: StripeError | {message: string, code: string, decline_code?: string} | undefined,
context: string,
context: 'method_setup'| 'card_change'| 'confirm',
) {
this.submitting = false;
this.stripeError = this.getStripeFriendlyError(error, context);
this.stripeError = getStripeFriendlyError(error, context);
this.toast.showError(this.stripeError);
this.stripeResponseErrorCode = error?.code;

this.jumpToStep('Payment details');
this.goToFirstVisibleError();
}

/**
* @param error
* @param context 'method_setup', 'card_change' or 'confirm'.
*/
private getStripeFriendlyError(
error: StripeError | {message: string, code: string, decline_code?: string, description?: string} | undefined,
context: string,
): string {


let prefix = '';
switch (context) {
case 'method_setup':
prefix = 'Payment setup failed: ';
break;
case 'card_change':
prefix = 'Payment method update failed: ';
break;
case 'confirm':
prefix = 'Payment processing failed: ';
}

if (! error || (! error.message && ! error.code)) {
if (error && error.hasOwnProperty('description')) {
// @ts-ignore - not sure why TS doesn't recognise that it must have a description because I just checked
// with hasOwnProperty.
return `${prefix}${error!.description}`;
}
return `${prefix}Sorry, we encountered an error. Please try again in a moment or contact us if this message persists.`;
}

let friendlyError = error.message;

let customMessage = false;
if (error.code === 'card_declined' && error.decline_code === 'generic_decline') {
// Probably a custom Radar rule -> relatively likely to be an incorrect postcode.
friendlyError = `The payment was declined. Please ensure details provided (including postcode) match your card. Contact your bank or [email protected] if the problem persists.`;
customMessage = true;
}

if (error.code === 'card_declined' && error.decline_code === 'invalid_amount') {
// We've seen e.g. HSBC in Nov '23 decline large donations with this code.
friendlyError = 'The payment was declined. You might need to contact your bank before making a donation of this amount.';
customMessage = true;
}

if (customMessage && context === 'confirm') {
prefix = ''; // Don't show extra context info in the most common `context`, when showing our already-long custom copy.
}

return `${prefix}${friendlyError}`;
}

private isBillingPostcodePossiblyInvalid() {
return this.stripeResponseErrorCode === 'card_declined';
}
Expand Down Expand Up @@ -2016,7 +1952,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon

// Uppercase it in-place, then we can use patterns that assume upper case.
homePostcode = homePostcode.toUpperCase();
var parts = homePostcode.match(this.postcodeFormatHelpRegExp);
var parts = homePostcode.match(postcodeFormatHelpRegExp);
if (parts === null) {
// If the input doesn't even match the much looser pattern here, it's going to fail
// the validator check in a moment and there's nothing we can/should do with it
Expand Down Expand Up @@ -2113,7 +2049,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon

return [
requiredNotBlankValidator,
Validators.pattern(this.postcodeRegExp),
Validators.pattern(postcodeRegExp),
];
}

Expand All @@ -2128,7 +2064,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
]);
this.paymentGroup.controls.billingPostcode!.setValidators([
requiredNotBlankValidator,
Validators.pattern(this.billingPostcodeRegExp),
Validators.pattern(billingPostcodeRegExp),
]);
}

Expand Down
12 changes: 12 additions & 0 deletions src/app/postcode.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ import { environment } from '../environments/environment';
import { GiftAidAddress } from './gift-aid-address.model';
import { GiftAidAddressSuggestion } from './gift-aid-address-suggestion.model';

/**
* Used just to take raw input and put together an all-caps, spaced UK postcode, assuming the
* input was valid (even if differently formatted). Loosely based on https://stackoverflow.com/a/10701634/2803757
* with an additional tweak to allow (and trim) surrounding spaces.
*/
export const postcodeFormatHelpRegExp = new RegExp('^\\s*([A-Z]{1,2}\\d{1,2}[A-Z]?)\\s*(\\d[A-Z]{2})\\s*$');
// Based on the simplified pattern suggestions in https://stackoverflow.com/a/51885364/2803757
export const postcodeRegExp = new RegExp('^([A-Z][A-HJ-Y]?\\d[A-Z\\d]? \\d[A-Z]{2}|GIR 0A{2})$');

// Intentionally looser to support most countries' formats.
export const billingPostcodeRegExp = new RegExp('^[0-9a-zA-Z -]{2,8}$');

@Injectable({
providedIn: 'root',
})
Expand Down
33 changes: 30 additions & 3 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 @@ -109,13 +118,22 @@
</biggive-text-input>
}

@if (paymentInfoErrorMessage) {
<div
class="error"
aria-live="polite"
>
{{ paymentInfoErrorMessage }}
</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(2)"
>Continue
</button>
</div>
Expand Down Expand Up @@ -150,7 +168,16 @@
<mat-icon class="b-va-bottom" aria-hidden="false" aria-label="Open in new tab">open_in_new</mat-icon>
read our Privacy Statement.</a>
</p>
<p>(todo: add link to regular giving terms & conditions)</p>
<p>(todo-regular-giving: add link to regular giving terms & conditions)</p>

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

@if (!submitting) {
<button
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;
}
Loading

0 comments on commit 1cf214c

Please sign in to comment.