Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add router hijacking for donation and membership forms #290

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/components/pages/DonationForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
<keep-alive>
<PaymentPage
v-if="currentPageIndex === 0"
@next-page="currentPageIndex = 1"
@next-page="goToAddressPage"
:assets-path="assetsPath"
:payment-amounts="paymentAmounts"
:payment-intervals="paymentIntervals"
:payment-types="paymentTypes"
/>
<AddressPage
v-else
@previous-page="currentPageIndex = 0"
@previous-page="goToPaymentPage"
:assets-path="assetsPath"
:validate-address-url="validateAddressUrl"
:validate-email-url="validateEmailUrl"
Expand All @@ -31,15 +31,15 @@
<keep-alive>
<PaymentPage
v-if="currentPageIndex === 0"
@next-page="currentPageIndex = 1"
@next-page="goToAddressPage"
:assets-path="assetsPath"
:payment-amounts="paymentAmounts"
:payment-intervals="paymentIntervals"
:payment-types="paymentTypes"
/>
<AddressPageDonationReceipt
v-else
@previous-page="currentPageIndex = 0"
@previous-page="goToPaymentPage"
:assets-path="assetsPath"
:validate-address-url="validateAddressUrl"
:validate-email-url="validateEmailUrl"
Expand Down Expand Up @@ -67,6 +67,7 @@ import { AddressValidation } from '@src/view_models/Validation';
import { Salutation } from '@src/view_models/Salutation';
import { CampaignValues } from '@src/view_models/CampaignValues';
import AddressPageDonationReceipt from '@src/components/pages/donation_form/subpages/AddressPageDonationReceipt.vue';
import { HistoryHijacker, PopStateEvent } from '@src/util/HistoryHijacker';

