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..e642b6269 100644
--- a/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html
+++ b/src/app/checkout/step-2/existingPaymentMethods/existingPaymentMethods.tpl.html
@@ -1,16 +1,24 @@
-
+
Your Payment Methods
+
-
+
diff --git a/src/app/checkout/step-2/step-2.component.js b/src/app/checkout/step-2/step-2.component.js
index d1f384c3a..2b6c54b4e 100644
--- a/src/app/checkout/step-2/step-2.component.js
+++ b/src/app/checkout/step-2/step-2.component.js
@@ -78,6 +78,7 @@ class Step2Controller {
onPaymentFormStateChange ($event) {
this.paymentFormState = $event.state
+
if ($event.state === 'loading' && $event.payload) {
const paymentType = $event.payload.creditCard ? $event.payload.creditCard['card-type'] : $event.payload.bankAccount ? $event.payload.bankAccount['account-type'] : 'Unknown'
const request = $event.update
@@ -109,6 +110,8 @@ class Step2Controller {
this.changeStep({ newStep: 'review' })
this.onStateChange({ state: 'submitted' })
this.paymentFormState = 'success'
+ } else if ($event.state === 'submitted') {
+ this.orderService.storeCardSecurityCode(this.selectedPaymentMethod.cvv, this.selectedPaymentMethod.self.uri)
} else if ($event.state === 'unsubmitted') {
this.onStateChange({ state: 'unsubmitted' })
} else if ($event.state === 'error') {
@@ -116,7 +119,10 @@ class Step2Controller {
}
}
- getContinueDisabled () {
+ isContinueDisabled () {
+ if (this.selectedPaymentMethod?.['card-type'] && !this.isCvvValid) {
+ return true
+ }
if (this.loadingPaymentMethods) {
return true
}
@@ -129,6 +135,10 @@ class Step2Controller {
}
return false
}
+
+ enableContinue (isCvvValid) {
+ this.isCvvValid = isCvvValid
+ }
}
export default angular
diff --git a/src/app/checkout/step-2/step-2.component.spec.js b/src/app/checkout/step-2/step-2.component.spec.js
index f3e61ae79..32dfa424c 100644
--- a/src/app/checkout/step-2/step-2.component.spec.js
+++ b/src/app/checkout/step-2/step-2.component.spec.js
@@ -164,6 +164,7 @@ describe('checkout', () => {
it('should update paymentFormState if transitioning to a different state', () => {
self.controller.paymentFormState = 'unsubmitted'
+ self.controller.selectedPaymentMethod = { cvv: '123', self: { uri: 'uri'} }
self.controller.onPaymentFormStateChange({ state: 'submitted' })
expect(self.controller.paymentFormState).toEqual('submitted')
@@ -249,14 +250,14 @@ describe('checkout', () => {
})
})
- describe('getContinueDisabled', () => {
+ describe('isContinueDisabled', () => {
it('should return true when there are existing payment methods but none are valid', () => {
self.controller.handleExistingPaymentLoading(true, true)
self.controller.handlePaymentChange(undefined)
expect(self.controller.existingPaymentMethods).toBe(true)
expect(self.controller.selectedPaymentMethod).toBeUndefined()
- expect(self.controller.getContinueDisabled()).toBe(true)
+ expect(self.controller.isContinueDisabled()).toBe(true)
})
it('should return false when there are existing payment methods and at least one is valid', () => {
@@ -265,7 +266,7 @@ describe('checkout', () => {
expect(self.controller.existingPaymentMethods).toBe(true)
expect(self.controller.selectedPaymentMethod).not.toBeUndefined()
- expect(self.controller.getContinueDisabled()).toBe(false)
+ expect(self.controller.isContinueDisabled()).toBe(false)
})
it('should return false when there are not existing payment methods', () => {
@@ -273,19 +274,19 @@ describe('checkout', () => {
expect(self.controller.existingPaymentMethods).toBe(false)
expect(self.controller.selectedPaymentMethod).toBeUndefined()
- expect(self.controller.getContinueDisabled()).toBe(false)
+ expect(self.controller.isContinueDisabled()).toBe(false)
})
it('should return true while the payment methods are loading', () => {
self.controller.$onInit()
expect(self.controller.loadingPaymentMethods).toBe(true)
- expect(self.controller.getContinueDisabled()).toBe(true)
+ expect(self.controller.isContinueDisabled()).toBe(true)
self.controller.handleExistingPaymentLoading(true, false)
expect(self.controller.loadingPaymentMethods).toBe(false)
- expect(self.controller.getContinueDisabled()).toBe(false)
+ expect(self.controller.isContinueDisabled()).toBe(false)
})
it('should return true while the payment form is encrypting or loading', () => {
@@ -295,18 +296,90 @@ describe('checkout', () => {
self.controller.onPaymentFormStateChange({ state: 'encrypting' })
expect(self.controller.paymentFormState).toBe('encrypting')
- expect(self.controller.getContinueDisabled()).toBe(true)
+ expect(self.controller.isContinueDisabled()).toBe(true)
self.controller.onPaymentFormStateChange({ state: 'loading', payload: {}, update: false })
expect(self.controller.paymentFormState).toBe('loading')
- expect(self.controller.getContinueDisabled()).toBe(true)
+ expect(self.controller.isContinueDisabled()).toBe(true)
deferred.resolve()
self.$flushPendingTasks()
expect(self.controller.paymentFormState).toBe('success')
- expect(self.controller.getContinueDisabled()).toBe(false)
+ expect(self.controller.isContinueDisabled()).toBe(false)
+ })
+
+ describe('existing credit card used', () => {
+ it('should disable continue when cvv is invalid', () => {
+ self.controller.handleExistingPaymentLoading(true, true)
+ self.controller.isCvvValid = false
+ self.controller.handlePaymentChange({'card-type': 'visa'})
+
+ expect(self.controller.isContinueDisabled()).toBe(true)
+ })
+
+ it('should disable continue when cvv is valid', () => {
+ self.controller.handleExistingPaymentLoading(true, true)
+ self.controller.isCvvValid = true
+ self.controller.handlePaymentChange({'card-type': 'visa'})
+
+ expect(self.controller.isContinueDisabled()).toBe(false)
+ })
+
+ it('should not disable continue when cvv is invalid', () => {
+ self.controller.handleExistingPaymentLoading(true, true)
+ self.controller.isCvvValid = false
+ self.controller.handlePaymentChange({'account-type': 'checking'})
+
+ expect(self.controller.isContinueDisabled()).toBe(false)
+ })
+ })
+
+ describe('existing EFT used', () => {
+ it('should not disable continue when cvv validity is undefined', () => {
+ self.controller.handleExistingPaymentLoading(true, true)
+ self.controller.isCvvValid = undefined
+ self.controller.handlePaymentChange({'account-type': 'checking'})
+
+ expect(self.controller.isContinueDisabled()).toBe(false)
+ })
+
+ it('should not disable continue when cvv is valid', () => {
+ self.controller.handleExistingPaymentLoading(true, true)
+ self.controller.isCvvValid = true
+ self.controller.handlePaymentChange({'account-type': 'checking'})
+
+ expect(self.controller.isContinueDisabled()).toBe(false)
+ })
+ })
+
+ describe('new credit card used', () => {
+ it('should disable continue when cvv is invalid and new credit card payment is added', () => {
+ self.controller.handlePaymentChange({'card-type': 'visa'})
+ self.controller.isCvvValid = false
+ expect(self.controller.isContinueDisabled()).toBe(true)
+ })
+ })
+
+ describe('new EFT used', () => {
+ it('should not disable continue when cvv is invalid and new EFT is added', () => {
+ self.controller.handlePaymentChange({'account-type': 'checking'})
+ self.controller.isCvvValid = false
+ expect(self.controller.isContinueDisabled()).toBe(false)
+ })
+ })
+ })
+
+ describe('enableContinue', () => {
+ it('should set isCvvValid to false', () => {
+ self.controller.enableContinue(false)
+ expect(self.controller.isCvvValid).toBe(false)
+ })
+
+ it('should set isCvvValid to true', () => {
+ self.controller.enableContinue(true)
+ expect(self.controller.isCvvValid).toBe(true)
})
})
})
diff --git a/src/app/checkout/step-2/step-2.tpl.html b/src/app/checkout/step-2/step-2.tpl.html
index 7a3edb704..d2203b9cd 100644
--- a/src/app/checkout/step-2/step-2.tpl.html
+++ b/src/app/checkout/step-2/step-2.tpl.html
@@ -31,7 +31,8 @@
default-payment-type="$ctrl.defaultPaymentType"
hide-payment-type-options="$ctrl.hidePaymentTypeOptions"
cart-data="$ctrl.cartData"
- branded-checkout-item="$ctrl.brandedCheckoutItem">
+ branded-checkout-item="$ctrl.brandedCheckoutItem"
+ enable-continue="$ctrl.enableContinue($event)">
@@ -42,7 +43,7 @@
-
diff --git a/src/common/directives/creditCardCvv.directive.js b/src/common/directives/creditCardCvv.directive.js
new file mode 100644
index 000000000..841118061
--- /dev/null
+++ b/src/common/directives/creditCardCvv.directive.js
@@ -0,0 +1,39 @@
+import angular from 'angular'
+
+import './creditCardCvv.directive.scss'
+const directiveName = 'creditCardCvv'
+
+const template =
+``
+
+const creditCardCvv = /* @ngInject */ () => {
+ const directiveDefinitionObject = {
+ restrict: 'E',
+ template
+ }
+ return directiveDefinitionObject
+}
+
+export default angular
+ .module(directiveName, [])
+ .directive(directiveName, creditCardCvv)
diff --git a/src/common/directives/creditCardCvv.directive.scss b/src/common/directives/creditCardCvv.directive.scss
new file mode 100644
index 000000000..a394a814e
--- /dev/null
+++ b/src/common/directives/creditCardCvv.directive.scss
@@ -0,0 +1,48 @@
+
+.existing-payment-method {
+ .credit-card-cvv-container {
+ display: flex;
+ margin: 8px 20px 8px 38px;
+ .col-sm-4 {
+ padding-left: 10px;
+ padding-right: 0;
+ }
+ .form-control {
+ line-height: 1.6;
+ height: auto;
+ }
+ .credit-card-cvv-label {
+ margin-top: 9px;
+ margin-right: 9px;
+ font-size: 15px;
+ line-height: 1.2em;
+ font-weight: bold;
+ white-space: nowrap;
+ &::after {
+ content: ':';
+ }
+ }
+ .credit-card-cvv-help-block {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.give-modal-content, .branded-checkout {
+ .credit-card-cvv-label:not(.existing-payment-method .credit-card-cvv-label) {
+ text-transform: uppercase;
+ font-weight: 500;
+ letter-spacing: 1px;
+ font-size: 14px;
+ display: block;
+ margin-bottom: 4px;
+ &::after {
+ content: '*';
+ display: inline-block;
+ font-size: 115%;
+ line-height: 0;
+ font-family: "fontawesome", "Font Awesome 5 Pro";
+ margin-left: 6px;
+ }
+ }
+}