From 5818f433e788ba8bb8de415be407b7fda422cfe9 Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Tue, 19 Nov 2024 16:42:23 -0800 Subject: [PATCH 01/14] Change to dev.v2.js for local development --- branded-checkout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/branded-checkout.html b/branded-checkout.html index cf436fd12..5874ff654 100644 --- a/branded-checkout.html +++ b/branded-checkout.html @@ -12,7 +12,7 @@ - + From 874f3fd388612877890122d8c27c39a1859b0c9e Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Tue, 19 Nov 2024 16:42:58 -0800 Subject: [PATCH 02/14] Add use-v3 config variable --- branded-checkout.html | 2 +- src/app/branded/branded-checkout.component.js | 3 ++- src/app/branded/branded-checkout.tpl.html | 3 ++- src/app/branded/step-1/branded-checkout-step-1.component.js | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/branded-checkout.html b/branded-checkout.html index 5874ff654..29f334990 100644 --- a/branded-checkout.html +++ b/branded-checkout.html @@ -11,7 +11,7 @@ - + diff --git a/src/app/branded/branded-checkout.component.js b/src/app/branded/branded-checkout.component.js index 065ac52df..3c7b6e26a 100644 --- a/src/app/branded/branded-checkout.component.js +++ b/src/app/branded/branded-checkout.component.js @@ -181,6 +181,7 @@ export default angular onOrderCompleted: '&', onOrderFailed: '&', language: '@', - showCoverFees: '@' + showCoverFees: '@', + useV3: '@', } }) diff --git a/src/app/branded/branded-checkout.tpl.html b/src/app/branded/branded-checkout.tpl.html index edf52f85a..1b04adde4 100644 --- a/src/app/branded/branded-checkout.tpl.html +++ b/src/app/branded/branded-checkout.tpl.html @@ -17,7 +17,8 @@ next="$ctrl.next()" on-payment-failed="$ctrl.onPaymentFailed($event.donorDetails)" radio-station-api-url="$ctrl.radioStationApiUrl" - radio-station-radius="$ctrl.radioStationRadius"> + radio-station-radius="$ctrl.radioStationRadius" + use-v3="$ctrl.useV3"> Date: Wed, 27 Nov 2024 15:34:03 -0500 Subject: [PATCH 03/14] EP-2519 - Add styles for reordering gift selection on useV3. (#1118) Add styles for reordering gift selection with useV3 flag. --- src/app/branded/branded-checkout.component.js | 2 +- .../branded-checkout-step-1.component.js | 2 +- .../step-1/branded-checkout-step-1.tpl.html | 5 +- .../productConfigForm.component.js | 3 +- .../productConfigForm.tpl.html | 284 +++++++++--------- src/assets/scss/_gift-config.scss | 8 +- 6 files changed, 155 insertions(+), 149 deletions(-) diff --git a/src/app/branded/branded-checkout.component.js b/src/app/branded/branded-checkout.component.js index 3c7b6e26a..4011bb267 100644 --- a/src/app/branded/branded-checkout.component.js +++ b/src/app/branded/branded-checkout.component.js @@ -182,6 +182,6 @@ export default angular onOrderFailed: '&', language: '@', showCoverFees: '@', - useV3: '@', + useV3: '@' } }) diff --git a/src/app/branded/step-1/branded-checkout-step-1.component.js b/src/app/branded/step-1/branded-checkout-step-1.component.js index ae84fbe0a..017f8e1ed 100644 --- a/src/app/branded/step-1/branded-checkout-step-1.component.js +++ b/src/app/branded/step-1/branded-checkout-step-1.component.js @@ -189,6 +189,6 @@ export default angular onPaymentFailed: '&', radioStationApiUrl: '<', radioStationRadius: '<', - useV3: '<', + useV3: '<' } }) diff --git a/src/app/branded/step-1/branded-checkout-step-1.tpl.html b/src/app/branded/step-1/branded-checkout-step-1.tpl.html index e2f85b2e2..6d6ff6fc8 100644 --- a/src/app/branded/step-1/branded-checkout-step-1.tpl.html +++ b/src/app/branded/step-1/branded-checkout-step-1.tpl.html @@ -11,7 +11,10 @@ submitted="$ctrl.submitted" on-state-change="$ctrl.onGiftConfigStateChange(state)" disable-session-restart="true" - ng-if="!$ctrl.loadingProductConfig && !$ctrl.errorLoadingProductConfig"> + ng-if="!$ctrl.loadingProductConfig && !$ctrl.errorLoadingProductConfig" + use-v3="$ctrl.useV3" + > +
-
+
+
- + - + - + - + -

- {{'GIFT_AMOUNT'}} -

-
-
-
- -
-
-
+

+ {{'GIFT_AMOUNT'}} +

+
+
-
-
{{'VALID_DOLLAR_AMOUNT_ERROR'}}
-
{{'AMOUNT_EMPTY_ERROR'}}
-
{{'AMOUNT_MIN_ERROR'}}
-
- {{'AMOUNT_MAX_ERROR'}} +
+
+
+ +
+
{{'VALID_DOLLAR_AMOUNT_ERROR'}}
+
{{'AMOUNT_EMPTY_ERROR'}}
+
{{'AMOUNT_MIN_ERROR'}}
+
+ {{'AMOUNT_MAX_ERROR'}} +
-
-
- - - -
-
{{'VALID_DOLLAR_AMOUNT_ERROR'}}
-
{{'AMOUNT_EMPTY_ERROR'}}
-
-
+
+ + + +
+
{{'VALID_DOLLAR_AMOUNT_ERROR'}}
+
{{'AMOUNT_EMPTY_ERROR'}}
+
+
+
+
+
+

+ {{'GIFT_FREQUENCY'}} +

+
+
+ +
+ + {{'CHANGING_FREQUENCY'}} +
-
-

- {{'GIFT_FREQUENCY'}} -

-
-
- - -
- - {{'CHANGING_FREQUENCY'}} - -
-
-
-
-

- {{'RECURRING_START'}} -

-
-
-
-
- +
+
+

+ {{'RECURRING_START'}} +

+
+
+
+
+ +
-
-
-
- +
+
+ +
-
-
-
- +
- {{$ctrl.startMonth( $ctrl.itemConfig['RECURRING_DAY_OF_MONTH'], $ctrl.itemConfig['RECURRING_START_MONTH'], $ctrl.nextDrawDate ).format('ll')}} + +
+ {{$ctrl.startMonth( $ctrl.itemConfig['RECURRING_DAY_OF_MONTH'], $ctrl.itemConfig['RECURRING_START_MONTH'], $ctrl.nextDrawDate ).format('ll')}} +
-
-
-
+
+
+

diff --git a/src/assets/scss/_gift-config.scss b/src/assets/scss/_gift-config.scss index 8888cf81c..fc1ff1f0a 100644 --- a/src/assets/scss/_gift-config.scss +++ b/src/assets/scss/_gift-config.scss @@ -25,8 +25,6 @@ label.custom-amount { width: auto; } - - .radio, .checkbox { label { @@ -217,6 +215,11 @@ label.btn.btn-default-form.active { } } +.give-selection-reverse-order { + display: flex; + flex-direction: column-reverse; +} + @media (max-width: 549px) { .give-modal-recipient { margin-top: 5px; @@ -340,4 +343,3 @@ label.btn.btn-default-form.active { } } } - From 41840af2a3fb818b1d26ba68a9720f99aebf4a8d Mon Sep 17 00:00:00 2001 From: Will James Date: Mon, 16 Dec 2024 12:00:11 -0500 Subject: [PATCH 04/14] EP-2525 - Hide unnecessary fields (#1120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Hides Middle Initial, ‘Optional’ section, Additional your information header, Suffix, Phone Number and Spouse information based on useV3 flag. --- .../branded/step-1/branded-checkout-step-1.tpl.html | 4 ++-- .../productConfigForm/productConfigForm.tpl.html | 2 +- .../components/contactInfo/contactInfo.component.js | 3 ++- .../components/contactInfo/contactInfo.tpl.html | 13 ++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/branded/step-1/branded-checkout-step-1.tpl.html b/src/app/branded/step-1/branded-checkout-step-1.tpl.html index 6d6ff6fc8..aa89f5f28 100644 --- a/src/app/branded/step-1/branded-checkout-step-1.tpl.html +++ b/src/app/branded/step-1/branded-checkout-step-1.tpl.html @@ -14,7 +14,6 @@ ng-if="!$ctrl.loadingProductConfig && !$ctrl.errorLoadingProductConfig" use-v3="$ctrl.useV3" > -

diff --git a/src/app/productConfig/productConfigForm/productConfigForm.tpl.html b/src/app/productConfig/productConfigForm/productConfigForm.tpl.html index 10fa6509b..279ef2f70 100644 --- a/src/app/productConfig/productConfigForm/productConfigForm.tpl.html +++ b/src/app/productConfig/productConfigForm/productConfigForm.tpl.html @@ -214,7 +214,7 @@

-
+

{{'OPTIONAL'}}

diff --git a/src/common/components/contactInfo/contactInfo.component.js b/src/common/components/contactInfo/contactInfo.component.js index 467f7226c..0ca566144 100644 --- a/src/common/components/contactInfo/contactInfo.component.js +++ b/src/common/components/contactInfo/contactInfo.component.js @@ -191,6 +191,7 @@ export default angular donorDetails: '=?', onSubmit: '&', radioStationApiUrl: '<', - radioStationRadius: '<' + radioStationRadius: '<', + useV3: '<' } }) diff --git a/src/common/components/contactInfo/contactInfo.tpl.html b/src/common/components/contactInfo/contactInfo.tpl.html index 585c7db32..43cdcfe14 100644 --- a/src/common/components/contactInfo/contactInfo.tpl.html +++ b/src/common/components/contactInfo/contactInfo.tpl.html @@ -45,7 +45,7 @@
-

