diff --git a/package-lock.json b/package-lock.json
index b6f7610a8..30c2064ee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,8 +20,8 @@
"@angular/platform-browser-dynamic": "^16.1.4",
"@angular/platform-server": "^16.1.4",
"@angular/router": "^16.1.4",
- "@biggive/components": "^202309260920.0.0",
- "@biggive/components-angular": "^202309260920.0.0",
+ "@biggive/components": "^202309271411.0.0",
+ "@biggive/components-angular": "^202309271411.0.0",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
@@ -2570,9 +2570,9 @@
}
},
"node_modules/@biggive/components": {
- "version": "202309260920.0.0",
- "resolved": "https://registry.npmjs.org/@biggive/components/-/components-202309260920.0.0.tgz",
- "integrity": "sha512-j2biNOnHf3svBhZzQTpkpxTcb9DPh+jI5D4ZkUP5Or2TL/4obF7bMTrb4O2ICMRfgNrHp24Eajh4A6qrVzg6OA==",
+ "version": "202309271411.0.0",
+ "resolved": "https://registry.npmjs.org/@biggive/components/-/components-202309271411.0.0.tgz",
+ "integrity": "sha512-SvkjcFgT5BtWITmQ5HuI/yYmjYuwngsAeTamSj9tItQxHUQhSNWI8fiTSWVvkXMfStVG+0r61fqtW0YMexxUjg==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2",
@@ -2584,9 +2584,9 @@
}
},
"node_modules/@biggive/components-angular": {
- "version": "202309260920.0.0",
- "resolved": "https://registry.npmjs.org/@biggive/components-angular/-/components-angular-202309260920.0.0.tgz",
- "integrity": "sha512-zI28F50Dvjy6lCg6loAeubg0ijIXFlFeXU8pifaFey30vMdXcdP2R98mh93Lh3V+sHm0XVqfY/Vhx6nU+nPmtQ==",
+ "version": "202309271411.0.0",
+ "resolved": "https://registry.npmjs.org/@biggive/components-angular/-/components-angular-202309271411.0.0.tgz",
+ "integrity": "sha512-BiCwN3/Gif+JgXdy1SiPbpyHSnWys9H0V7831T+lspxPeI5K3sV2gX9IOtbtVaiPm/h0kQ1z59ZEMIO1W2PJKg==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -20263,9 +20263,9 @@
}
},
"@biggive/components": {
- "version": "202309260920.0.0",
- "resolved": "https://registry.npmjs.org/@biggive/components/-/components-202309260920.0.0.tgz",
- "integrity": "sha512-j2biNOnHf3svBhZzQTpkpxTcb9DPh+jI5D4ZkUP5Or2TL/4obF7bMTrb4O2ICMRfgNrHp24Eajh4A6qrVzg6OA==",
+ "version": "202309271411.0.0",
+ "resolved": "https://registry.npmjs.org/@biggive/components/-/components-202309271411.0.0.tgz",
+ "integrity": "sha512-SvkjcFgT5BtWITmQ5HuI/yYmjYuwngsAeTamSj9tItQxHUQhSNWI8fiTSWVvkXMfStVG+0r61fqtW0YMexxUjg==",
"requires": {
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2",
@@ -20277,9 +20277,9 @@
}
},
"@biggive/components-angular": {
- "version": "202309260920.0.0",
- "resolved": "https://registry.npmjs.org/@biggive/components-angular/-/components-angular-202309260920.0.0.tgz",
- "integrity": "sha512-zI28F50Dvjy6lCg6loAeubg0ijIXFlFeXU8pifaFey30vMdXcdP2R98mh93Lh3V+sHm0XVqfY/Vhx6nU+nPmtQ==",
+ "version": "202309271411.0.0",
+ "resolved": "https://registry.npmjs.org/@biggive/components-angular/-/components-angular-202309271411.0.0.tgz",
+ "integrity": "sha512-BiCwN3/Gif+JgXdy1SiPbpyHSnWys9H0V7831T+lspxPeI5K3sV2gX9IOtbtVaiPm/h0kQ1z59ZEMIO1W2PJKg==",
"requires": {
"tslib": "^2.3.0"
}
diff --git a/package.json b/package.json
index 37ccf294e..034edaea7 100644
--- a/package.json
+++ b/package.json
@@ -38,8 +38,8 @@
"@angular/platform-browser-dynamic": "^16.1.4",
"@angular/platform-server": "^16.1.4",
"@angular/router": "^16.1.4",
- "@biggive/components": "^202309260920.0.0",
- "@biggive/components-angular": "^202309260920.0.0",
+ "@biggive/components": "^202309271411.0.0",
+ "@biggive/components-angular": "^202309271411.0.0",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
diff --git a/src/app/donation-start/donation-start-form/donation-start-form.component.html b/src/app/donation-start/donation-start-form/donation-start-form.component.html
index 9bb83f9c4..38c5bbf1c 100644
--- a/src/app/donation-start/donation-start-form/donation-start-form.component.html
+++ b/src/app/donation-start/donation-start-form/donation-start-form.component.html
@@ -427,7 +427,7 @@
How does Big Give u
class="continue b-w-100 b-rt-0"
mat-raised-button
color="primary"
- [disabled]="!stripePaymentMethodReady && !selectedSavedMethod"
+ [disabled]="(!stripePaymentMethodReady && !selectedSavedMethod) || !paymentGroup.valid"
(click)="next()"
>Continue
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 02595a132..0c411a97d 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
@@ -101,8 +101,6 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
recaptchaIdSiteKey = environment.recaptchaIdentitySiteKey;
- countryOptions = COUNTRIES;
-
creditPenceToUse = 0; // Set non-zero if logged in and Customer has a credit balance to spend. Caps donation amount too in that case.
currencySymbol: string;
@@ -120,13 +118,13 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
* of custom tip, including zero.
*/
minimumTipPercentage = 1 as const;
- readonly suggestedTipPercentages = {
- '7.5': '7.5%',
- '10': '10%',
- '12.5': '12.5%',
- '15': '15%',
- 'Other': 'Other'
- };
+ readonly suggestedTipPercentages = [
+ {value: '7.5', label: '7.5%'},
+ {value: '10', label: '10%'},
+ {value: '12.5', label: '12.5%'},
+ {value: '15', label: '15%'},
+ {value: 'Other', label: 'Other'}
+ ] as const;
noPsps = false;
psp: 'stripe';
@@ -195,7 +193,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
/**
* Keys are ISO2 codes, values are names.
*/
- public countryOptionsObject: Record;
+ public countryOptionsObject: Array<{label: string, value: string}>;
public tipControlStyle: 'dropdown'|'slider';
private tipAmountFromSlider: number;
@@ -253,10 +251,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
public timeLeftPipe: TimeLeftPipe,
) {
this.defaultCountryCode = this.donationService.getDefaultCounty();
- this.countryOptionsObject = Object.assign(
- {},
- ...(this.countryOptions.map(country => ({[country.iso2]: country.country})))
- );
+ this.countryOptionsObject = COUNTRIES.map(country => ({label: country.country, value: country.iso2}))
this.selectedCountryCode = this.defaultCountryCode;
this.tipControlStyle = (route.snapshot.queryParams?.tipControl?.toLowerCase() === 'slider')
@@ -268,10 +263,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
this.clearDonation(this.donation, false);
}
- if (this.stripePaymentElement) {
- this.stripePaymentElement.off('change');
- this.stripePaymentElement.destroy();
- }
+ this.destroyStripeElements();
}
ngOnInit() {
@@ -498,6 +490,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
this.donationForm.reset();
this.identityService.clearJWT();
this.idCaptcha.reset();
+ this.destroyStripeElements();
location.reload();
}
@@ -534,17 +527,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
}
async stepChanged(event: StepperSelectionEvent) {
- if (event.selectedStep.label === this.yourDonationStepLabel) {
- // workaround bug issue DON-883 - without resestting the page the stripe element is not usable for the new donation that will be created in this step.
- // Not ideal as this loses content the donor may have typed already, but better to reset the page than let them enter donation details and then fail to
- // take the payment.
-
- if (this.donation) {
- this.donationService.cancel(this.donation).subscribe(() => this.reset());
- }
- }
-
- // We need to allow enough time for the Stepper's animation to get the window to
+ // We need to allow enough time for the Stepper's animation to get the window to
// its final position for this step, before this scroll position update can be reliably
// helpful.
setTimeout(() => {
@@ -593,8 +576,6 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
// e-commerce funnel steps defined in our Analytics campaign, besides 1
// (which we fire on donation create API callback) and 4 (which we fire
// alongside calling payWithStripe()).
-
-
}
// Create a donation if coming from first step and not offering to resume
@@ -610,8 +591,9 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
}
if (this.psp === 'stripe' && this.donation) {
- this.stripeElements = this.stripeService.stripeElements(this.donation, this.campaign);
- this.prepareCardInput();
+ // Whether the donation's new or not, we need Stripe ready including an expected `amount` based
+ // on the latest core donation + tip values.
+ this.prepareStripeElements();
}
return;
@@ -841,7 +823,6 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
this.submitting = false;
}
-
get donationAmountField() {
if (!this.donationForm) {
return undefined;
@@ -976,7 +957,6 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
return;
}
-
// For all other errors, attempting to proceed should just help the donor find
// the error on the page if there is one.
if (!this.goToFirstVisibleError()) {
@@ -997,7 +977,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
this.updateFormWithBillingDetails(this.selectedSavedMethod);
} else {
this.selectedSavedMethod = undefined;
- this.prepareCardInput();
+ this.prepareStripeElements();
}
}
@@ -1019,14 +999,20 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
}
}
- private prepareCardInput() {
- if (this.cardInfo.nativeElement.children.length > 0) {
- // Card input was already ready.
+ private prepareStripeElements() {
+ if (!this.donation) {
+ console.log('Donation not ready for Stripe');
return;
}
- if (!this.stripeElements) {
- console.error('Stripe Elements not ready');
+ if (this.stripeElements) {
+ this.stripeService.updateAmount(this.stripeElements, this.donation);
+ } else {
+ this.stripeElements = this.stripeService.stripeElements(this.donation, this.campaign);
+ }
+
+ if (this.stripePaymentElement) {
+ // Payment element was already ready & we presume mounted.
return;
}
@@ -1052,8 +1038,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
}
},
},
- business:
- {name: "Big Give"}
+ business: {name: "Big Give"},
}
);
@@ -1063,6 +1048,15 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
}
}
+ private destroyStripeElements() {
+ if (this.stripePaymentElement) {
+ this.stripePaymentElement.off('change');
+ this.stripePaymentElement.destroy();
+ this.stripePaymentElement = undefined;
+ this.stripeElements = undefined;
+ }
+ }
+
/**
* Updates the balance of doantion credits available for use, and connected readiness + validation vars.
*/
@@ -1302,7 +1296,6 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
* @private
*/
private promptForCaptcha() {
-
if (this.idCaptchaCode) {
return false;
}
@@ -1346,6 +1339,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
this.donationCreateError = true;
this.stepper.previous(); // Go back to step 1 to surface the internal error.
}
+
private newDonationSuccess(response: DonationCreatedResponse) {
this.creatingDonation = false;
@@ -1390,8 +1384,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
if (this.creditPenceToUse > 0) {
this.stripePaymentMethodReady = true;
} else {
- this.stripeElements = this.stripeService.stripeElements(this.donation, this.campaign)
- this.prepareCardInput();
+ this.prepareStripeElements();
}
}
@@ -1508,21 +1501,13 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon
this.creatingDonation = false;
this.donationCreateError = false;
this.donationUpdateError = false;
- this.stripeError = undefined;
- this.stripeResponseErrorCode = undefined;
- this.stripePaymentMethodReady = false;
if (this.stripeSavedMethods.length < 1) {
this.selectedSavedMethod = undefined;
}
this.retrying = false;
this.submitting = false;
- this.stripeManualCardInputValid = false;
- if (this.stripePaymentElement) {
- this.stripePaymentElement.clear();
- }
-
delete this.donation;
this.donationChangeCallBack(undefined)
}
diff --git a/src/app/donation.service.spec.ts b/src/app/donation.service.spec.ts
index 585e31632..edbc13a8a 100644
--- a/src/app/donation.service.spec.ts
+++ b/src/app/donation.service.spec.ts
@@ -90,7 +90,7 @@ describe('DonationService', () => {
expect(false).toBe(true); // Always fail if observable errors
});
- const mockPost = httpMock.expectOne(`${environment.donationsApiPrefix}/donations?forNewPaymentElement=true`);
+ const mockPost = httpMock.expectOne(`${environment.donationsApiPrefix}/donations`);
expect(mockPost.request.method).toEqual('POST');
expect(mockPost.cancelled).toBeFalsy();
expect(mockPost.request.responseType).toEqual('json');
diff --git a/src/app/donation.service.ts b/src/app/donation.service.ts
index 188d4cb7b..75726eca1 100644
--- a/src/app/donation.service.ts
+++ b/src/app/donation.service.ts
@@ -170,7 +170,7 @@ export class DonationService {
: `${environment.donationsApiPrefix}${this.apiPath}`;
return this.http.post(
- endpoint + "?forNewPaymentElement=true", // temp flag until its always true
+ endpoint,
donation,
this.getPersonAuthHttpOptions(jwt),
);
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts
index bc7e70b08..56d9151fd 100644
--- a/src/app/home/home.component.ts
+++ b/src/app/home/home.component.ts
@@ -1,9 +1,31 @@
import {Component, OnInit} from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
+import {ActivatedRoute} from '@angular/router';
import {PageMetaService} from '../page-meta.service';
import {HighlightCard} from "./HighlightCard";
import {environment} from "../../environments/environment";
+const ArtsForImpactCard: HighlightCard = {
+ headerText: "Applications for Arts for Impact are now open!",
+ backgroundImageUrl: new URL('/assets/images/red-coral-texture.png', environment.donateGlobalUriPrefix),
+ iconColor: 'brand-afa-pink',
+ bodyText: 'Apply by 3rd November 2023',
+ button: {
+ text: "Apply now",
+ href: new URL('/artsforimpact/', environment.blogUriPrefix)
+ }
+} as const;
+
+const AnchorMatchFundCard: HighlightCard = {
+ headerText: 'Applications for Anchor Match Fund are open!',
+ backgroundImageUrl: new URL('/assets/images/anchor-match-fund.jpg', environment.donateGlobalUriPrefix),
+ iconColor: 'primary',
+ bodyText: 'Second edition deadline is 31st December 2023',
+ button: {
+ text: 'Apply now',
+ href: new URL('/anchor-match-fund/', environment.blogUriPrefix)
+ }
+} as const;
+
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
@@ -16,7 +38,7 @@ export class HomeComponent implements OnInit {
totalCountFormatted: string
};
-
+
highlightCards: readonly HighlightCard[] = [
{
headerText: 'Save the date for Women and Girls Match Fund',
@@ -38,27 +60,7 @@ export class HomeComponent implements OnInit {
href: new URL('/campaign/a056900001xpxqVAAQ', environment.donateGlobalUriPrefix)
}
},
- {
- headerText: 'Applications for Anchor Match Fund are open!',
- backgroundImageUrl: new URL('/assets/images/anchor-match-fund.jpg', environment.donateGlobalUriPrefix),
- iconColor: 'primary',
- bodyText: 'Second edition deadline is 31st December 2023',
- button: {
- text: 'Apply now',
- href: new URL('/anchor-match-fund/', environment.blogUriPrefix)
- }
- },
- // TODO: hould replace Anchor Mach Fund card on 2nd October 2023
- // {
- // headerText: ' Applications for Arts for Impact are now open!',
- // bodyText: "Apply by 3rd November 2023",
- // iconColor: "primary",
- // backgroundImageUrl: new URL('/assets/images/red-coral-texture.png', environment.donateGlobalUriPrefix),
- // button: {
- // text: "Apply now",
- // href: new URL('/artsforimpact', environment.environment.blogUriPrefix),
- // }
- // },
+ new Date() > new Date('2023-10-09T12:00:00') ? ArtsForImpactCard : AnchorMatchFundCard,
];
public constructor(
diff --git a/src/app/stripe.service.ts b/src/app/stripe.service.ts
index d4d7705b5..79ea14295 100644
--- a/src/app/stripe.service.ts
+++ b/src/app/stripe.service.ts
@@ -27,14 +27,11 @@ export class StripeService {
this.stripe = await loadStripe(environment.psps.stripe.publishableKey);
}
- stripeElements(donation: Donation, campaign: Campaign)
- {
+ stripeElements(donation: Donation, campaign: Campaign) {
if (!this.stripe) {
throw new Error('Stripe not ready');
}
- const amountInMinorUnit = Math.floor((donation.tipAmount + donation.donationAmount) * 100);
-
return this.stripe.elements({
fonts: [
{
@@ -45,13 +42,17 @@ export class StripeService {
],
mode: 'payment',
currency: donation.currencyCode.toLowerCase(),
- amount: amountInMinorUnit,
+ amount: this.amountIncTipInMinorUnit(donation),
setup_future_usage: 'on_session',
on_behalf_of: campaign.charity.stripeAccountId,
paymentMethodCreation: 'manual',
});
}
+ updateAmount(elements: StripeElements, donation: Donation) {
+ elements.update({amount: this.amountIncTipInMinorUnit(donation)});
+ }
+
async prepareMethodFromPaymentElement(donation: Donation, elements: StripeElements): Promise {
if (! this.stripe) {
throw new Error("Stripe not ready");
@@ -88,4 +89,8 @@ export class StripeService {
clientSecret: clientSecret
});
}
+
+ private amountIncTipInMinorUnit(donation: Donation) {
+ return Math.floor((donation.tipAmount + donation.donationAmount) * 100);
+ }
}