interface Props {
assetsPath: string;
Expand All @@ -83,14 +84,30 @@ interface Props {
campaignValues: CampaignValues;
addressValidationPatterns: AddressValidation;
startPageIndex: number;
historyHijacker: HistoryHijacker;
}

const AddressPageName = 'AddressPage';
const props = defineProps<Props>();

const currentPageIndex = ref<number>( props.startPageIndex );

watch( currentPageIndex, () => {
window.scrollTo( 0, 0 );
} );

props.historyHijacker.addHistoryCallback( ( e: PopStateEvent ) => {
// If the state is the address page then the user has hit the forward button after hitting back
currentPageIndex.value = e.state === AddressPageName ? 1 : 0;
} );

const goToAddressPage = () => {
currentPageIndex.value = 1;
props.historyHijacker.addPushState( AddressPageName );
};

const goToPaymentPage = () => {
currentPageIndex.value = 0;
props.historyHijacker.back();
};

</script>
109 changes: 44 additions & 65 deletions src/components/pages/MembershipForm.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
<template>
<form name="laika-membership" ref="form" :action="`/apply-for-membership?${trackingParams}`" method="post">
<form name="laika-membership" ref="form" :action="`/apply-for-membership?${campaignParams}`" method="post">
<keep-alive>
<component
ref="currentPage"
:is="currentFormComponent"
v-on:next-page="changePageIndex( 1 )"
v-on:previous-page="changePageIndex( -1 )"
v-bind="currentProperties">
</component>
<PaymentPage
v-if="currentPageIndex === 0"
@next-page="goToAddressPage"
:validate-fee-url="validateFeeUrl"
:payment-amounts="paymentAmounts"
:validate-legacy-bank-data-url="validateLegacyBankDataUrl"
:payment-intervals="paymentIntervals"
:payment-types="paymentTypes"
:validate-bank-data-url="validateBankDataUrl"
:show-membership-type-option="showMembershipTypeOption"
/>
<AddressPage
v-else
@previous-page="goToPaymentPage"
:campaign-values="campaignValues"
:validate-email-url="validateEmailUrl"
:validate-address-url="validateAddressUrl"
:address-validation-patterns="addressValidationPatterns"
:countries="countries"
:date-of-birth-validation-pattern="dateOfBirthValidationPattern"
:salutations="salutations"
:tracking-data="trackingData"
/>
</keep-alive>
</form>
</template>

<script setup lang="ts">
import { Component, computed, ref, watch } from 'vue';
import { inject, ref, watch } from 'vue';
import { Country } from '@src/view_models/Country';
import { CampaignValues } from '@src/view_models/CampaignValues';
import { AddressValidation } from '@src/view_models/Validation';
import { TrackingData } from '@src/view_models/TrackingData';
import { Salutation } from '@src/view_models/Salutation';
import PaymentPage from '@src/components/pages/membership_form/subpages/PaymentPage.vue';
import AddressPage from '@src/components/pages/membership_form/subpages/AddressPage.vue';
import { HistoryHijacker, PopStateEvent } from '@src/util/HistoryHijacker';
import { QUERY_STRING_INJECTION_KEY } from '@src/util/createCampaignQueryString';

interface Props {
validateAddressUrl: string;
Expand All @@ -38,70 +56,31 @@ interface Props {
dateOfBirthValidationPattern: String,
campaignValues: CampaignValues;
trackingData: TrackingData
startPageIndex?: number;
historyHijacker: HistoryHijacker;
}

const props = withDefaults( defineProps<Props>(), {
startPageIndex: 0,
} );

const currentPageIndex = ref<number>( props.startPageIndex );
const pages = [ PaymentPage, AddressPage ];

const trackingParams = computed( (): string => {
const params = new URLSearchParams( window.location.search );
const campaign = params.get( 'piwik_campaign' );
const kwd = params.get( 'piwik_kwd' );
if ( kwd && campaign ) {
return `piwik_campaign=${campaign}&piwik_kwd=${kwd}`;
}
return '';
} );
const AddressPageName = 'AddressPage';
const props = defineProps<Props>();
const currentPageIndex = ref<number>( 0 );
const campaignParams = inject<string>( QUERY_STRING_INJECTION_KEY, '' );

const currentFormComponent = computed( (): Component => {
return pages[ currentPageIndex.value ];
watch( currentPageIndex, () => {
window.scrollTo( 0, 0 );
} );

const currentProperties = computed( (): object => {
if ( currentFormComponent.value === AddressPage ) {
return {
validateAddressUrl: props.validateAddressUrl,
validateEmailUrl: props.validateEmailUrl,
validateFeeUrl: props.validateFeeUrl,
countries: props.countries,
salutations: props.salutations,
addressValidationPatterns: props.addressValidationPatterns,
dateOfBirthValidationPattern: props.dateOfBirthValidationPattern,
trackingData: props.trackingData,
campaignValues: props.campaignValues,
};
}
return {
showMembershipTypeOption: props.showMembershipTypeOption,
validateFeeUrl: props.validateFeeUrl,
paymentAmounts: props.paymentAmounts,
paymentIntervals: props.paymentIntervals,
paymentTypes: props.paymentTypes,
validateBankDataUrl: props.validateBankDataUrl,
validateLegacyBankDataUrl: props.validateLegacyBankDataUrl,
salutations: props.salutations,
};
props.historyHijacker.addHistoryCallback( ( e: PopStateEvent ) => {
// If the state is the address page then the user has hit the forward button after hitting back
currentPageIndex.value = e.state === AddressPageName ? 1 : 0;
} );

const scrollToTop = (): void => {
window.scrollTo( 0, 0 );
const goToAddressPage = () => {
currentPageIndex.value = 1;
props.historyHijacker.addPushState( AddressPageName );
};

watch( currentPageIndex, () => {
scrollToTop();
} );

function changePageIndex( indexChange: number ): void {
const newIndex = currentPageIndex.value + indexChange;
if ( newIndex >= 0 && newIndex < pages.length ) {
currentPageIndex.value = newIndex;
scrollToTop();
}
}
const goToPaymentPage = () => {
currentPageIndex.value = 0;
props.historyHijacker.back();
};

</script>
2 changes: 2 additions & 0 deletions src/pages/donation_form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import App from '@src/components/App.vue';
import DonationForm from '@src/components/pages/DonationForm.vue';
import { ApiCityAutocompleteResource } from '@src/util/CityAutocompleteResource';
import { createFeatureFetcher } from '@src/util/FeatureFetcher';
import { WindowHistoryHijacker } from '@src/util/HistoryHijacker';

interface DonationFormModel {
initialFormValues: any,
Expand Down Expand Up @@ -81,6 +82,7 @@ dataPersister.initialize( persistenceItems ).then( () => {
campaignValues: campaignParameters.getCampaignValues(),
addressValidationPatterns: pageData.applicationVars.addressValidationPatterns,
startPageIndex: paymentDataComplete ? 1 : 0,
historyHijacker: new WindowHistoryHijacker(),
},
} );
app.provide( 'cityAutocompleteResource', new ApiCityAutocompleteResource() );
Expand Down
2 changes: 2 additions & 0 deletions src/pages/membership_application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { createFeatureFetcher } from '@src/util/FeatureFetcher';
import { bucketIdToCssClass } from '@src/util/bucket_id_to_css_class';
import CampaignParameters from '@src/util/CampaignParameters';
import { TrackingData } from '@src/view_models/TrackingData';
import { WindowHistoryHijacker } from '@src/util/HistoryHijacker';

interface MembershipAmountModel {
presetAmounts: Array<string>,
Expand Down Expand Up @@ -108,6 +109,7 @@ dataPersister.initialize( persistenceItems ).then( () => {
dateOfBirthValidationPattern: pageData.applicationVars.dateOfBirthValidationPattern,
trackingData: pageData.applicationVars.tracking,
campaignValues: campaignParameters.getCampaignValues(),
historyHijacker: new WindowHistoryHijacker(),
},
} );
app.provide( 'cityAutocompleteResource', new ApiCityAutocompleteResource() );
Expand Down
33 changes: 33 additions & 0 deletions src/util/HistoryHijacker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export interface PopStateEvent {
state: string;
}

export interface HistoryHijacker {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the function of this interface from first glance and/or from the implementation. Could you add some docblocks that explains the pupose of the interface and what the methods do?

addHistoryCallback( callback: ( e: PopStateEvent ) => void ): void;

addPushState( pageName: string ): void;

back(): void;
}

export class WindowHistoryHijacker implements HistoryHijacker {
private hasPushState: boolean = false;

addHistoryCallback( callback: ( e: PopStateEvent ) => void ): void {
window.addEventListener( 'popstate', callback );
}

addPushState( pageName: string ) {
window.history.pushState( pageName, null, null );
this.hasPushState = true;
}

back(): void {
if ( !this.hasPushState ) {
return;
}

this.hasPushState = false;
window.history.back();
}
}
36 changes: 28 additions & 8 deletions tests/unit/components/pages/donation_form/DonationForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,17 @@ import DonationForm from '@src/components/pages/DonationForm.vue';
import countries from '@src/../tests/data/countries';
import { AddressValidation } from '@src/view_models/Validation';
import { createFeatureToggle } from '@src/util/createFeatureToggle';

declare global {
namespace NodeJS {
interface Global {
window: Window;
}
}
}
import { HistoryHijacker } from '@src/util/HistoryHijacker';

const PaymentPage = { template: '<div class="i-am-payment" />' };
const AddressPage = { template: '<div class="i-am-address-form" />' };

describe( 'DonationForm.vue', () => {
let historyHijacker: HistoryHijacker;

beforeEach( () => {
global.window.scrollTo = jest.fn();
historyHijacker = { addHistoryCallback: jest.fn(), addPushState: jest.fn(), back: jest.fn() };
} );

const getWrapper = ( startPageIndex: number = 0 ): VueWrapper<any> => {
Expand All @@ -38,6 +33,7 @@ describe( 'DonationForm.vue', () => {
salutations: [],
addressValidationPatterns: {} as AddressValidation,
startPageIndex,
historyHijacker,
},
global: {
stubs: {
Expand Down Expand Up @@ -103,4 +99,28 @@ describe( 'DonationForm.vue', () => {
expect( global.window.scrollTo ).toHaveBeenCalledTimes( 2 );
} );

it( 'sets a history hijack callback when mounted', async () => {
getWrapper( 0 );

expect( historyHijacker.addHistoryCallback ).toHaveBeenCalledTimes( 1 );
} );

it( 'adds history hijack push state when payment page is submitted', async () => {
const wrapper = getWrapper( 0 );

const paymentPage = wrapper.findComponent( PaymentPage );
await paymentPage.vm.$emit( 'next-page' );

expect( historyHijacker.addPushState ).toHaveBeenCalledTimes( 1 );
} );

it( 'calls history hijack back when donor clicks back link', async () => {
const wrapper = getWrapper( 1 );

const addressPage = wrapper.findComponent( AddressPage );
await addressPage.vm.$emit( 'previous-page' );

expect( historyHijacker.back ).toHaveBeenCalledTimes( 1 );
} );

} );
Loading
Loading