Skip to content

Commit

Permalink
Merge pull request #1330 from thebiggive/develop
Browse files Browse the repository at this point in the history
main <- develop
  • Loading branch information
NoelLH authored Oct 6, 2023
2 parents 324fa54 + 863f27d commit 0a75677
Show file tree
Hide file tree
Showing 23 changed files with 499 additions and 162 deletions.
2 changes: 1 addition & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
-->
<div
id="cookie-banner"
*ngIf="isPlatformBrowser && (userHasExpressedCookiePreference$ | async) === false && flags.cookieBannerEnabled"
*ngIf="isPlatformBrowser && (userHasExpressedCookiePreference$ | async) === false"
>
<biggive-cookie-banner [blogUriPrefix]="environment.blogUriPrefix" />
</div>
26 changes: 10 additions & 16 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {APP_BASE_HREF, isPlatformBrowser} from '@angular/common';
import {AfterViewInit, Component, HostListener, Inject, OnInit, PLATFORM_ID, ViewChild} from '@angular/core';
import {Event as RouterEvent, NavigationEnd, NavigationStart, Router,} from '@angular/router';
import {BiggiveMainMenu} from '@biggive/components-angular';
import {MatomoTracker} from "ngx-matomo";
import {filter} from 'rxjs/operators';

import {DonationService} from './donation.service';
Expand All @@ -17,7 +18,6 @@ import {
CookiePreferences,
CookiePreferenceService
} from "./cookiePreference.service";
import {MatomoTracker} from "ngx-matomo";

