diff --git a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.js b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.js index ce59dea7a..04b7d188d 100644 --- a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.js +++ b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.js @@ -6,19 +6,20 @@ import paymentMethodDisplay from 'common/components/paymentMethods/paymentMethod import paymentMethodFormModal from 'common/components/paymentMethods/paymentMethodForm/paymentMethodForm.modal.component' import coverFees from 'common/components/paymentMethods/coverFees/coverFees.component' +import * as cruPayments from '@cruglobal/cru-payments/dist/cru-payments' import orderService from 'common/services/api/order.service' import cartService from 'common/services/api/cart.service' import { validPaymentMethod } from 'common/services/paymentHelpers/validPaymentMethods' import giveModalWindowTemplate from 'common/templates/giveModalWindow.tpl.html' import { SignInEvent } from 'common/services/session/session.service' - +import creditCardCvv from '../../../../common/directives/creditCardCvv.directive' import template from './existingPaymentMethods.tpl.html' const componentName = 'checkoutExistingPaymentMethods' class ExistingPaymentMethodsController { /* @ngInject */ - constructor ($log, $scope, orderService, cartService, $uibModal) { + constructor ($log, $scope, orderService, cartService, $uibModal, $window) { this.$log = $log this.$scope = $scope this.orderService = orderService @@ -26,6 +27,7 @@ class ExistingPaymentMethodsController { this.$uibModal = $uibModal this.paymentFormResolve = {} this.validPaymentMethod = validPaymentMethod + this.sessionStorage = $window.sessionStorage this.$scope.$on(SignInEvent, () => { this.$onInit() @@ -33,7 +35,9 @@ class ExistingPaymentMethodsController { } $onInit () { + this.enableContinue({ $event: false }) this.loadPaymentMethods() + this.waitForFormInitialization() } $onChanges (changes) { @@ -52,6 +56,27 @@ class ExistingPaymentMethodsController { } } + waitForFormInitialization () { + const unregister = this.$scope.$watch('$ctrl.creditCardPaymentForm.securityCode', () => { + if (this.creditCardPaymentForm && this.creditCardPaymentForm.securityCode) { + unregister() + this.addCvvValidators() + this.switchPayment() + } + }) + } + + addCvvValidators () { + this.$scope.$watch('$ctrl.creditCardPaymentForm.securityCode.$viewValue', (number) => { + if (this.selectedPaymentMethod?.['card-type'] && this.creditCardPaymentForm.securityCode) { + this.creditCardPaymentForm.securityCode.$validators.minLength = cruPayments.creditCard.cvv.validate.minLength + this.creditCardPaymentForm.securityCode.$validators.maxLength = cruPayments.creditCard.cvv.validate.maxLength + this.enableContinue({ $event: cruPayments.creditCard.cvv.validate.minLength(number) && cruPayments.creditCard.cvv.validate.maxLength(number) }) + this.selectedPaymentMethod.cvv = number + } + }) + } + loadPaymentMethods () { this.orderService.getExistingPaymentMethods() .subscribe((data) => { @@ -80,6 +105,7 @@ class ExistingPaymentMethodsController { // Select the first payment method this.selectedPaymentMethod = paymentMethods[0] } + this.shouldRecoverCvv = true this.switchPayment() } @@ -130,6 +156,13 @@ class ExistingPaymentMethodsController { switchPayment () { this.onPaymentChange({ selectedPaymentMethod: this.selectedPaymentMethod }) + if (this.selectedPaymentMethod?.['card-type'] && this.creditCardPaymentForm?.securityCode) { + // Set cvv from session storage + const storage = this.shouldRecoverCvv ? JSON.parse(this.sessionStorage.getItem('cvv')) : '' + this.creditCardPaymentForm.securityCode.$setViewValue(storage) + this.creditCardPaymentForm.securityCode.$render() + this.shouldRecoverCvv = false + } if (this.selectedPaymentMethod?.['bank-name']) { // This is an EFT payment method so we need to remove any fee coverage this.orderService.storeCoverFeeDecision(false) @@ -144,7 +177,8 @@ export default angular paymentMethodFormModal.name, coverFees.name, orderService.name, - cartService.name + cartService.name, + creditCardCvv.name ]) .component(componentName, { controller: ExistingPaymentMethodsController, @@ -159,6 +193,7 @@ export default angular brandedCheckoutItem: '<', onPaymentFormStateChange: '&', onPaymentChange: '&', - onLoad: '&' + onLoad: '&', + enableContinue: '&' } }) diff --git a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.spec.js b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.spec.js index d2194b8ca..372ea7726 100644 --- a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.spec.js +++ b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.component.spec.js @@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable' import 'rxjs/add/observable/of' import 'rxjs/add/observable/throw' import 'rxjs/add/operator/toPromise' +import * as cruPayments from '@cruglobal/cru-payments/dist/cru-payments' import { SignInEvent } from 'common/services/session/session.service' @@ -15,24 +16,43 @@ describe('checkout', () => { beforeEach(angular.mock.module(module.name)) const self = {} - beforeEach(inject(($componentController, $timeout) => { + beforeEach(inject(($componentController, $timeout, $window) => { self.$timeout = $timeout self.controller = $componentController(module.name, {}, { onLoad: jest.fn(), onPaymentChange: jest.fn(), + enableContinue: jest.fn(), onPaymentFormStateChange: jest.fn(), - cartData: { items: [] } + cartData: { items: [] }, + creditCardPaymentForm: { + securityCode: { + $valid: true, + $validators: { + minLength: (value) => cruPayments.creditCard.cvv.validate.minLength(value), + maxLength: cruPayments.creditCard.cvv.validate.maxLength + }, + $setViewValue: jest.fn(), + $render: jest.fn(), + } + }, + selectedPaymentMethod: { + cvv: '', + 'card-type': 'Visa' + } }) + self.$window = $window + self.$window.sessionStorage.clear() })) - describe('$onInit', () => { it('should call loadPaymentMethods', () => { jest.spyOn(self.controller, 'loadPaymentMethods').mockImplementation(() => {}) + jest.spyOn(self.controller, 'waitForFormInitialization').mockImplementation(() => {}) self.controller.$onInit() expect(self.controller.loadPaymentMethods).toHaveBeenCalled() + expect(self.controller.waitForFormInitialization).toHaveBeenCalled() }) it('should be called on sign in', () => { @@ -329,6 +349,80 @@ describe('checkout', () => { expect(self.controller.onPaymentChange).toHaveBeenCalledWith({ selectedPaymentMethod: undefined }) expect(self.controller.orderService.storeCoverFeeDecision).not.toHaveBeenCalled() }) + + it('should reset securityCode viewValue', () => { + self.controller.switchPayment() + + expect(self.controller.creditCardPaymentForm.securityCode.$setViewValue).toHaveBeenCalledWith('') + expect(self.controller.creditCardPaymentForm.securityCode.$render).toHaveBeenCalled() + }) + + it('should add securityCode viewValue from sessionStorage', () => { + self.$window.sessionStorage.setItem( + 'cvv', + '456' + ) + self.controller.shouldRecoverCvv = true + self.controller.switchPayment() + + expect(self.controller.creditCardPaymentForm.securityCode.$setViewValue).toHaveBeenCalledWith(456) + expect(self.controller.creditCardPaymentForm.securityCode.$render).toHaveBeenCalled() + }) + }) + + describe('addCvvValidators', () => { + it('should add a watch on the security code value', () => { + self.controller.creditCardPaymentForm = { + $valid: true, + $dirty: false, + securityCode: { + $viewValue: '123', + $validators: {} + } + } + self.controller.addCvvValidators() + expect(self.controller.$scope.$$watchers.length).toEqual(1) + expect(self.controller.$scope.$$watchers[0].exp).toEqual('$ctrl.creditCardPaymentForm.securityCode.$viewValue') + }) + + it('should add validator functions to creditCardPaymentForm.securityCode', () => { + jest.spyOn(self.controller, 'addCvvValidators') + self.controller.selectedPaymentMethod.self = { + type: 'cru.creditcards.named-credit-card', + uri: 'selected uri' + } + self.controller.waitForFormInitialization() + self.controller.$scope.$digest() + + expect(self.controller.addCvvValidators).toHaveBeenCalled() + expect(Object.keys(self.controller.creditCardPaymentForm.securityCode.$validators).length).toEqual(2) + expect(typeof self.controller.creditCardPaymentForm.securityCode.$validators.minLength).toBe('function') + expect(typeof self.controller.creditCardPaymentForm.securityCode.$validators.maxLength).toBe('function') + }) + + it('should call enableContinue when cvv is valid', () => { + self.controller.creditCardPaymentForm.securityCode.$viewValue = '123' + self.controller.addCvvValidators() + self.controller.$scope.$apply() + + expect(self.controller.enableContinue).toHaveBeenCalledWith({ $event: true }) + }) + + it('should call enableContinue when cvv is too long', () => { + self.controller.creditCardPaymentForm.securityCode.$viewValue = '12345' + self.controller.addCvvValidators() + self.controller.$scope.$apply() + + expect(self.controller.enableContinue).toHaveBeenCalledWith({ $event: false }) + }) + + it('should call enableContinue when cvv is too short', () => { + self.controller.creditCardPaymentForm.securityCode.$viewValue = '1' + self.controller.addCvvValidators() + self.controller.$scope.$apply() + + expect(self.controller.enableContinue).toHaveBeenCalledWith({ $event: false }) + }) }) }) }) diff --git a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html index 54080a339..287cc758b 100644 --- a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html +++ b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html @@ -1,17 +1,24 @@ -