From 8d0b5abae8aa1752a4bac8ab6ebc1cd98de8d81a Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Wed, 8 Jan 2025 11:55:18 +0000 Subject: [PATCH 1/2] DON-998: Send Stripe confirmation token when setting up first regular giving mandate for each account --- .../donation-start-form.component.ts | 5 +- .../regular-giving.component.html | 101 +++++++++++------- .../regular-giving.component.ts | 53 +++++---- src/app/regularGiving.service.ts | 7 ++ src/app/stripe.service.ts | 9 +- 5 files changed, 112 insertions(+), 63 deletions(-) diff --git a/src/app/donation-start/donation-start-form/donation-start-form.component.ts b/src/app/donation-start/donation-start-form/donation-start-form.component.ts index 722d85be5..f931f0bec 100644 --- a/src/app/donation-start/donation-start-form/donation-start-form.component.ts +++ b/src/app/donation-start/donation-start-form/donation-start-form.component.ts @@ -947,7 +947,10 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon let confirmationTokenResult: ConfirmationTokenResult | undefined; let confirmationToken: ConfirmationToken | undefined; let paymentMethod: PaymentMethod | undefined; - confirmationTokenResult = await this.stripeService.prepareConfirmationTokenFromPaymentElement(this.donation, this.stripeElements); + confirmationTokenResult = await this.stripeService.prepareConfirmationTokenFromPaymentElement( + {countryCode: this.donation.countryCode!, billingPostalAddress: this.donation.billingPostalAddress!}, + this.stripeElements + ); confirmationToken = confirmationTokenResult.confirmationToken; if (confirmationToken || paymentMethod) { diff --git a/src/app/regular-giving/regular-giving.component.html b/src/app/regular-giving/regular-giving.component.html index 752aeacd0..1d40385f8 100644 --- a/src/app/regular-giving/regular-giving.component.html +++ b/src/app/regular-giving/regular-giving.component.html @@ -1,11 +1,12 @@
- @if(!campaign.isRegularGiving) { -
-

- Sorry, campaign {{ campaign.title }} is not a regular giving campaign.

-

To make a one-off donation to {{ campaign.charity.name }} please visit the regular campaign page. -

-
+ @if (!campaign.isRegularGiving) { +
+

+ Sorry, campaign {{ campaign.title }} is not a regular giving campaign.

+

To make a one-off donation to {{ campaign.charity.name }} please visit the regular campaign page. +

+
} @else {
@@ -38,7 +39,7 @@ slot="label" for="donationAmount" > - Monthly donation to {{campaign.charity.name}} + Monthly donation to {{ campaign.charity.name }} Continue + >Continue +
@@ -72,34 +74,40 @@ @if (donorAccount.regularGivingPaymentMethod) { -

- Your existing regular giving payment method will be used for these donations. +

+ Your existing regular giving payment method {{ donorAccount.regularGivingPaymentMethod }} + will be used for these donations. To change this please visit your account page + + (todo-regular-giving: replace this paragraph with an element showing details of the saved method)

} @else { - Payment Method + Payment Method +
- } -

+

- - + + - - - - + + + + + }

+ >Continue +
@@ -122,15 +131,16 @@ - + - +
Name{{donor.first_name}} {{donor.last_name}}{{ donor.first_name }} {{ donor.last_name }}
Email{{donor.email_address}}{{ donor.email_address }}
-

By clicking on the Start regular giving now button, you agree to +

By clicking on the Start regular giving now + button, you agree to Big Give's Terms and Conditions and Privacy Statement.
open_in_new @@ -142,15 +152,24 @@

(todo: add link to regular giving terms & conditions)

- + @if (!submitting) { + + } @else { +
+ @if (submitting) { + + } +
+ } diff --git a/src/app/regular-giving/regular-giving.component.ts b/src/app/regular-giving/regular-giving.component.ts index 885816c71..06c6b690d 100644 --- a/src/app/regular-giving/regular-giving.component.ts +++ b/src/app/regular-giving/regular-giving.component.ts @@ -2,8 +2,6 @@ import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute, Router} from "@angular/router"; import {Campaign} from "../campaign.model"; import {ComponentsModule} from "@biggive/components-angular"; -import {CampaignInfoComponent} from "../campaign-info/campaign-info.component"; -import {AsyncPipe} from "@angular/common"; import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; import {MatStep, MatStepper} from "@angular/material/stepper"; import {StepperSelectionEvent} from "@angular/cdk/stepper"; @@ -12,7 +10,7 @@ import {MatButton} from "@angular/material/button"; import {MatIcon} from "@angular/material/icon"; import {Person} from "../person.model"; import {RegularGivingService} from "../regularGiving.service"; -import { Mandate } from '../mandate.model'; +import {Mandate} from '../mandate.model'; import {myRegularGivingPath} from "../app-routing"; import {requiredNotBlankValidator} from "../validators/notBlank"; import {getCurrencyMinValidator} from "../validators/currency-min"; @@ -22,23 +20,23 @@ import {DonorAccount} from "../donorAccount.model"; import {countryOptions} from "../countries"; import {PageMetaService} from "../page-meta.service"; import {StripeService} from "../stripe.service"; -import {StripeElements, StripePaymentElement} from "@stripe/stripe-js"; +import {ConfirmationToken, StripeElements, StripePaymentElement} from "@stripe/stripe-js"; import {DonationService, StripeCustomerSession} from "../donation.service"; +import {MatProgressSpinner} from "@angular/material/progress-spinner"; @Component({ selector: 'app-regular-giving', standalone: true, imports: [ ComponentsModule, - CampaignInfoComponent, - AsyncPipe, FormsModule, MatStep, MatStepper, ReactiveFormsModule, MatInput, MatButton, - MatIcon + MatIcon, + MatProgressSpinner ], templateUrl: './regular-giving.component.html', styleUrl: './regular-giving.component.scss' @@ -60,6 +58,7 @@ export class RegularGivingComponent implements OnInit { @ViewChild('cardInfo') protected cardInfo: ElementRef; private stripeCustomerSession: StripeCustomerSession | undefined; + protected submitting: boolean = false; constructor( private route: ActivatedRoute, @@ -136,7 +135,7 @@ export class RegularGivingComponent implements OnInit { this.stepper.next(); } - submit() { + async submit() { const invalid = this.mandateForm.invalid; if (invalid) { let errorMessage = 'Form error: '; @@ -147,25 +146,43 @@ export class RegularGivingComponent implements OnInit { return; } - const donationAmountPounds = this.getDonationAmountPounds(); - const amountInPence = donationAmountPounds * 100; + const billingPostcode: string = this.mandateForm.value.billingPostcode; + const billingCountry: string = this.selectedBillingCountryCode; - const billingPostcode = this.mandateForm.value.billingPostcode; - const billingCountry = this.selectedBillingCountryCode; + let confirmationToken: ConfirmationToken | undefined; + if (!this.stripeElements && !this.donorAccount.regularGivingPaymentMethod) { + throw new Error('Missing both stripe elements and on-file payment method, cannot setup regular giving mandate.'); + } + + this.submitting = true; + + if (this.stripeElements && !this.donorAccount.regularGivingPaymentMethod) { + const confirmationTokenResult = await this.stripeService.prepareConfirmationTokenFromPaymentElement( + {billingPostalAddress: billingPostcode, countryCode: billingCountry}, + this.stripeElements + ); + + confirmationToken = confirmationTokenResult.confirmationToken; + } + if (!this.donorAccount.regularGivingPaymentMethod && !confirmationToken) { + this.submitting = false; + throw new Error("Stripe Confirmation token is missing"); + } /** - * @todo consider if we need to send this from FE - if we're not displaying it to donor better for matchbot to + * @todo-regular-giving consider if we need to send this from FE - if we're not displaying it to donor better for matchbot to * generate it.*/ const dayOfMonth = Math.min(new Date().getDate(), 28); this.regularGivingService.startMandate({ - amountInPence, + amountInPence: this.getDonationAmountPence(), dayOfMonth, campaignId: this.campaign.id, currency: "GBP", giftAid: false, billingPostcode, billingCountry, + stripeConfirmationTokenId: confirmationToken?.id }).subscribe({ next: async (mandate: Mandate) => { await this.router.navigateByUrl(`${myRegularGivingPath}/${mandate.id}`); @@ -178,8 +195,8 @@ export class RegularGivingComponent implements OnInit { }) } - private getDonationAmountPounds(): number { - return +this.mandateForm.value.donationAmount; + private getDonationAmountPence(): number { + return 100 * this.mandateForm.value.donationAmount; } protected setSelectedCountry = ((countryCode: string) => { @@ -203,11 +220,11 @@ export class RegularGivingComponent implements OnInit { } if (this.stripeElements) { - this.stripeElements.update({amount: this.getDonationAmountPounds() * 100}) + this.stripeElements.update({amount: this.getDonationAmountPence()}) } else { this.stripeElements = this.stripeService.stripeElements( { - amount: this.getDonationAmountPounds() * 100, + amount: this.getDonationAmountPence(), currency: this.campaign.currencyCode }, 'off_session', diff --git a/src/app/regularGiving.service.ts b/src/app/regularGiving.service.ts index bb3673ec3..2df61da94 100644 --- a/src/app/regularGiving.service.ts +++ b/src/app/regularGiving.service.ts @@ -17,6 +17,13 @@ type StartMandateParams = { /** Must match postcode on the DonorAccount if the latter is non-null */ billingPostcode: string, + + /** Should only be set if the donor previously had no payment + * method selected on their account for regular-giving use, as the payment method is common to all regular giving + * agreements for the account. If it needs to changed that will be handled at the account, rather than as part of + * a mandate. + */ + stripeConfirmationTokenId?: string, }; @Injectable({ diff --git a/src/app/stripe.service.ts b/src/app/stripe.service.ts index 644d47b93..8b212cba1 100644 --- a/src/app/stripe.service.ts +++ b/src/app/stripe.service.ts @@ -167,7 +167,10 @@ export class StripeService { elements.update({amount: this.amountIncTipInMinorUnit(donation)}); } - async prepareConfirmationTokenFromPaymentElement(donation: Donation, elements: StripeElements): Promise { + async prepareConfirmationTokenFromPaymentElement( + {countryCode, billingPostalAddress}: {countryCode: string, billingPostalAddress: string}, + elements: StripeElements + ): Promise { if (! this.stripe) { throw new Error("Stripe not ready"); } @@ -183,8 +186,8 @@ export class StripeService { billing_details: { address: { - country: donation.countryCode, - postal_code: donation.billingPostalAddress + country: countryCode, + postal_code: billingPostalAddress }, } }; From f8b1d89fba2614ce988a033a4e77a903d9cf6372 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Wed, 8 Jan 2025 15:02:16 +0000 Subject: [PATCH 2/2] DON-998: Clarify copy for linking to non-regular donation page Co-authored-by: Noel Light-Hilary --- src/app/regular-giving/regular-giving.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/regular-giving/regular-giving.component.html b/src/app/regular-giving/regular-giving.component.html index 1d40385f8..48b9054a4 100644 --- a/src/app/regular-giving/regular-giving.component.html +++ b/src/app/regular-giving/regular-giving.component.html @@ -3,7 +3,7 @@

Sorry, campaign {{ campaign.title }} is not a regular giving campaign.

-

To make a one-off donation to {{ campaign.charity.name }} please visit the regular To make a one-off donation to {{ campaign.charity.name }} please visit the campaign page.