From 98527efcdbbb22ce9bca8e9e4d2669b9ac4a7763 Mon Sep 17 00:00:00 2001 From: Noel Light-Hilary Date: Tue, 26 Sep 2023 15:19:12 +0100 Subject: [PATCH 1/6] Tidy up temporary donation create query param --- src/app/donation.service.spec.ts | 2 +- src/app/donation.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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), ); From acad0aac146aa3865c812837145d4a4698e777f3 Mon Sep 17 00:00:00 2001 From: Noel Light-Hilary Date: Tue, 26 Sep 2023 15:34:39 +0100 Subject: [PATCH 2/6] =?UTF-8?q?DON-883=20=E2=80=93=20keep=20Stripe.js=20in?= =?UTF-8?q?=20sync=20with=20the=20Donation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And rationalise what we reset, leaving an already valid Payment Element and its status tracking properties untouched where possible. --- .../donation-start-form.component.ts | 70 ++++++++----------- src/app/stripe.service.ts | 15 ++-- 2 files changed, 40 insertions(+), 45 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 02595a132..163f2c08c 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 @@ -268,10 +268,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 +495,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon this.donationForm.reset(); this.identityService.clearJWT(); this.idCaptcha.reset(); + this.destroyStripeElements(); location.reload(); } @@ -534,17 +532,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 +581,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 +596,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 +828,6 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon this.submitting = false; } - get donationAmountField() { if (!this.donationForm) { return undefined; @@ -976,7 +962,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 +982,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon this.updateFormWithBillingDetails(this.selectedSavedMethod); } else { this.selectedSavedMethod = undefined; - this.prepareCardInput(); + this.prepareStripeElements(); } } @@ -1019,14 +1004,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 +1043,7 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon } }, }, - business: - {name: "Big Give"} + business: {name: "Big Give"}, } ); @@ -1063,6 +1053,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 +1301,6 @@ export class DonationStartFormComponent implements AfterContentChecked, AfterCon * @private */ private promptForCaptcha() { - if (this.idCaptchaCode) { return false; } @@ -1346,6 +1344,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 +1389,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 +1506,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/stripe.service.ts b/src/app/stripe.service.ts index d4d7705b5..18a5cce70 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.getAmountInMinorUnit(donation), setup_future_usage: 'on_session', on_behalf_of: campaign.charity.stripeAccountId, paymentMethodCreation: 'manual', }); } + updateAmount(elements: StripeElements, donation: Donation) { + elements.update({amount: this.getAmountInMinorUnit(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 getAmountInMinorUnit(donation: Donation) { + return Math.floor((donation.tipAmount + donation.donationAmount) * 100); + } } From adec0b2104be7a72df1e609b890987bca458a6a0 Mon Sep 17 00:00:00 2001 From: Noel Light-Hilary Date: Tue, 26 Sep 2023 15:46:37 +0100 Subject: [PATCH 3/6] =?UTF-8?q?DON-883=20=E2=80=93=20factor=20`paymentGrou?= =?UTF-8?q?p`=20validity=20into=20Continue=20button=20state=20for=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In addition to Stripe payment method readiness --- .../donation-start-form/donation-start-form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ddc067df351049eed45e25277a2b47bab6f09e5f Mon Sep 17 00:00:00 2001 From: Noel Light-Hilary Date: Tue, 26 Sep 2023 16:23:12 +0100 Subject: [PATCH 4/6] Apply suggestions from code review Rename amount sum fn Co-authored-by: Barney Laurance --- src/app/stripe.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/stripe.service.ts b/src/app/stripe.service.ts index 18a5cce70..79ea14295 100644 --- a/src/app/stripe.service.ts +++ b/src/app/stripe.service.ts @@ -42,7 +42,7 @@ export class StripeService { ], mode: 'payment', currency: donation.currencyCode.toLowerCase(), - amount: this.getAmountInMinorUnit(donation), + amount: this.amountIncTipInMinorUnit(donation), setup_future_usage: 'on_session', on_behalf_of: campaign.charity.stripeAccountId, paymentMethodCreation: 'manual', @@ -50,7 +50,7 @@ export class StripeService { } updateAmount(elements: StripeElements, donation: Donation) { - elements.update({amount: this.getAmountInMinorUnit(donation)}); + elements.update({amount: this.amountIncTipInMinorUnit(donation)}); } async prepareMethodFromPaymentElement(donation: Donation, elements: StripeElements): Promise { @@ -90,7 +90,7 @@ export class StripeService { }); } - private getAmountInMinorUnit(donation: Donation) { + private amountIncTipInMinorUnit(donation: Donation) { return Math.floor((donation.tipAmount + donation.donationAmount) * 100); } } From cb0d3a19a902935a028b6f0e5fc26c806d4316e6 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Wed, 27 Sep 2023 12:35:51 +0100 Subject: [PATCH 5/6] DON-887: Sow suggested tip options in correct order --- package-lock.json | 28 +++++++++---------- package.json | 4 +-- .../donation-start-form.component.ts | 23 ++++++--------- 3 files changed, 25 insertions(+), 30 deletions(-) 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.ts b/src/app/donation-start/donation-start-form/donation-start-form.component.ts index 163f2c08c..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') From 701bcfcde3e2cf0ae7a841fd66030418e4f3d8b3 Mon Sep 17 00:00:00 2001 From: Barney Laurance Date: Wed, 27 Sep 2023 17:31:24 +0100 Subject: [PATCH 6/6] DON-874: Advertise Arts For Impact from the 9th of October --- src/app/home/home.component.ts | 48 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 23 deletions(-) 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(