@Component({
selector: 'app-root',
Expand Down Expand Up @@ -80,21 +80,15 @@ export class AppComponent implements AfterViewInit, OnInit {

ngOnInit() {
if (this.isPlatformBrowser) {
if (flags.cookieBannerEnabled) {
this.cookiePreferenceService.userOptInToSomeCookies().subscribe((preferences: CookiePreferences) => {
if (agreesToThirdParty(preferences)) {
this.getSiteControlService.init();
}

if (agreesToAnalyticsAndTracking(preferences)) {
this.matomoTracker.setCookieConsentGiven();
}
});
} else {
this.getSiteControlService.init();
// no-need to simulate user consent for matomo here, if the banner isn't enabled we don't have
// `requireCookieConsent: true` in the matomo config.
}
this.cookiePreferenceService.userOptInToSomeCookies().subscribe((preferences: CookiePreferences) => {
if (agreesToThirdParty(preferences)) {
this.getSiteControlService.init();
}

if (agreesToAnalyticsAndTracking(preferences)) {
this.matomoTracker.setCookieConsentGiven();
}
});

// Temporarily client-side redirect the previous non-global domain to the new one.
// Once most inbound links are updated, we can probably replace the app redirect
Expand Down
3 changes: 1 addition & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { CharityCampaignsResolver } from './charity-campaigns.resolver';
import { TBG_DONATE_STORAGE } from './donation.service';
import { environment } from '../environments/environment';
import { TBG_DONATE_ID_STORAGE } from './identity.service';
import {flags} from "./featureFlags";

const matomoBaseUri = 'https://biggive.matomo.cloud';
const matomoTrackers = environment.matomoSiteId ? [
Expand All @@ -48,7 +47,7 @@ const matomoTrackers = environment.matomoSiteId ? [
routeTracking: {
enable: true,
},
requireCookieConsent: flags.cookieBannerEnabled,
requireCookieConsent: true,
}),
RouterModule.forRoot(routes, {
bindToComponentInputs: true,
Expand Down
2 changes: 1 addition & 1 deletion src/app/campaign-info/campaign-info.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class CampaignInfoComponent implements OnInit {
}

getPercentageRaised(campaign: Campaign): number | undefined {
return CampaignService.percentRaised(campaign);
return CampaignService.percentRaised(campaign, true);
}

getBeneficiaryIcon(beneficiary: string) {
Expand Down
20 changes: 20 additions & 0 deletions src/app/campaign.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,24 @@ describe('CampaignService', () => {

expect(CampaignService.isOpenForDonations(campaign)).toBe(false);
});

it ('should return the % raised for itself when its parent does not use shared funds', () => {
const campaign = getDummyCampaign();
campaign.parentUsesSharedFunds = false;
campaign.amountRaised = 98;
campaign.target = 200;

expect(CampaignService.percentRaised(campaign, true)).toBe(49);
});

it ('should return the % raised for the parent campaign when its parent does use shared funds', () => {
const campaign = getDummyCampaign();
campaign.parentUsesSharedFunds = true;
campaign.amountRaised = 98;
campaign.target = 200;
campaign.parentAmountRaised = 1000;
campaign.parentTarget = 2000;

expect(CampaignService.percentRaised(campaign, true)).toBe(50);
});
});
17 changes: 16 additions & 1 deletion src/app/campaign.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,22 @@ export class CampaignService {
return dateToUse;
}

static percentRaised(campaign: (Campaign | CampaignSummary)): number | undefined {
/**
* @param useParentIfApplicable Whether to get a percentage for the parent/meta-campaign if possible. This
* is possible when `campaign` is a detailed Campaign, e.g. on /campaign/..., and
* it takes effect when the `parentUsesSharedFunds`.
*/
static percentRaised(
campaign: (Campaign | CampaignSummary),
useParentIfApplicable = false,
): number | undefined {
if (useParentIfApplicable) {
campaign = campaign as Campaign;
if (campaign.parentUsesSharedFunds && campaign.parentTarget && campaign.parentAmountRaised) {
return Math.round((campaign.parentAmountRaised / campaign.parentTarget) * 100);
}
}

if (!campaign.target) {
return undefined;
}
Expand Down
3 changes: 1 addition & 2 deletions src/app/conversionTracking.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {Injectable} from "@angular/core";
import {Donation} from "./donation.model";
import {Campaign} from "./campaign.model";
import {agreesToAnalyticsAndTracking, CookiePreferences, CookiePreferenceService} from "./cookiePreference.service";
import {flags} from "./featureFlags";

@Injectable({
providedIn: 'root',
Expand All @@ -25,7 +24,7 @@ export class ConversionTrackingService {
}

private trackConversionWithMatomo(donation: Donation, campaign: Campaign) {
if (flags.cookieBannerEnabled && ! this.analyticsAndTrackingCookiesAllowed) {
if (! this.analyticsAndTrackingCookiesAllowed) {
return;
}

Expand Down
84 changes: 46 additions & 38 deletions src/app/donation-complete/donation-complete.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,36 +151,35 @@ export class DonationCompleteComponent implements OnInit {
}

this.person.raw_password = password;
this.identityService.update(this.person)
.subscribe(
() => { // Success. Must subscribe for call to fire.
this.registerError = undefined;
this.registrationComplete = true;
this.matomoTracker.trackEvent('identity', 'person_password_set', 'Account password creation complete');

// We should only auto-login (and therefore execute the captcha) if the donor requested a persistent session.
if (stayLoggedIn) {
this.captcha.execute(); // Leads to loginCaptchaReturn() assuming the captcha succeeds.
} else {
// Otherwise we should remove even the temporary ID token.
this.identityService.clearJWT();
}
},
(error: HttpErrorResponse) => {
const htmlErrorDescription = error.error?.error?.htmlDescription;
if (error.error?.error?.type === "DUPLICATE_EMAIL_ADDRESS_WITH_PASSWORD") {
this.registerErrorDescription = "Your password could not be set. There is already a password set for your email address.";
} else if (htmlErrorDescription) {
// we bypass security because we trust the Identity server.
this.registerErrorDescriptionHtml = this.sanitizer.bypassSecurityTrustHtml(htmlErrorDescription)
} else {
this.registerErrorDescription = error.error?.error?.description
}

this.registerError = error.message;
this.matomoTracker.trackEvent('identity_error', 'person_password_set_failed', `${error.status}: ${error.message}`);
},
);
this.identityService.update(this.person).subscribe({
next: () => { // Success. Must subscribe for call to fire.
this.registerError = undefined;
this.registrationComplete = true;
this.matomoTracker.trackEvent('identity', 'person_password_set', 'Account password creation complete');

// We should only auto-login (and therefore execute the captcha) if the donor requested a persistent session.
if (stayLoggedIn) {
this.captcha.execute(); // Leads to loginCaptchaReturn() assuming the captcha succeeds.
} else {
// Otherwise we should remove even the temporary ID token.
this.identityService.clearJWT();
}
},
error: (error: HttpErrorResponse) => {
const htmlErrorDescription = error.error?.error?.htmlDescription;
if (error.error?.error?.type === "DUPLICATE_EMAIL_ADDRESS_WITH_PASSWORD") {
this.registerErrorDescription = "Your password could not be set. There is already a password set for your email address.";
} else if (htmlErrorDescription) {
// we bypass security because we trust the Identity server.
this.registerErrorDescriptionHtml = this.sanitizer.bypassSecurityTrustHtml(htmlErrorDescription)
} else {
this.registerErrorDescription = error.error?.error?.description
}

this.registerError = error.message;
this.matomoTracker.trackEvent('identity_error', 'person_password_set_failed', `${error.status}: ${error.message}`);
},
});
}

private setDonation(donation: Donation) {
Expand All @@ -202,14 +201,17 @@ export class DonationCompleteComponent implements OnInit {
this.registrationComplete = true;
} else {
this.identityService.update(person)
.subscribe(person => {
this.patchedCorePersonInfo = true;
this.person = person;
this.offerToSetPassword = !person.has_password;
}, (error: HttpErrorResponse) => {
// For now we probably don't really need to inform donors if we didn't patch their Person data, and just won't ask them to
// set a password if the first step failed. We'll want to monitor Analytics for any patterns suggesting a problem in the logic though.
this.matomoTracker.trackEvent('identity_error', 'person_core_data_update_failed', `${error.status}: ${error.message}`);
.subscribe({
next: person => {
this.patchedCorePersonInfo = true;
this.person = person;
this.offerToSetPassword = !person.has_password;
},
error: (error: HttpErrorResponse) => {
// For now we probably don't really need to inform donors if we didn't patch their Person data, and just won't ask them to
// set a password if the first step failed. We'll want to monitor Analytics for any patterns suggesting a problem in the logic though.
this.matomoTracker.trackEvent('identity_error', 'person_core_data_update_failed', `${error.status}: ${error.message}`);
},
});
} // End token-not-finalised condition.
} // Else no ID JWT saved. Donor may have already set a password but opted to log out.
Expand All @@ -227,8 +229,14 @@ export class DonationCompleteComponent implements OnInit {
});

if (donation && this.donationService.isComplete(donation)) {
// This is linked to the Donation Complete goal in Matomo, so we don't need to separately
// `trackGoal()` for that.
this.matomoTracker.trackEvent('donate', 'thank_you_fully_loaded', `Donation to campaign ${donation.projectId}`);

if (donation.tipAmount > 0 && environment.matomoNonZeroTipGoalId && donation.currencyCode === 'GBP') {
this.matomoTracker.trackGoal(environment.matomoNonZeroTipGoalId, donation.tipAmount);
}

this.cardChargedAmount = donation.donationAmount + donation.feeCoverAmount + donation.tipAmount;
this.giftAidAmount = donation.giftAid ? 0.25 * donation.donationAmount : 0;
this.totalValue = donation.donationAmount + this.giftAidAmount + donation.matchedAmount;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {HttpClientTestingModule} from "@angular/common/http/testing";
import {Component} from "@angular/core";
import {ComponentFixture, TestBed} from '@angular/core/testing';
import { MatDialogModule } from '@angular/material/dialog';
import {ActivatedRoute} from "@angular/router";
Expand All @@ -7,17 +8,33 @@ import {InMemoryStorageService} from "ngx-webstorage-service";
import {of} from "rxjs";

import {DonationStartContainerComponent} from "./donation-start-container.component";
import {DonationStartFormComponent} from "../donation-start-form/donation-start-form.component";
import {TBG_DONATE_ID_STORAGE} from "../../identity.service";
import {TBG_DONATE_STORAGE} from "../../donation.service";
import {Campaign} from "../../campaign.model";

describe('DonationStartLoginComponent', () => {
// See https://medium.com/angular-in-depth/angular-unit-testing-viewchild-4525e0c7b756
@Component({
selector: 'app-donation-start-form',
template: '',
providers: [
{ provide: DonationStartFormComponent, useClass: DonationStartFormStubComponent },
],
})
class DonationStartFormStubComponent {
resumeDonationsIfPossible() {}
}

describe('DonationStartContainer', () => {
let component: DonationStartContainerComponent;
let fixture: ComponentFixture<DonationStartContainerComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DonationStartContainerComponent ],
declarations: [
DonationStartContainerComponent,
DonationStartFormStubComponent,
],
imports: [
HttpClientTestingModule,
MatDialogModule,
Expand Down Expand Up @@ -51,8 +68,11 @@ describe('DonationStartLoginComponent', () => {
fixture.detectChanges();
});

it('should create', () => {
it(`should create and call form's donation resume helper`, () => {
spyOn(component.donationStartForm, 'resumeDonationsIfPossible');
component.ngAfterViewInit();
expect(component).toBeTruthy();
expect(component.donationStartForm.resumeDonationsIfPossible).toHaveBeenCalled();
});

const getDummyCampaign = (campaignId: string) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core';
import {Campaign} from "../../campaign.model";
import {ActivatedRoute} from "@angular/router";
import { Donation } from 'src/app/donation.model';
Expand All @@ -11,7 +11,7 @@ import {ImageService} from "../../image.service";
templateUrl: './donation-start-container.component.html',
styleUrls: ['./donation-start-container.component.scss']
})
export class DonationStartContainerComponent implements OnInit{
export class DonationStartContainerComponent implements AfterViewInit, OnInit{
campaign: Campaign;
campaignOpenOnLoad: boolean;
donation: Donation | undefined = undefined;
Expand All @@ -31,23 +31,22 @@ export class DonationStartContainerComponent implements OnInit{
) {
}

ngOnInit() {
ngOnInit() {
this.campaign = this.route.snapshot.data.campaign;
this.campaignOpenOnLoad = this.campaignIsOpen();
this.imageService.getImageUri(this.campaign.bannerUri, 830).subscribe(uri => this.bannerUri = uri);
this.campaignOpenOnLoad = this.campaignIsOpen();
this.imageService.getImageUri(this.campaign.bannerUri, 830).subscribe(uri => this.bannerUri = uri);
}

const idAndJWT = this.identityService.getIdAndJWT();
if (idAndJWT) {
this.loadAuthedPersonInfo(idAndJWT.id, idAndJWT.jwt);
} else {
if (!this.donationStartForm) {
console.error("Donation start form not loaded");
}
// this.donationStartForm is undefined when we're running donation-start-login.component.spec.ts . I'm not sure
// tbh why this class is even called from that test. Null safe call to let it pass.
this.donationStartForm?.resumeDonationsIfPossible();
}
}
ngAfterViewInit() {
const idAndJWT = this.identityService.getIdAndJWT();
if (idAndJWT) {
// Callback will `resumeDonationsIfPossible()` after loading the person.
this.loadAuthedPersonInfo(idAndJWT.id, idAndJWT.jwt);
return;
}

this.donationStartForm.resumeDonationsIfPossible();
}

/**
* Unlike the CampaignService method which is more forgiving if the status gets stuck Active (we don't trust
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { DonationStartContainerRoutingModule } from './donation-start-routing.mo
import {DonationStartFormComponent} from "../donation-start-form/donation-start-form.component";
import {MatExpansionModule} from '@angular/material/expansion';
import { DonationTippingSliderComponent } from '../donation-start-form/donation-tipping-slider/donation-tipping-slider.component';
import {MatSnackBarModule} from "@angular/material/snack-bar";
@NgModule({
imports: [
...allChildComponentImports,
Expand All @@ -50,6 +51,7 @@ import { DonationTippingSliderComponent } from '../donation-start-form/donation-
RecaptchaModule,
TimeLeftPipe,
CampaignDetailsModule,
MatSnackBarModule
],
declarations: [
DonationStartContainerComponent,
Expand Down
Loading

0 comments on commit 0a75677

Please sign in to comment.