{{'YOUR_INFORMATION'}}

+

{{'YOUR_INFORMATION'}}

@@ -65,7 +65,7 @@

{{'YOUR_INFORMAT

-
+
-
+
-
+
@@ -239,7 +238,7 @@

{{'CONTACT_INFO'

-
+
-
-
-

- {{'RECURRING_START'}} -

-
-
-
-
- -
+
+

+ {{'RECURRING_START'}} +

+
+
+
+
+
-
-
- -
+
+
+
+
-
+
+
+
+
- -
- {{$ctrl.startMonth( $ctrl.itemConfig['RECURRING_DAY_OF_MONTH'], $ctrl.itemConfig['RECURRING_START_MONTH'], $ctrl.nextDrawDate ).format('ll')}} -
+ {{$ctrl.startMonth( $ctrl.itemConfig['RECURRING_DAY_OF_MONTH'], $ctrl.itemConfig['RECURRING_START_MONTH'], $ctrl.nextDrawDate ).format('ll')}}
-
-
-
+
+
+

diff --git a/src/assets/scss/_gift-config.scss b/src/assets/scss/_gift-config.scss index fc1ff1f0a..8e84a8d20 100644 --- a/src/assets/scss/_gift-config.scss +++ b/src/assets/scss/_gift-config.scss @@ -217,7 +217,16 @@ label.btn.btn-default-form.active { .give-selection-reverse-order { display: flex; - flex-direction: column-reverse; + flex-direction: column; + & > .panel.panel-default:nth-child(1) { + order: 3; + } + & > .panel.panel-default:nth-child(2) { + order: 1; + } + & > .panel.panel-default:nth-child(3) { + order: 2; + } } @media (max-width: 549px) { From 1522b6aaa8ac82f4bf196e1773be94d43b0d65e2 Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Wed, 18 Dec 2024 17:41:01 -0800 Subject: [PATCH 06/14] Add submitOrder() service --- src/common/services/api/order.service.js | 52 ++++++ src/common/services/api/order.service.spec.js | 155 ++++++++++++++++++ 2 files changed, 207 insertions(+) diff --git a/src/common/services/api/order.service.js b/src/common/services/api/order.service.js index fcbb2ad41..136b21a9b 100644 --- a/src/common/services/api/order.service.js +++ b/src/common/services/api/order.service.js @@ -6,10 +6,14 @@ import 'rxjs/add/operator/combineLatest' import 'rxjs/add/operator/pluck' import 'rxjs/add/observable/of' import 'rxjs/add/observable/throw' +import 'rxjs/add/operator/do' +import 'rxjs/add/operator/finally' import map from 'lodash/map' import omit from 'lodash/omit' +import isString from 'lodash/isString' import sortPaymentMethods from 'common/services/paymentHelpers/paymentMethodSort' import extractPaymentAttributes from 'common/services/paymentHelpers/extractPaymentAttributes' +import { cartUpdatedEvent } from 'common/lib/cartEvents' import cortexApiService from '../cortexApi.service' import cartService from './cart.service' @@ -35,6 +39,7 @@ class Order { this.analyticsFactory = analyticsFactory this.sessionStorage = $window.sessionStorage this.localStorage = $window.localStorage + this.$window = $window this.$log = $log this.$filter = $filter } @@ -399,6 +404,53 @@ class Order { return startedOrderWithoutSpouse } } + + submitOrder (controller) { + delete controller.submissionError + delete controller.submissionErrorStatus + // Prevent multiple submissions + if (controller.submittingOrder) { + return Observable.empty() + } + controller.submittingOrder = true + controller.onSubmittingOrder({ value: true }) + + let submitRequest + if (controller.bankAccountPaymentDetails) { + submitRequest = this.submit() + } else if (controller.creditCardPaymentDetails) { + const cvv = this.retrieveCardSecurityCode() + submitRequest = this.submit(cvv) + } else { + submitRequest = Observable.throw({ data: 'Current payment type is unknown' }) + } + return submitRequest + .do(() => { + this.clearCardSecurityCodes() + this.clearCoverFees() + controller.onSubmitted() + controller.$scope.$emit(cartUpdatedEvent) + }, + (error) => { + // Handle the error side effects when the observable errors + this.analyticsFactory.checkoutFieldError('submitOrder', 'failed') + + controller.loadCart() + + if (error.config && error.config.data && error.config.data['security-code']) { + error.config.data['security-code'] = error.config.data['security-code'].replace(/./g, 'X') // Mask security-code + } + this.$log.error('Error submitting purchase:', error) + controller.onSubmitted() + controller.submissionErrorStatus = error.status + controller.submissionError = isString(error && error.data) ? (error && error.data).replace(/[:].*$/, '') : 'generic error' // Keep prefix before first colon for easier ng-switch matching + this.$window.scrollTo(0, 0) + }) + .finally(() => { + controller.submittingOrder = false + controller.onSubmittingOrder({ value: false }) + }); + } } export default angular diff --git a/src/common/services/api/order.service.spec.js b/src/common/services/api/order.service.spec.js index 8be9e6019..91ca7bb5b 100644 --- a/src/common/services/api/order.service.spec.js +++ b/src/common/services/api/order.service.spec.js @@ -4,6 +4,7 @@ import omit from 'lodash/omit' import cloneDeep from 'lodash/cloneDeep' import { Observable } from 'rxjs/Observable' import 'rxjs/add/observable/of' +import 'rxjs/add/observable/empty' import formatAddressForTemplate from '../addressHelpers/formatAddressForTemplate' import { Roles } from 'common/services/session/session.service' @@ -17,6 +18,7 @@ import purchaseFormResponse from 'common/services/api/fixtures/cortex-order-purc import donorDetailsResponse from 'common/services/api/fixtures/cortex-donordetails.fixture.js' import needInfoResponse from 'common/services/api/fixtures/cortex-order-needinfo.fixture.js' import purchaseResponse from 'common/services/api/fixtures/cortex-purchase.fixture.js' +import { cartUpdatedEvent } from 'common/lib/cartEvents' describe('order service', () => { beforeEach(angular.mock.module(module.name)) @@ -1206,4 +1208,157 @@ describe('order service', () => { expect(self.$window.localStorage.getItem('currentOrder')).toEqual('order id 2') }) }) + + describe('submitOrder', () => { + let mockController + + beforeEach(() => { + mockController = { + submittingOrder: false, + onSubmittingOrder: jest.fn(), + onSubmitted: jest.fn(), + $scope: { + $emit: jest.fn(), + }, + } + + // Mock the submit() method to return a resolved observable + self.orderService.submit = jest.fn().mockReturnValue(Observable.of({})) + }) + + describe('another order submission in progress', () => { + it('should not submit the order twice', () => { + mockController.submittingOrder = true + // Call submitOrder + const result = self.orderService.submitOrder(mockController) + + // The submit method should not be called + expect(self.orderService.submit).not.toHaveBeenCalled() + + // It should return an empty observable + expect(result).toEqual(Observable.empty()) + }) + }) + + describe('submit single order', () => { + beforeEach(() => { + self.orderService.clearCardSecurityCodes = jest.fn() + self.orderService.retrieveCardSecurityCode = jest.fn() + self.orderService.clearCoverFees = jest.fn() + mockController.loadCart = jest.fn() + self.$window.scrollTo = jest.fn() + }) + + afterEach(() => { + expect(mockController.onSubmittingOrder).toHaveBeenCalledWith({ value: true }) + expect(mockController.onSubmittingOrder).toHaveBeenCalledWith({ value: false }) + expect(mockController.onSubmitted).toHaveBeenCalled() + }) + + it('should submit the order normally if paying with a bank account', (done) => { + mockController.bankAccountPaymentDetails = {} + self.orderService.submitOrder(mockController).subscribe( + () => { + expect(self.orderService.submit).toHaveBeenCalled() + expect(self.orderService.clearCardSecurityCodes).toHaveBeenCalled() + + expect(mockController.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent) + done() + }) + }) + + it('should handle an error submitting an order with a bank account', (done) => { + self.orderService.submit.mockImplementation(() => Observable.throw({ data: 'error saving bank account' })) + + mockController.bankAccountPaymentDetails = {} + self.orderService.submitOrder(mockController).subscribe( + () => done("Observable unexpectedly succeeded"), + () => { + // Handle the error and continue with assertions + expect(self.orderService.submit).toHaveBeenCalled() + expect(mockController.loadCart).toHaveBeenCalled() + expect(self.orderService.clearCardSecurityCodes).not.toHaveBeenCalled() + expect(self.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'error saving bank account' }]) + expect(mockController.submissionError).toEqual('error saving bank account') + expect(self.$window.scrollTo).toHaveBeenCalledWith(0, 0) + + done() + }) + }) + + it('should submit the order with a CVV if paying with a credit card', (done) => { + self.orderService.retrieveCardSecurityCode = jest.fn().mockReturnValue('1234') + mockController.creditCardPaymentDetails = {} + mockController.coverFeeDecision = true + self.orderService.submitOrder(mockController).subscribe(() => { + expect(self.orderService.submit).toHaveBeenCalledWith('1234') + expect(self.orderService.clearCardSecurityCodes).toHaveBeenCalled() + expect(mockController.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent) + done() + }) + }) + + it('should submit the order without a CVV if paying with an existing credit card or the cvv in session storage is missing', (done) => { + mockController.creditCardPaymentDetails = {} + self.orderService.retrieveCardSecurityCode = jest.fn().mockReturnValue(undefined) + mockController.coverFeeDecision = true + self.orderService.submitOrder(mockController).subscribe(() => { + expect(self.orderService.submit).toHaveBeenCalledWith(undefined) + expect(self.orderService.clearCardSecurityCodes).toHaveBeenCalled() + expect(mockController.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent) + done() + }, done) + }) + + it('should handle an error submitting an order with a credit card', (done) => { + self.orderService.submit.mockImplementation(() => Observable.throw({ data: 'CardErrorException: Invalid Card Number: some details' })) + mockController.creditCardPaymentDetails = {} + self.orderService.retrieveCardSecurityCode = jest.fn().mockReturnValue('1234') + self.orderService.submitOrder(mockController).subscribe( + () => done("Observable unexpectedly succeeded"), + () => { // error handler + expect(self.orderService.submit).toHaveBeenCalledWith('1234') + expect(self.orderService.clearCardSecurityCodes).not.toHaveBeenCalled() + expect(self.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'CardErrorException: Invalid Card Number: some details' }]) + expect(mockController.submissionError).toEqual('CardErrorException') + expect(self.$window.scrollTo).toHaveBeenCalledWith(0, 0) + done() + }) + }) + + it('should mask the security code on a credit card error', (done) => { + self.orderService.submit.mockReturnValue(Observable.throw({ data: 'some error', config: { data: { 'security-code': '1234' } } })) + self.orderService.retrieveCardSecurityCode = jest.fn().mockReturnValue('1234') + mockController.creditCardPaymentDetails = {} + self.orderService.submitOrder(mockController).subscribe( + () => done("Observable unexpectedly succeeded"), + () => { // error handler + expect(self.orderService.submit).toHaveBeenCalledWith('1234') + expect(self.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'some error', config: { data: { 'security-code': 'XXXX' } } }]) + done() + }) + }) + + it('should throw an error if neither bank account or credit card details are loaded', (done) => { + self.orderService.submitOrder(mockController).subscribe( + () => done("Observable unexpectedly succeeded"), + () => { // error handler + expect(self.orderService.submit).not.toHaveBeenCalled() + expect(self.orderService.clearCardSecurityCodes).not.toHaveBeenCalled() + expect(self.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'Current payment type is unknown' }]) + expect(mockController.submissionError).toEqual('Current payment type is unknown') + expect(self.$window.scrollTo).toHaveBeenCalledWith(0, 0) + done() + }) + }) + + it('should clear out cover fee data', (done) => { + mockController.creditCardPaymentDetails = {} + self.orderService.submitOrder(mockController).subscribe(() => { + done() + }, done) + expect(self.orderService.clearCoverFees).toHaveBeenCalled() + }) + }) + }) }) From 70f2559702fc48bfe6ffd063b285602423474939 Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Wed, 18 Dec 2024 17:50:10 -0800 Subject: [PATCH 07/14] Add submitOrderInternal and helper functions to branded checkout step 1 --- .../branded-checkout-step-1.component.js | 116 +++++++++++++++++- 1 file changed, 113 insertions(+), 3 deletions(-) diff --git a/src/app/branded/step-1/branded-checkout-step-1.component.js b/src/app/branded/step-1/branded-checkout-step-1.component.js index ae84fbe0a..a6d16d249 100644 --- a/src/app/branded/step-1/branded-checkout-step-1.component.js +++ b/src/app/branded/step-1/branded-checkout-step-1.component.js @@ -7,19 +7,25 @@ import checkoutStep2 from 'app/checkout/step-2/step-2.component' import cartService from 'common/services/api/cart.service' import orderService from 'common/services/api/order.service' +import analyticsFactory from '../../analytics/analytics.factory' import brandedAnalyticsFactory from '../../branded/analytics/branded-analytics.factory' - import { FEE_DERIVATIVE } from 'common/components/paymentMethods/coverFees/coverFees.component' import template from './branded-checkout-step-1.tpl.html' +import 'rxjs/add/operator/catch' +import 'rxjs/add/operator/do' +import 'rxjs/add/operator/finally' const componentName = 'brandedCheckoutStep1' class BrandedCheckoutStep1Controller { /* @ngInject */ - constructor ($log, $filter, brandedAnalyticsFactory, cartService, orderService) { + constructor ($scope, $log, $filter, $window, analyticsFactory, brandedAnalyticsFactory, cartService, orderService) { + this.$scope = $scope this.$log = $log this.$filter = $filter + this.$window = $window + this.analyticsFactory = analyticsFactory this.brandedAnalyticsFactory = brandedAnalyticsFactory this.cartService = cartService this.orderService = orderService @@ -154,12 +160,112 @@ class BrandedCheckoutStep1Controller { checkSuccessfulSubmission () { if (every(this.submission, 'completed')) { if (every(this.submission, { error: false })) { - this.next() + if (this.useV3 === 'true') { + this.submitOrderInternal() + } else { + this.next() + } } else { this.submitted = false } } } + + loadCart () { + this.errorLoadingCart = false + + const cart = this.cartService.get() + cart.subscribe( + data => { + // Setting cart data and analytics + this.cartData = data + this.brandedAnalyticsFactory.saveCoverFees(this.orderService.retrieveCoverFeeDecision()) + if (this.cartData && this.cartData.items) { + this.brandedAnalyticsFactory.saveItem(this.cartData.items[0]) + } + this.brandedAnalyticsFactory.addPaymentInfo() + }, + error => { + // Handle errors by setting flag and logging the error + this.errorLoadingCart = true + this.$log.error('Error loading cart data for branded checkout (single step)', error) + } + ) + return cart + } + + loadCurrentPayment () { + this.loadingCurrentPayment = true + + const getCurrentPayment = this.orderService.getCurrentPayment() + getCurrentPayment.finally(() => { + this.loadingCurrentPayment = false + }).subscribe( + data => { + if (!data) { + this.$log.error('Error loading current payment info: current payment doesn\'t seem to exist') + } else if (data['account-type']) { + this.bankAccountPaymentDetails = data + } else if (data['card-type']) { + this.creditCardPaymentDetails = data + } else { + this.$log.error('Error loading current payment info: current payment type is unknown') + } + }, + error => { + this.$log.error('Error loading current payment info', error) + } + ) + return getCurrentPayment + } + + checkErrors () { + // Then check for errors on the API + return this.orderService.checkErrors().do( + (data) => { + this.needinfoErrors = data + }) + .catch(error => { + this.$log.error('Error loading checkErrors', error) + }) + } + + submitOrderInternal () { + this.loadingAndSubmitting = true + this.loadCart() + .mergeMap(() => { + return this.loadCurrentPayment() + }) + .mergeMap(() => { + return this.checkErrors() + }) + .mergeMap(() => { + return this.orderService.submitOrder(this) + }) + .finally(() => { + this.loadingAndSubmitting = false + }) + .subscribe(() => { + this.next() + }) + } + + handleRecaptchaFailure () { + this.analyticsFactory.checkoutFieldError('submitOrder', 'failed') + this.submittingOrder = false + this.loadingAndSubmitting = false + this.onSubmittingOrder({ value: false }) + + this.loadCart() + + this.onSubmitted() + this.submissionError = 'generic error' + this.$window.scrollTo(0, 0) + } + + canSubmitOrder () { + return !this.submittingOrder + } } export default angular @@ -169,6 +275,7 @@ export default angular checkoutStep2.name, cartService.name, orderService.name, + analyticsFactory.name, brandedAnalyticsFactory.name ]) .component(componentName, { @@ -189,6 +296,9 @@ export default angular onPaymentFailed: '&', radioStationApiUrl: '<', radioStationRadius: '<', + onSubmittingOrder: '&', + onSubmitted: '&', useV3: '<', + loadingAndSubmitting: '<' } }) From 315bd4803d3951bb41ae52b0e576fe47212a2a72 Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Wed, 18 Dec 2024 17:53:56 -0800 Subject: [PATCH 08/14] Remove submitOrderInternal() from step 3 and use orderService.submitOrder instead --- src/app/checkout/step-3/step-3.component.js | 55 +----- .../checkout/step-3/step-3.component.spec.js | 175 ++++-------------- src/app/checkout/step-3/step-3.tpl.html | 2 +- 3 files changed, 44 insertions(+), 188 deletions(-) diff --git a/src/app/checkout/step-3/step-3.component.js b/src/app/checkout/step-3/step-3.component.js index 76d955ed8..280a9bd66 100644 --- a/src/app/checkout/step-3/step-3.component.js +++ b/src/app/checkout/step-3/step-3.component.js @@ -1,6 +1,4 @@ import angular from 'angular' -import isString from 'lodash/isString' -import { Observable } from 'rxjs/Observable' import 'rxjs/add/observable/throw' import displayAddressComponent from 'common/components/display-address/display-address.component' @@ -12,7 +10,6 @@ import orderService from 'common/services/api/order.service' import profileService from 'common/services/api/profile.service' import capitalizeFilter from 'common/filters/capitalize.filter' import desigSrcDirective from 'common/directives/desigSrc.directive' -import { cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component' import { SignInEvent } from 'common/services/session/session.service' import { startDate } from 'common/services/giftHelpers/giftDates.service' import recaptchaComponent from 'common/components/Recaptcha/RecaptchaWrapper' @@ -38,7 +35,6 @@ class Step3Controller { this.commonService = commonService this.startDate = startDate this.sessionStorage = $window.sessionStorage - this.selfReference = this this.isBranded = envService.read('isBrandedCheckout') this.$scope.$on(SignInEvent, () => { @@ -46,7 +42,7 @@ class Step3Controller { }) this.$rootScope.$on(recaptchaFailedEvent, () => { - this.handleRecaptchaFailure(this) + this.handleRecaptchaFailure() }) this.$rootScope.$on(submitOrderEvent, () => { this.submitOrder() @@ -134,51 +130,12 @@ class Step3Controller { } submitOrder () { - this.submitOrderInternal(this) - } - - submitOrderInternal (componentInstance) { - delete componentInstance.submissionError - delete componentInstance.submissionErrorStatus - // Prevent multiple submissions - if (componentInstance.submittingOrder) return - componentInstance.submittingOrder = true - componentInstance.onSubmittingOrder({ value: true }) - - let submitRequest - if (componentInstance.bankAccountPaymentDetails) { - submitRequest = componentInstance.orderService.submit() - } else if (componentInstance.creditCardPaymentDetails) { - const cvv = componentInstance.orderService.retrieveCardSecurityCode() - submitRequest = componentInstance.orderService.submit(cvv) - } else { - submitRequest = Observable.throw({ data: 'Current payment type is unknown' }) - } - submitRequest.subscribe(() => { - componentInstance.analyticsFactory.purchase(componentInstance.donorDetails, componentInstance.cartData, componentInstance.orderService.retrieveCoverFeeDecision()) - componentInstance.submittingOrder = false - componentInstance.onSubmittingOrder({ value: false }) - componentInstance.orderService.clearCardSecurityCodes() - componentInstance.orderService.clearCoverFees() - componentInstance.onSubmitted() - componentInstance.$scope.$emit(cartUpdatedEvent) - componentInstance.changeStep({ newStep: 'thankYou' }) - }, - error => { - componentInstance.analyticsFactory.checkoutFieldError('submitOrder', 'failed') - componentInstance.submittingOrder = false - componentInstance.onSubmittingOrder({ value: false }) - - componentInstance.loadCart() - - if (error.config && error.config.data && error.config.data['security-code']) { - error.config.data['security-code'] = error.config.data['security-code'].replace(/./g, 'X') // Mask security-code + this.orderService.submitOrder(this).subscribe(() => { + if (!this.isBranded) { + // Branded checkout submits its purchase analytics event on the thank you page + this.analyticsFactory.purchase(this.donorDetails, this.cartData, this.orderService.retrieveCoverFeeDecision()) } - componentInstance.$log.error('Error submitting purchase:', error) - componentInstance.onSubmitted() - componentInstance.submissionErrorStatus = error.status - componentInstance.submissionError = isString(error && error.data) ? (error && error.data).replace(/[:].*$/, '') : 'generic error' // Keep prefix before first colon for easier ng-switch matching - componentInstance.$window.scrollTo(0, 0) + this.changeStep({ newStep: 'thankYou' }) }) } diff --git a/src/app/checkout/step-3/step-3.component.spec.js b/src/app/checkout/step-3/step-3.component.spec.js index e40f0d1ca..21a543241 100644 --- a/src/app/checkout/step-3/step-3.component.spec.js +++ b/src/app/checkout/step-3/step-3.component.spec.js @@ -4,7 +4,6 @@ import { Observable } from 'rxjs/Observable' import 'rxjs/add/observable/of' import 'rxjs/add/observable/throw' -import { cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component' import { SignInEvent } from 'common/services/session/session.service' import module from './step-3.component' @@ -37,6 +36,7 @@ describe('checkout', () => { getCurrentPayment: () => Observable.of(self.loadedPayment), checkErrors: () => Observable.of(['email-info']), submit: () => Observable.of('called submit'), + submitOrder: () => Observable.of('called submitOrder'), retrieveCardSecurityCode: () => self.storedCvv, retrieveLastPurchaseLink: () => Observable.of('purchaseLink'), retrieveCoverFeeDecision: () => self.coverFeeDecision, @@ -323,144 +323,18 @@ describe('checkout', () => { }) }) - describe('submitOrder', () => { - beforeEach(() => { - jest.spyOn(self.controller.orderService, 'submit') - jest.spyOn(self.controller.profileService, 'getPurchase') - jest.spyOn(self.controller.analyticsFactory, 'purchase') - }) - - describe('another order submission in progress', () => { - it('should not submit the order twice', () => { - self.controller.submittingOrder = true - self.controller.submitOrder() - - expect(self.controller.onSubmittingOrder).not.toHaveBeenCalled() - expect(self.controller.onSubmitted).not.toHaveBeenCalled() - }) - }) - - describe('submit single order', () => { - beforeEach(() => { - jest.spyOn(self.controller.$scope, '$emit').mockImplementation(() => {}) - }) - - afterEach(() => { - expect(self.controller.onSubmittingOrder).toHaveBeenCalledWith({ value: true }) - expect(self.controller.onSubmittingOrder).toHaveBeenCalledWith({ value: false }) - expect(self.controller.onSubmitted).toHaveBeenCalled() - }) - - it('should submit the order normally if paying with a bank account', () => { - self.controller.bankAccountPaymentDetails = {} - self.controller.submitOrder() - - expect(self.controller.orderService.submit).toHaveBeenCalled() - expect(self.controller.analyticsFactory.purchase).toHaveBeenCalledWith(self.controller.donorDetails, self.controller.cartData, self.coverFeeDecision) - expect(self.controller.orderService.clearCardSecurityCodes).toHaveBeenCalled() - expect(self.controller.changeStep).toHaveBeenCalledWith({ newStep: 'thankYou' }) - expect(self.controller.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent) - }) - - it('should handle an error submitting an order with a bank account', () => { - self.controller.orderService.submit.mockImplementation(() => Observable.throw({ data: 'error saving bank account' })) - self.controller.bankAccountPaymentDetails = {} - self.controller.submitOrder() - - expect(self.controller.orderService.submit).toHaveBeenCalled() - expect(self.controller.loadCart).toHaveBeenCalled() - expect(self.controller.analyticsFactory.purchase).not.toHaveBeenCalled() - expect(self.controller.orderService.clearCardSecurityCodes).not.toHaveBeenCalled() - expect(self.controller.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'error saving bank account' }]) - expect(self.controller.changeStep).not.toHaveBeenCalled() - expect(self.controller.submissionError).toEqual('error saving bank account') - expect(self.controller.$window.scrollTo).toHaveBeenCalledWith(0, 0) - }) - - it('should submit the order with a CVV if paying with a credit card', () => { - self.controller.creditCardPaymentDetails = {} - self.storedCvv = '1234' - self.coverFeeDecision = true - self.controller.submitOrder() - - expect(self.controller.orderService.submit).toHaveBeenCalledWith('1234') - expect(self.controller.analyticsFactory.purchase).toHaveBeenCalledWith(self.controller.donorDetails, self.controller.cartData, self.coverFeeDecision) - expect(self.controller.orderService.clearCardSecurityCodes).toHaveBeenCalled() - expect(self.controller.changeStep).toHaveBeenCalledWith({ newStep: 'thankYou' }) - expect(self.controller.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent) - }) - - it('should submit the order without a CVV if paying with an existing credit card or the cvv in session storage is missing', () => { - self.controller.creditCardPaymentDetails = {} - self.storedCvv = undefined - self.coverFeeDecision = true - self.controller.submitOrder() - - expect(self.controller.orderService.submit).toHaveBeenCalledWith(undefined) - expect(self.controller.analyticsFactory.purchase).toHaveBeenCalledWith(self.controller.donorDetails, self.controller.cartData, self.coverFeeDecision) - expect(self.controller.orderService.clearCardSecurityCodes).toHaveBeenCalled() - expect(self.controller.changeStep).toHaveBeenCalledWith({ newStep: 'thankYou' }) - expect(self.controller.$scope.$emit).toHaveBeenCalledWith(cartUpdatedEvent) - }) - - it('should handle an error submitting an order with a credit card', () => { - self.controller.orderService.submit.mockImplementation(() => Observable.throw({ data: 'CardErrorException: Invalid Card Number: some details' })) - self.controller.creditCardPaymentDetails = {} - self.storedCvv = '1234' - self.controller.submitOrder() - - expect(self.controller.orderService.submit).toHaveBeenCalledWith('1234') - expect(self.controller.analyticsFactory.purchase).not.toHaveBeenCalled() - expect(self.controller.orderService.clearCardSecurityCodes).not.toHaveBeenCalled() - expect(self.controller.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'CardErrorException: Invalid Card Number: some details' }]) - expect(self.controller.changeStep).not.toHaveBeenCalled() - expect(self.controller.submissionError).toEqual('CardErrorException') - expect(self.controller.$window.scrollTo).toHaveBeenCalledWith(0, 0) - }) - - it('should mask the security code on a credit card error', () => { - self.controller.orderService.submit.mockReturnValue(Observable.throw({ data: 'some error', config: { data: { 'security-code': '1234' } } })) - self.controller.creditCardPaymentDetails = {} - self.storedCvv = '1234' - self.controller.submitOrder() - - expect(self.controller.orderService.submit).toHaveBeenCalledWith('1234') - expect(self.controller.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'some error', config: { data: { 'security-code': 'XXXX' } } }]) - }) - - it('should throw an error if neither bank account or credit card details are loaded', () => { - self.controller.submitOrder() - - expect(self.controller.orderService.submit).not.toHaveBeenCalled() - expect(self.controller.orderService.clearCardSecurityCodes).not.toHaveBeenCalled() - expect(self.controller.$log.error.logs[0]).toEqual(['Error submitting purchase:', { data: 'Current payment type is unknown' }]) - expect(self.controller.changeStep).not.toHaveBeenCalled() - expect(self.controller.submissionError).toEqual('Current payment type is unknown') - expect(self.controller.$window.scrollTo).toHaveBeenCalledWith(0, 0) - }) - - it('should clear out cover fee data', () => { - self.controller.creditCardPaymentDetails = {} - self.controller.submitOrder() - - expect(self.controller.orderService.clearCoverFees).toHaveBeenCalled() - }) - }) - }) - describe('handleRecaptchaFailure', () => { it('should show an error if recaptcha fails', () => { - const componentInstance = self.controller - jest.spyOn(componentInstance.analyticsFactory, 'checkoutFieldError').mockImplementation(() => {}) - self.controller.handleRecaptchaFailure(componentInstance) - - expect(componentInstance.analyticsFactory.checkoutFieldError).toHaveBeenCalledWith('submitOrder', 'failed') - expect(componentInstance.submittingOrder).toEqual(false) - expect(componentInstance.onSubmittingOrder).toHaveBeenCalledWith({ value: false }) - expect(componentInstance.loadCart).toHaveBeenCalled() - expect(componentInstance.onSubmitted).toHaveBeenCalled() - expect(componentInstance.submissionError).toEqual('generic error') - expect(componentInstance.$window.scrollTo).toHaveBeenCalledWith(0, 0) + jest.spyOn(self.controller.analyticsFactory, 'checkoutFieldError').mockImplementation(() => {}) + self.controller.handleRecaptchaFailure() + + expect(self.controller.analyticsFactory.checkoutFieldError).toHaveBeenCalledWith('submitOrder', 'failed') + expect(self.controller.submittingOrder).toEqual(false) + expect(self.controller.onSubmittingOrder).toHaveBeenCalledWith({ value: false }) + expect(self.controller.loadCart).toHaveBeenCalled() + expect(self.controller.onSubmitted).toHaveBeenCalled() + expect(self.controller.submissionError).toEqual('generic error') + expect(self.controller.$window.scrollTo).toHaveBeenCalledWith(0, 0) }) }) @@ -474,7 +348,32 @@ describe('checkout', () => { it('should call handleRecaptchaFailure if the recaptchaFailedEvent is received', () => { jest.spyOn(self.controller, 'handleRecaptchaFailure').mockImplementation(() => {}) self.controller.$rootScope.$emit(recaptchaFailedEvent) - expect(self.controller.handleRecaptchaFailure).toHaveBeenCalledWith(self.controller) + expect(self.controller.handleRecaptchaFailure).toHaveBeenCalled(); + }) + }) + + describe('submitOrder', () => { + beforeEach(() => { + jest.spyOn(self.controller.analyticsFactory, 'purchase') + jest.spyOn(self.controller.orderService, 'retrieveCoverFeeDecision').mockReturnValue(true) + }) + + it('should call analyticsFactory when it is not branded checkout', () => { + self.controller.isBranded = false + + self.controller.submitOrder() + + expect(self.controller.analyticsFactory.purchase).toHaveBeenCalledWith(self.controller.donorDetails, self.controller.cartData, self.controller.orderService.retrieveCoverFeeDecision()) + expect(self.controller.changeStep).toHaveBeenCalledWith({ newStep: 'thankYou' }) + }) + + it('should not call analyticsFactory when it is branded checkout', () => { + self.controller.isBranded = true + + self.controller.submitOrder() + + expect(self.controller.analyticsFactory.purchase).not.toHaveBeenCalled() + expect(self.controller.changeStep).toHaveBeenCalledWith({ newStep: 'thankYou' }) }) }) }) diff --git a/src/app/checkout/step-3/step-3.tpl.html b/src/app/checkout/step-3/step-3.tpl.html index 6e62128de..004827433 100644 --- a/src/app/checkout/step-3/step-3.tpl.html +++ b/src/app/checkout/step-3/step-3.tpl.html @@ -204,7 +204,7 @@
Date: Wed, 18 Dec 2024 17:58:14 -0800 Subject: [PATCH 09/14] Add recaptcha button and loading if using V3 --- src/app/branded/branded-checkout.component.js | 11 +++++++--- .../step-1/branded-checkout-step-1.tpl.html | 20 +++++++++++++++++-- src/common/components/loading/loading.scss | 7 +++++++ .../components/loading/loading.tpl.html | 5 +++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/app/branded/branded-checkout.component.js b/src/app/branded/branded-checkout.component.js index 3c7b6e26a..8b79056f5 100644 --- a/src/app/branded/branded-checkout.component.js +++ b/src/app/branded/branded-checkout.component.js @@ -75,8 +75,13 @@ class BrandedCheckoutController { next () { switch (this.checkoutStep) { case 'giftContactPayment': - this.checkoutStep = 'review' - this.fireAnalyticsEvents('review') + // If it is a single step form, the next step should be 'thankYou' + if (this.useV3 === 'true') { + this.checkoutStep = 'thankYou' + } else { + this.checkoutStep = 'review' + this.fireAnalyticsEvents('review') + } break case 'review': this.checkoutStep = 'thankYou' @@ -182,6 +187,6 @@ export default angular onOrderFailed: '&', language: '@', showCoverFees: '@', - useV3: '@', + useV3: '@' } }) diff --git a/src/app/branded/step-1/branded-checkout-step-1.tpl.html b/src/app/branded/step-1/branded-checkout-step-1.tpl.html index e2f85b2e2..8fd9bb70b 100644 --- a/src/app/branded/step-1/branded-checkout-step-1.tpl.html +++ b/src/app/branded/step-1/branded-checkout-step-1.tpl.html @@ -46,10 +46,26 @@

{{'PAYMENT'}}

-
+ + -
+

+ + {{'SUBMITTING_GIFT'}} + diff --git a/src/common/components/loading/loading.scss b/src/common/components/loading/loading.scss index 6e87dfc1e..40aa7f250 100644 --- a/src/common/components/loading/loading.scss +++ b/src/common/components/loading/loading.scss @@ -66,6 +66,13 @@ loading{ z-index: 2; } + .loading-fixed:first-child { + position: fixed; + top: 50%; + left: 50%; + z-index: 2; + } + .loadingWrap{ display: inline-block } diff --git a/src/common/components/loading/loading.tpl.html b/src/common/components/loading/loading.tpl.html index 3b6f33ff8..af5d5faef 100644 --- a/src/common/components/loading/loading.tpl.html +++ b/src/common/components/loading/loading.tpl.html @@ -1,6 +1,7 @@
From 4408c96f39f7ca7a7fefe4395258364aaf49afbb Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Wed, 18 Dec 2024 18:04:14 -0800 Subject: [PATCH 10/14] Remove componentInstance from Recaptcha and use apply() to make Angular rerender --- .../cart-summary/cart-summary.component.js | 11 ++++---- .../cart-summary/cart-summary.spec.js | 12 ++++---- src/app/checkout/step-3/step-3.component.js | 16 +++++------ src/app/checkout/step-3/step-3.tpl.html | 2 +- .../components/Recaptcha/Recaptcha.test.tsx | 1 - src/common/components/Recaptcha/Recaptcha.tsx | 19 ++++++------- .../Recaptcha/RecaptchaWrapper.test.tsx | 6 ++++ .../components/Recaptcha/RecaptchaWrapper.tsx | 28 ++++++++++++++----- 8 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/app/checkout/cart-summary/cart-summary.component.js b/src/app/checkout/cart-summary/cart-summary.component.js index 087f0ba97..eec5c7068 100644 --- a/src/app/checkout/cart-summary/cart-summary.component.js +++ b/src/app/checkout/cart-summary/cart-summary.component.js @@ -13,8 +13,9 @@ export const submitOrderEvent = 'submitOrderEvent' class CartSummaryController { /* @ngInject */ - constructor (cartService, $scope) { + constructor (cartService, $scope, $rootScope) { this.$scope = $scope + this.$rootScope = $rootScope this.cartService = cartService } @@ -22,12 +23,12 @@ class CartSummaryController { return this.cartService.buildCartUrl() } - handleRecaptchaFailure (componentInstance) { - componentInstance.$rootScope.$emit(recaptchaFailedEvent) + handleRecaptchaFailure () { + this.$rootScope.$emit(recaptchaFailedEvent) } - onSubmit (componentInstance) { - componentInstance.$rootScope.$emit(submitOrderEvent) + onSubmit () { + this.$rootScope.$emit(submitOrderEvent) } } diff --git a/src/app/checkout/cart-summary/cart-summary.spec.js b/src/app/checkout/cart-summary/cart-summary.spec.js index 5adf7ad7b..9d64e518c 100644 --- a/src/app/checkout/cart-summary/cart-summary.spec.js +++ b/src/app/checkout/cart-summary/cart-summary.spec.js @@ -31,17 +31,17 @@ describe('checkout', function () { describe('onSubmit', () => { it('should emit an event', () => { - jest.spyOn(componentInstance.$rootScope, '$emit').mockImplementation(() => {}) - self.controller.onSubmit(componentInstance) - expect(componentInstance.$rootScope.$emit).toHaveBeenCalledWith(submitOrderEvent) + jest.spyOn(self.controller.$rootScope, '$emit').mockImplementation(() => {}) + self.controller.onSubmit() + expect(self.controller.$rootScope.$emit).toHaveBeenCalledWith(submitOrderEvent) }) }) describe('handleRecaptchaFailure', () => { it('should emit an event', () => { - jest.spyOn(componentInstance.$rootScope, '$emit').mockImplementation(() => {}) - self.controller.handleRecaptchaFailure(componentInstance) - expect(componentInstance.$rootScope.$emit).toHaveBeenCalledWith(recaptchaFailedEvent) + jest.spyOn(self.controller.$rootScope, '$emit').mockImplementation(() => {}) + self.controller.handleRecaptchaFailure() + expect(self.controller.$rootScope.$emit).toHaveBeenCalledWith(recaptchaFailedEvent) }) }) }) diff --git a/src/app/checkout/step-3/step-3.component.js b/src/app/checkout/step-3/step-3.component.js index 280a9bd66..6745c4153 100644 --- a/src/app/checkout/step-3/step-3.component.js +++ b/src/app/checkout/step-3/step-3.component.js @@ -139,16 +139,16 @@ class Step3Controller { }) } - handleRecaptchaFailure (componentInstance) { - componentInstance.analyticsFactory.checkoutFieldError('submitOrder', 'failed') - componentInstance.submittingOrder = false - componentInstance.onSubmittingOrder({ value: false }) + handleRecaptchaFailure () { + this.analyticsFactory.checkoutFieldError('submitOrder', 'failed') + this.submittingOrder = false + this.onSubmittingOrder({ value: false }) - componentInstance.loadCart() + this.loadCart() - componentInstance.onSubmitted() - componentInstance.submissionError = 'generic error' - componentInstance.$window.scrollTo(0, 0) + this.onSubmitted() + this.submissionError = 'generic error' + this.$window.scrollTo(0, 0) } } diff --git a/src/app/checkout/step-3/step-3.tpl.html b/src/app/checkout/step-3/step-3.tpl.html index 004827433..fd65b98fe 100644 --- a/src/app/checkout/step-3/step-3.tpl.html +++ b/src/app/checkout/step-3/step-3.tpl.html @@ -206,7 +206,7 @@ action="$ctrl.isBranded ? 'branded_submit' : 'submit_gift'" on-success="$ctrl.submitOrder" on-failure="$ctrl.handleRecaptchaFailure" - component-instance="$ctrl.selfReference" + component-instance="$ctrl" button-id="'submitOrderButton'" button-type="'submit'" button-classes="'btn btn-primary btn-lg btn-block'" diff --git a/src/common/components/Recaptcha/Recaptcha.test.tsx b/src/common/components/Recaptcha/Recaptcha.test.tsx index cb7e1994a..51396115f 100644 --- a/src/common/components/Recaptcha/Recaptcha.test.tsx +++ b/src/common/components/Recaptcha/Recaptcha.test.tsx @@ -247,7 +247,6 @@ describe('Recaptcha component', () => { action='submit_gift' onSuccess={onSuccess} onFailure={onFailure} - componentInstance={{}} buttonId='id' buttonType={ButtonType.Submit} buttonClasses='btn' diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx index 49f7163e3..4a739ff86 100644 --- a/src/common/components/Recaptcha/Recaptcha.tsx +++ b/src/common/components/Recaptcha/Recaptcha.tsx @@ -22,9 +22,8 @@ const isValidAction = (action: string): boolean => { interface RecaptchaProps { action: string - onSuccess: (componentInstance: any) => void - onFailure: (componentInstance: any) => void - componentInstance: any + onSuccess: () => void + onFailure: () => void buttonId: string buttonType?: ButtonType buttonClasses: string @@ -40,7 +39,6 @@ export const Recaptcha = ({ action, onSuccess, onFailure, - componentInstance, buttonId, buttonType, buttonClasses, @@ -89,29 +87,29 @@ export const Recaptcha = ({ if (data?.success === true && isValidAction(data?.action)) { if (data.score < 0.5) { $log.warn(`Captcha score was below the threshold: ${data.score}`) - onFailure(componentInstance) + onFailure() return } - onSuccess(componentInstance) + onSuccess() return } if (data?.success === false && isValidAction(data?.action)) { $log.warn('Recaptcha call was unsuccessful, continuing anyway') - onSuccess(componentInstance) + onSuccess() return } if (!data) { $log.warn('Data was missing!') - onSuccess(componentInstance) + onSuccess() return } if (!isValidAction(data?.action)) { $log.warn(`Invalid action: ${data?.action}`) - onFailure(componentInstance) + onFailure() } } catch (error) { $log.error(`Failed to verify recaptcha, continuing on: ${error}`) - onSuccess(componentInstance) + onSuccess() } }) }, [grecaptcha, buttonId, ready]) @@ -135,7 +133,6 @@ export default angular 'action', 'onSuccess', 'onFailure', - 'componentInstance', 'buttonId', 'buttonType', 'buttonClasses', diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx index 6e962f040..f71570bd8 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx @@ -16,6 +16,10 @@ describe('RecaptchaWrapper component', () => { warn: jest.fn() } + const $rootScope = { + $apply: jest.fn() + } + const mockExecuteRecaptcha = jest.fn() const mockRecaptchaReady = jest.fn() const mockRecaptcha = { @@ -48,6 +52,7 @@ describe('RecaptchaWrapper component', () => { envService={envService} $translate={$translate} $log={$log} + $rootScope={$rootScope} /> ) expect(getAllByRole('button')).toHaveLength(1) @@ -75,6 +80,7 @@ describe('RecaptchaWrapper component', () => { envService={envService} $translate={$translate} $log={$log} + $rootScope={$rootScope} /> ) expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.tsx index 05a863846..95d4c43dd 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.tsx @@ -13,8 +13,8 @@ declare global { interface RecaptchaWrapperProps { action: string - onSuccess: (componentInstance: any) => void - onFailure: (componentInstance: any) => void + onSuccess: () => void + onFailure: () => void componentInstance: any buttonId: string buttonType?: ButtonType @@ -24,6 +24,7 @@ interface RecaptchaWrapperProps { envService: any $translate: any $log: any + $rootScope: any } export const RecaptchaWrapper = ({ @@ -38,7 +39,8 @@ export const RecaptchaWrapper = ({ buttonLabel, envService, $translate, - $log + $log, + $rootScope }: RecaptchaWrapperProps): JSX.Element => { const recaptchaKey = envService.read('recaptchaKey') const apiUrl = envService.read('apiUrl') @@ -50,11 +52,23 @@ export const RecaptchaWrapper = ({ document.body.appendChild(script) }, []) + // Because The onSuccess and onFailure callbacks are called by a React component, AngularJS doesn't know that an event happened and doesn't know it needs to rerender. We have to use $apply to ensure that AngularJS rerenders after the event handlers return. + const onSuccessWrapped = (() => { + $rootScope.$apply(() => { + onSuccess.call(componentInstance) + }) + }) + + const onFailureWrapped = (() => { + $rootScope.$apply(() => { + onFailure.call(componentInstance) + }) + }) + return ( Date: Wed, 18 Dec 2024 18:05:43 -0800 Subject: [PATCH 11/14] Move variables to cartEvents.js to avoid circular dependency --- src/app/cart/cart.component.js | 2 +- src/app/cart/cart.component.spec.js | 2 +- .../productConfigForm/productConfigForm.component.js | 2 +- .../productConfigForm/productConfigForm.component.spec.js | 2 +- .../giveOneTimeGift/giveOneTimeGift.modal.component.js | 2 +- .../giveOneTimeGift/giveOneTimeGift.modal.component.spec.js | 2 +- src/common/components/nav/navCart/navCart.component.js | 4 +--- src/common/components/nav/navCart/navCart.component.spec.js | 3 ++- src/common/components/nav/navCartIcon.component.js | 3 ++- src/common/components/nav/navCartIcon.component.spec.js | 2 +- src/common/lib/cartEvents.js | 2 ++ 11 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 src/common/lib/cartEvents.js diff --git a/src/app/cart/cart.component.js b/src/app/cart/cart.component.js index 9eef901ba..9ccbb83e7 100644 --- a/src/app/cart/cart.component.js +++ b/src/app/cart/cart.component.js @@ -10,7 +10,7 @@ import productModalService from 'common/services/productModal.service' import desigSrcDirective from 'common/directives/desigSrc.directive' import displayRateTotals from 'common/components/displayRateTotals/displayRateTotals.component' -import { cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component' +import { cartUpdatedEvent } from 'common/lib/cartEvents' import analyticsFactory from 'app/analytics/analytics.factory' diff --git a/src/app/cart/cart.component.spec.js b/src/app/cart/cart.component.spec.js index ca14206fe..0f1a0fa29 100644 --- a/src/app/cart/cart.component.spec.js +++ b/src/app/cart/cart.component.spec.js @@ -6,7 +6,7 @@ import { Observable } from 'rxjs/Observable' import 'rxjs/add/observable/of' import 'rxjs/add/observable/throw' -import { cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component' +import { cartUpdatedEvent } from 'common/lib/cartEvents' describe('cart', () => { beforeEach(angular.mock.module(module.name)) diff --git a/src/app/productConfig/productConfigForm/productConfigForm.component.js b/src/app/productConfig/productConfigForm/productConfigForm.component.js index e6193fe8c..43ce5c9f5 100644 --- a/src/app/productConfig/productConfigForm/productConfigForm.component.js +++ b/src/app/productConfig/productConfigForm/productConfigForm.component.js @@ -27,7 +27,7 @@ import { } from 'common/services/giftHelpers/giftDates.service' import desigSrcDirective from 'common/directives/desigSrc.directive' import showErrors from 'common/filters/showErrors.filter' -import { giftAddedEvent, cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component' +import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents' import { giveGiftParams } from '../giveGiftParams' import loading from 'common/components/loading/loading.component' import analyticsFactory from 'app/analytics/analytics.factory' diff --git a/src/app/productConfig/productConfigForm/productConfigForm.component.spec.js b/src/app/productConfig/productConfigForm/productConfigForm.component.spec.js index de678fb24..70f639879 100644 --- a/src/app/productConfig/productConfigForm/productConfigForm.component.spec.js +++ b/src/app/productConfig/productConfigForm/productConfigForm.component.spec.js @@ -7,7 +7,7 @@ import 'rxjs/add/observable/of' import 'rxjs/add/observable/throw' import module, { brandedCoverFeeCheckedEvent } from './productConfigForm.component' -import { giftAddedEvent, cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component' +import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents' import { giveGiftParams } from '../giveGiftParams' import { brandedCheckoutAmountUpdatedEvent } from '../../../common/components/paymentMethods/coverFees/coverFees.component' diff --git a/src/app/profile/yourGiving/giveOneTimeGift/giveOneTimeGift.modal.component.js b/src/app/profile/yourGiving/giveOneTimeGift/giveOneTimeGift.modal.component.js index 8d6e75948..22204aac0 100644 --- a/src/app/profile/yourGiving/giveOneTimeGift/giveOneTimeGift.modal.component.js +++ b/src/app/profile/yourGiving/giveOneTimeGift/giveOneTimeGift.modal.component.js @@ -5,7 +5,7 @@ import concat from 'lodash/concat' import step1SelectRecentRecipients from './step1/selectRecentRecipients.component' import step1SearchRecipients from './step1/searchRecipients.component' import step2EnterAmounts from './step2/enterAmounts.component' -import { giftAddedEvent } from 'common/components/nav/navCart/navCart.component' +import { giftAddedEvent } from 'common/lib/cartEvents' import RecurringGiftModel from 'common/models/recurringGift.model' diff --git a/src/app/profile/yourGiving/giveOneTimeGift/giveOneTimeGift.modal.component.spec.js b/src/app/profile/yourGiving/giveOneTimeGift/giveOneTimeGift.modal.component.spec.js index 70251998b..b30c57ea7 100644 --- a/src/app/profile/yourGiving/giveOneTimeGift/giveOneTimeGift.modal.component.spec.js +++ b/src/app/profile/yourGiving/giveOneTimeGift/giveOneTimeGift.modal.component.spec.js @@ -6,7 +6,7 @@ import 'rxjs/add/observable/from' import 'rxjs/add/observable/throw' import RecurringGiftModel from 'common/models/recurringGift.model' -import { giftAddedEvent } from 'common/components/nav/navCart/navCart.component' +import { giftAddedEvent } from 'common/lib/cartEvents' import module from './giveOneTimeGift.modal.component' diff --git a/src/common/components/nav/navCart/navCart.component.js b/src/common/components/nav/navCart/navCart.component.js index 42f30aedf..c5a5f2f57 100644 --- a/src/common/components/nav/navCart/navCart.component.js +++ b/src/common/components/nav/navCart/navCart.component.js @@ -7,9 +7,7 @@ import orderService from 'common/services/api/order.service' import analyticsFactory from 'app/analytics/analytics.factory' import template from './navCart.tpl.html' - -export const giftAddedEvent = 'giftAddedToCart' -export const cartUpdatedEvent = 'cartUpdatedEvent' +import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents' const componentName = 'navCart' diff --git a/src/common/components/nav/navCart/navCart.component.spec.js b/src/common/components/nav/navCart/navCart.component.spec.js index 3ea72ba33..8423fcf85 100644 --- a/src/common/components/nav/navCart/navCart.component.spec.js +++ b/src/common/components/nav/navCart/navCart.component.spec.js @@ -4,7 +4,8 @@ import { Observable } from 'rxjs/Observable' import { Subject } from 'rxjs/Subject' import 'rxjs/add/observable/of' import 'rxjs/add/observable/throw' -import module, { giftAddedEvent, cartUpdatedEvent } from './navCart.component' +import module from './navCart.component' +import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents' describe('navCart', () => { beforeEach(angular.mock.module(module.name)) diff --git a/src/common/components/nav/navCartIcon.component.js b/src/common/components/nav/navCartIcon.component.js index 12bdd894c..32ca30663 100644 --- a/src/common/components/nav/navCartIcon.component.js +++ b/src/common/components/nav/navCartIcon.component.js @@ -1,6 +1,7 @@ import angular from 'angular' -import navCart, { giftAddedEvent, cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component' +import navCart from 'common/components/nav/navCart/navCart.component' +import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents' import uibDropdown from 'angular-ui-bootstrap/src/dropdown' import analyticsFactory from 'app/analytics/analytics.factory' diff --git a/src/common/components/nav/navCartIcon.component.spec.js b/src/common/components/nav/navCartIcon.component.spec.js index 517c6c5be..c2397ca4c 100644 --- a/src/common/components/nav/navCartIcon.component.spec.js +++ b/src/common/components/nav/navCartIcon.component.spec.js @@ -2,7 +2,7 @@ import angular from 'angular' import 'angular-mocks' import module from './navCartIcon.component' -import { giftAddedEvent, cartUpdatedEvent } from 'common/components/nav/navCart/navCart.component' +import { giftAddedEvent, cartUpdatedEvent } from 'common/lib/cartEvents' describe('nav cart icon', function () { beforeEach(angular.mock.module(module.name)) diff --git a/src/common/lib/cartEvents.js b/src/common/lib/cartEvents.js new file mode 100644 index 000000000..d4b9d0c42 --- /dev/null +++ b/src/common/lib/cartEvents.js @@ -0,0 +1,2 @@ +export const giftAddedEvent = 'giftAddedToCart' +export const cartUpdatedEvent = 'cartUpdatedEvent' From 1b24d2669e20f6a2bb9ea2174af988b14b0d0044 Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Wed, 18 Dec 2024 18:06:36 -0800 Subject: [PATCH 12/14] Move checkout error messages to a new component --- .../branded-checkout-step-1.component.js | 2 + .../step-1/branded-checkout-step-1.tpl.html | 5 ++ .../checkout-error-messages.component.js | 19 +++++++ .../checkout-error-messages.tpl.html | 44 +++++++++++++++++ src/app/checkout/step-3/step-3.component.js | 4 +- src/app/checkout/step-3/step-3.tpl.html | 49 ++----------------- 6 files changed, 78 insertions(+), 45 deletions(-) create mode 100644 src/app/checkout/checkout-error-messages/checkout-error-messages.component.js create mode 100644 src/app/checkout/checkout-error-messages/checkout-error-messages.tpl.html diff --git a/src/app/branded/step-1/branded-checkout-step-1.component.js b/src/app/branded/step-1/branded-checkout-step-1.component.js index a6d16d249..2c5de9e44 100644 --- a/src/app/branded/step-1/branded-checkout-step-1.component.js +++ b/src/app/branded/step-1/branded-checkout-step-1.component.js @@ -4,6 +4,7 @@ import every from 'lodash/every' import productConfigForm from 'app/productConfig/productConfigForm/productConfigForm.component' import contactInfo from 'common/components/contactInfo/contactInfo.component' import checkoutStep2 from 'app/checkout/step-2/step-2.component' +import checkoutErrorMessages from 'app/checkout/checkout-error-messages/checkout-error-messages.component' import cartService from 'common/services/api/cart.service' import orderService from 'common/services/api/order.service' @@ -273,6 +274,7 @@ export default angular productConfigForm.name, contactInfo.name, checkoutStep2.name, + checkoutErrorMessages.name, cartService.name, orderService.name, analyticsFactory.name, diff --git a/src/app/branded/step-1/branded-checkout-step-1.tpl.html b/src/app/branded/step-1/branded-checkout-step-1.tpl.html index 8fd9bb70b..76de3fa51 100644 --- a/src/app/branded/step-1/branded-checkout-step-1.tpl.html +++ b/src/app/branded/step-1/branded-checkout-step-1.tpl.html @@ -1,3 +1,8 @@ + +
diff --git a/src/app/checkout/checkout-error-messages/checkout-error-messages.component.js b/src/app/checkout/checkout-error-messages/checkout-error-messages.component.js new file mode 100644 index 000000000..d6a5a7f60 --- /dev/null +++ b/src/app/checkout/checkout-error-messages/checkout-error-messages.component.js @@ -0,0 +1,19 @@ +import angular from 'angular' + +import template from './checkout-error-messages.tpl.html' + +const componentName = 'checkoutErrorMessages' + +class CheckoutErrorMessagesController {} + +export default angular + .module(componentName, []) + .component(componentName, { + controller: CheckoutErrorMessagesController, + templateUrl: template, + bindings: { + needinfoErrors: '<', + submissionError: '<', + submissionErrorStatus: '<' + } + }) diff --git a/src/app/checkout/checkout-error-messages/checkout-error-messages.tpl.html b/src/app/checkout/checkout-error-messages/checkout-error-messages.tpl.html new file mode 100644 index 000000000..187a80e06 --- /dev/null +++ b/src/app/checkout/checkout-error-messages/checkout-error-messages.tpl.html @@ -0,0 +1,44 @@ + diff --git a/src/app/checkout/step-3/step-3.component.js b/src/app/checkout/step-3/step-3.component.js index 6745c4153..f2d871741 100644 --- a/src/app/checkout/step-3/step-3.component.js +++ b/src/app/checkout/step-3/step-3.component.js @@ -2,6 +2,7 @@ import angular from 'angular' import 'rxjs/add/observable/throw' import displayAddressComponent from 'common/components/display-address/display-address.component' +import checkoutErrorMessages from 'app/checkout/checkout-error-messages/checkout-error-messages.component' import displayRateTotals from 'common/components/displayRateTotals/displayRateTotals.component' import commonService from 'common/services/api/common.service' @@ -163,7 +164,8 @@ export default angular analyticsFactory.name, cartService.name, commonService.name, - recaptchaComponent.name + recaptchaComponent.name, + checkoutErrorMessages.name ]) .component(componentName, { controller: Step3Controller, diff --git a/src/app/checkout/step-3/step-3.tpl.html b/src/app/checkout/step-3/step-3.tpl.html index fd65b98fe..67fa357a2 100644 --- a/src/app/checkout/step-3/step-3.tpl.html +++ b/src/app/checkout/step-3/step-3.tpl.html @@ -1,48 +1,9 @@
- + +
From d9647031b913f5cc0f9a6de63f6c86b00d835065 Mon Sep 17 00:00:00 2001 From: Will James Date: Tue, 7 Jan 2025 14:44:23 -0500 Subject: [PATCH 13/14] EP-2560 (#1129) *Moves FN, LN, *ORG Name, *Spouse name, Email, and Phone under Contact Information --- .../step-1/branded-checkout-step-1.tpl.html | 2 +- .../contactInfo/contactInfo.tpl.html | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/app/branded/step-1/branded-checkout-step-1.tpl.html b/src/app/branded/step-1/branded-checkout-step-1.tpl.html index a6340b195..192ff348e 100644 --- a/src/app/branded/step-1/branded-checkout-step-1.tpl.html +++ b/src/app/branded/step-1/branded-checkout-step-1.tpl.html @@ -28,7 +28,7 @@
-

{{'YOUR_INFORMATION'}}

+

{{'YOUR_INFORMATION'}}

{{'YOUR_INFORMATION'}}

+

{{'CONTACT_INFO'}}

@@ -118,8 +119,23 @@

+
+
+ +
+
{{'EMAIL_MISSING_ERROR'}}
+
{{'EMAIL_INVALID_ERROR'}}
+
{{'EMAIL_LENGTH_ERROR'}}
+
+
+
+

@@ -222,7 +238,7 @@

{{'MAILING_ADDRE

-
+

{{'CONTACT_INFO'}}

@@ -238,7 +254,7 @@

{{'CONTACT_INFO'

-
+
@@ -225,6 +209,16 @@

+
+ + +
+

+
@@ -242,17 +236,10 @@

{{'MAILING_ADDRE

{{'CONTACT_INFO'}}

-
- -
-
{{'EMAIL_MISSING_ERROR'}}
-
{{'EMAIL_INVALID_ERROR'}}
-
{{'EMAIL_LENGTH_ERROR'}}
-
-
+ +
diff --git a/src/common/components/contactInfo/emailField/emailField.component.js b/src/common/components/contactInfo/emailField/emailField.component.js new file mode 100644 index 000000000..2a9c186a3 --- /dev/null +++ b/src/common/components/contactInfo/emailField/emailField.component.js @@ -0,0 +1,17 @@ +import angular from 'angular' +import template from './emailField.tpl.html' + +const componentName = 'emailField' + +class EmailFieldController {} + +export default angular + .module(componentName, []) + .component(componentName, { + controller: EmailFieldController, + templateUrl: template, + bindings: { + donorDetails: '<', + detailsForm: '<', + } + }) diff --git a/src/common/components/contactInfo/emailField/emailField.tpl.html b/src/common/components/contactInfo/emailField/emailField.tpl.html new file mode 100644 index 000000000..0c21433ce --- /dev/null +++ b/src/common/components/contactInfo/emailField/emailField.tpl.html @@ -0,0 +1,11 @@ +
+ +
+
{{'EMAIL_MISSING_ERROR'}}
+
{{'EMAIL_INVALID_ERROR'}}
+
{{'EMAIL_LENGTH_ERROR'}}
+
+