Skip to content

Commit

Permalink
Merge pull request #1818 from thebiggive/DON-998-regular-giving-strip…
Browse files Browse the repository at this point in the history
…e-card

DON-998: Send Stripe confirmation token when setting up regular g…
  • Loading branch information
bdsl authored Jan 8, 2025
2 parents ba76c2c + f8b1d89 commit 9d26706
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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, <StripeElements>this.stripeElements);
confirmationTokenResult = await this.stripeService.prepareConfirmationTokenFromPaymentElement(
{countryCode: this.donation.countryCode!, billingPostalAddress: this.donation.billingPostalAddress!},
this.stripeElements
);
confirmationToken = confirmationTokenResult.confirmationToken;

if (confirmationToken || paymentMethod) {
Expand Down
101 changes: 60 additions & 41 deletions src/app/regular-giving/regular-giving.component.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<main class="b-container">
@if(!campaign.isRegularGiving) {
<div>
<p class="error">
Sorry, campaign {{ campaign.title }} is not a regular giving campaign.</p>
<p>To make a one-off donation to {{ campaign.charity.name }} please visit the regular <a href="/campaign/{{campaign.id}}">campaign page</a>.
</p>
</div>
@if (!campaign.isRegularGiving) {
<div>
<p class="error">
Sorry, campaign {{ campaign.title }} is not a regular giving campaign.</p>
<p>To make a one-off donation to {{ campaign.charity.name }} please visit the <a
href="/campaign/{{campaign.id}}">campaign page</a>.
</p>
</div>
} @else {
<div class="c-form-container">
<biggive-page-section>
Expand Down Expand Up @@ -38,7 +39,7 @@
slot="label"
for="donationAmount"
>
Monthly donation to {{campaign.charity.name}}
Monthly donation to {{ campaign.charity.name }}
</label>
<input
maxlength="10"
Expand All @@ -56,7 +57,8 @@
mat-raised-button
color="primary"
(click)="next()"
>Continue</button>
>Continue
</button>
</div>

</mat-step>
Expand All @@ -72,34 +74,40 @@

@if (donorAccount.regularGivingPaymentMethod) {
<!-- @todo-regular-giving: show existing payment method here, and/or on account page -->
<p>
Your existing regular giving payment method will be used for these donations.
<p style="border: 2px solid blue; background: pink">
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)
</p>
} @else {
<biggive-text-input>
<span slot="label">Payment Method</span> <!-- no label because we don't have a labelable form element - the card form is supplied by Stripe not us -->
<span slot="label">Payment Method</span>
<!-- no label because we don't have a labelable form element - the card form is supplied by Stripe not us -->
<div slot="input" class="sr-input sr-card-element" id="card-info" #cardInfo></div>
</biggive-text-input>
}

<p>
<p>

<biggive-form-field-select
[prompt]="'Billing country'"
[options]="countryOptionsObject"
[selectedValue]="selectedBillingCountryCode"
[backgroundColour]="'grey'"
[selectionChanged]="setSelectedCountry"
[spaceBelow]="3"
[selectedOptionColour]="'inherit'"
>
</biggive-form-field-select>
<biggive-form-field-select
[prompt]="'Billing country'"
[options]="countryOptionsObject"
[selectedValue]="selectedBillingCountryCode"
[backgroundColour]="'grey'"
[selectionChanged]="setSelectedCountry"
[spaceBelow]="3"
[selectedOptionColour]="'inherit'"
>
</biggive-form-field-select>

<biggive-text-input>
<label slot="label" for="billingPostcode">Billing postcode</label>
<input slot="input" formControlName="billingPostcode" id="billingPostcode" matInput (change)="onBillingPostCodeChanged($event)" autocapitalize="characters" autocomplete="postal-code">
</biggive-text-input>
<biggive-text-input>
<label slot="label" for="billingPostcode">Billing postcode</label>
<input slot="input" formControlName="billingPostcode" id="billingPostcode" matInput
(change)="onBillingPostCodeChanged($event)" autocapitalize="characters"
autocomplete="postal-code">
</biggive-text-input>
}

<div style="text-align: center">
<button style="width: 40%;"
Expand All @@ -108,7 +116,8 @@
mat-raised-button
color="primary"
(click)="next()"
>Continue</button>
>Continue
</button>
</div>

</mat-step>
Expand All @@ -122,15 +131,16 @@
<table id="personal-details">
<tr>
<td>Name</td>
<td>{{donor.first_name}} {{donor.last_name}}</td>
<td>{{ donor.first_name }} {{ donor.last_name }}</td>
</tr>
<tr>
<td>Email</td>
<td>{{donor.email_address}}</td>
<td>{{ donor.email_address }}</td>
</tr>
</table>

<p class="b-rt-0 b-m-0 b-mt-40">By clicking on the <span class="b-bold">Start regular giving now</span> button, you agree to
<p class="b-rt-0 b-m-0 b-mt-40">By clicking on the <span class="b-bold">Start regular giving now</span>
button, you agree to
Big Give's Terms and Conditions and Privacy Statement. <br/>
<a [href]="termsUrl" target="_blank">
<mat-icon class="b-va-bottom" aria-hidden="false" aria-label="Open in new tab">open_in_new</mat-icon>
Expand All @@ -142,15 +152,24 @@
</p>
<p>(todo: add link to regular giving terms & conditions)</p>

<button
(click)="submit()"
(keyup.enter)="submit()"
class="c-donate-button b-donate-button b-w-100 b-rt-1"
mat-raised-button
color="primary"
>
Start regular giving now
</button>
@if (!submitting) {
<button
(click)="submit()"
(keyup.enter)="submit()"
class="c-donate-button b-donate-button b-w-100 b-rt-1"
mat-raised-button
color="primary"
>
Start regular giving now
</button>
} @else {
<div aria-live="polite">
@if (submitting) {
<mat-spinner color="primary" diameter="30"
aria-label="Processing your regular giving mandate"></mat-spinner>
}
</div>
}
</mat-step>
</mat-vertical-stepper>
</form>
Expand Down
53 changes: 35 additions & 18 deletions src/app/regular-giving/regular-giving.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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: ';
Expand All @@ -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}`);
Expand All @@ -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) => {
Expand All @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions src/app/regularGiving.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
9 changes: 6 additions & 3 deletions src/app/stripe.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ export class StripeService {
elements.update({amount: this.amountIncTipInMinorUnit(donation)});
}

async prepareConfirmationTokenFromPaymentElement(donation: Donation, elements: StripeElements): Promise<ConfirmationTokenResult> {
async prepareConfirmationTokenFromPaymentElement(
{countryCode, billingPostalAddress}: {countryCode: string, billingPostalAddress: string},
elements: StripeElements
): Promise<ConfirmationTokenResult> {
if (! this.stripe) {
throw new Error("Stripe not ready");
}
Expand All @@ -183,8 +186,8 @@ export class StripeService {
billing_details:
{
address: {
country: donation.countryCode,
postal_code: donation.billingPostalAddress
country: countryCode,
postal_code: billingPostalAddress
},
}
};
Expand Down

0 comments on commit 9d26706

Please sign in to comment.