diff --git a/package-lock.json b/package-lock.json index de029cb4..26792b8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.4.0", "axios-retry": "^3.5.0", "bootstrap": "^5.3", + "iframe-resizer": "^4.3.9", "maska": "^1.5.0", "pinia": "^2.1.4", "pinia-plugin-persistedstate": "^2.2.0", @@ -9734,6 +9735,18 @@ } ] }, + "node_modules/iframe-resizer": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.9.tgz", + "integrity": "sha512-MCt+V/THB4a9OcAdrWo5NsI2CRpeMM4ijhTfiLtsdgDJXWYXf62Ve8yO8rKGmYNs991zty/EolYOxActlkfU+A==", + "engines": { + "node": ">=0.8.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/davidjbradshaw/iframe-resizer/blob/master/FUNDING.md" + } + }, "node_modules/ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", diff --git a/package.json b/package.json index 36f4c832..9c287dc0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "axios": "^1.4.0", "axios-retry": "^3.5.0", "bootstrap": "^5.3", + "iframe-resizer": "^4.3.9", "maska": "^1.5.0", "pinia": "^2.1.4", "pinia-plugin-persistedstate": "^2.2.0", diff --git a/public/css/README.md b/public/css/README.md new file mode 100644 index 00000000..74cd82e3 --- /dev/null +++ b/public/css/README.md @@ -0,0 +1,13 @@ +# Using Sass in this folder + +This public/css folder uses a standalone .scss > .css setup. +## Usage + +From a Terminal window, run the Sass Watch command from the public folder only: + +```bash +sass --no-source-map --watch scss:css +``` +This will compile and output .css files (into the public/css folder) that can be consumed by the Payment page. + +# Do not use the .css files on their own. You must use the .scss files and have Sass compile them to .css files. diff --git a/public/css/hop-styling.css b/public/css/hop-styling.css new file mode 100644 index 00000000..c81dda28 --- /dev/null +++ b/public/css/hop-styling.css @@ -0,0 +1,205 @@ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +body { + font-family: Roboto; + font-weight: 400; + color: #000; + margin: 0; + background-color: white; + padding: 8px; +} +body .validation-info { + display: none; +} +body .italicSmall { + font-style: normal; + font-size: 14px; +} +body span { + line-height: 24px; +} +body a { + color: #1574A1; + text-decoration: none; +} +body .form-group { + display: flex; + flex-direction: column; +} +body .headerlinecontainer .headerline1 { + font-size: 20px; + line-height: 32px; + font-weight: 400; + margin: 24px 0; + text-align: center; +} +body input, +body select { + margin-top: 4px; +} +body input, +body select { + border-radius: 8px; + border: 1px solid #8E9292; + padding: 16px; + font-size: 16px; + color: #000; + background-color: #fff; +} +body input:focus, body input:focus-visible, +body select:focus, +body select:focus-visible { + box-shadow: 0 0 0 2.5px #1574a1; + border: 1px solid transparent; + outline: none; +} +body input:disabled, body input.disabled, +body select:disabled, +body select.disabled { + background-color: #f5f5f5; + filter: grayscale(100%); +} +body input:disabled:hover, body input.disabled:hover, +body select:disabled:hover, +body select.disabled:hover { + box-shadow: 0 0 0 4px transparent; + border: 1px solid #8e9292; +} +body input:hover, +body select:hover { + border: 1px solid #8e9292; + box-shadow: 0 0 0 4px #9fcee6; +} +body select { + border-radius: 8px; + border: 1px solid #8E9292; + padding: 16px; + width: 100%; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: 95% 50%; + background-size: 15px 9px; + appearance: none; + font-size: 16px; + color: #000; +} +body .creditCardSpecific div { + padding: 8px 0; +} +body .creditCardSpecific div .headerlinecontainer { + display: none; +} +body .creditCardSpecific div label { + font-weight: 500; +} +body .creditCardSpecific #infoRow2 { + margin-bottom: 0; +} +body #cardExpirationContainer { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0; +} +@media (min-width: 400px) { + body #cardExpirationContainer { + flex-direction: row; + } + body #cardExpirationContainer .card_expirationYear, + body #cardExpirationContainer .card_expirationMonth { + display: flex; + flex-direction: column; + width: 100%; + } + body #cardExpirationContainer .card_expirationYear { + margin-left: 16px; + } +} +body .optional-label span { + display: none; +} +body .optional-label:after { + content: " (optional)"; + font-weight: 300; + font-style: normal; + font-size: 15px; +} +body .buttonContainer { + height: 48px; +} +body .buttonContainer button { + position: relative; + background: linear-gradient(270deg, #1574a1 0%, #003d58 100%); + border: none; + border-radius: 8px; + color: #fff; + justify-content: center; + font-weight: 500; + font-size: 16px; + height: 48px; +} +@media (hover: hover) { + body .buttonContainer button { + background: linear-gradient(270deg, #1574a1 0%, #003d58 100%); + } +} +body .buttonContainer button:focus { + box-shadow: 0 0 0 3px, 0 0 0 5.5px #06577c; +} +body .buttonContainer button:focus, body .buttonContainer button:focus-visible { + outline: none; + box-shadow: 0 0 0 3px, 0 0 0 5.5px #06577c; + color: #fff; + background: linear-gradient(270deg, rgb(6, 87, 124) 0%, rgb(6, 87, 124) 100%); +} +body .buttonContainer button:disabled { + background: #e3e4e4 !important; + background: linear-gradient(270deg, #e3e4e4 0%, #e3e4e4 100%) !important; + color: #4d5151 !important; + font-weight: 400; + height: 48px; + border: none; + border-radius: 8px; + cursor: pointer; + pointer-events: all; +} +body .buttonContainer button.has-loader { + color: #fff; + background: #06577c; + box-shadow: 0 0 0 3px, 0 0 0 5.5px #06577c; + pointer-events: auto; +} +body .buttonContainer button.delay { + transition: background 0s 0s ease-in-out; +} +body .has-error input, +body .has-error select { + border: 1px solid #d4281c; +} +body .has-error input:focus, +body .has-error select:focus { + box-shadow: 0 0 0 2.5px #fcbfbb; +} +body .has-error input:hover, +body .has-error select:hover { + border: 1px solid #d4281c; + box-shadow: 0 0 0 4px #fcbfbb; +} +body .has-error select { + border: 1px solid #d4281c; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: 95% 50%; + background-size: 15px 9px; + appearance: none; +} +body .has-error small { + margin-top: 4px; + color: #d4281c; +} +body .cc-error-container #alert-message { + background-color: #fcbfbb; + border: 1px solid #d4281c; +} diff --git a/public/css/site-2020Styling.css b/public/css/site-2020Styling.css new file mode 100644 index 00000000..07d33158 --- /dev/null +++ b/public/css/site-2020Styling.css @@ -0,0 +1,114 @@ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* Style the modal when PayPal button is selected */ +#pageLoadingModal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + display: flex; +} +#pageLoadingModal .modal-content { + margin: auto auto; +} +#pageLoadingModal p { + margin: 0; + text-transform: uppercase; + font-size: 14px; +} +#pageLoadingModal .modal { + display: none; + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + -webkit-overflow-scrolling: touch; + outline: 0; +} +#pageLoadingModal .modal-content { + position: relative; + background-color: #fff; + border-radius: 8px; + background-clip: padding-box; + outline: 0; + width: 176px; + height: 136px; + display: flex; + justify-content: center; + align-items: center; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 84 32'%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' fill='%23fff'/%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' fill='%23fff'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06.8c8.08 0 11.89.935 11.89.935 3.58.576 5.696 7.207 5.696 7.207h.727c0-2.427 1.302-2.397 2.341-2 .723.292 1.363.76 1.86 1.361 1.319 1.547.465 1.674.465 1.674h-5.067l3.846 3.01v12.016c0 2.41-2.029 2.31-2.029 2.31H22.338s-2.029.1-2.029-2.31V12.996l3.84-3.009H19.07s-.853-.127.466-1.674a4.693 4.693 0 0 1 1.86-1.361c1.032-.397 2.341-.427 2.341 2h.736s2.117-6.64 5.696-7.21c0 0 3.81-.942 11.89-.942Z' fill='%23fff' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06 8.847c7.924 0 14.685.511 14.685.511 0-2.585-2.38-6.011-2.38-6.011S50.4 2.519 42.06 2.519s-12.303.828-12.303.828-2.38 3.426-2.38 6.011c0 0 6.76-.51 14.683-.51Z' fill='%23DA291C' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M35.277 16.523a62.558 62.558 0 0 1 13.277 0m3.276-.439s2.384-2.257 8.608-2.257c0 0 1.179 2.657-2.54 3.37m-25.894-1.113s-2.387-2.257-8.611-2.257c0 0-1.16 2.579 2.543 3.37m-3.546 5.856s21.52 3.647 39.123 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: center center; + background-size: 96px; +} +#pageLoadingModal .modal-content:after { + content: ""; + border-radius: 50%; + position: absolute; + width: 5.5rem; + height: 5.5rem; + border: 0.45em solid #d4281c; + border-right-color: transparent; + animation: spinnerrotate 0.75s linear infinite; +} +@keyframes spinnerrotate { + 0% { + transform: rotate(0); + } + 100% { + transform: rotate(360deg); + } +} +#pageLoadingModal .modal-content p { + display: none; +} +#pageLoadingModal .modal-backdrop { + position: absolute; + top: 0; + right: 0; + left: 0; + background-color: #000000; +} +#pageLoadingModal .modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); +} +#pageLoadingModal.hide { + display: none; +} + +.buy-backdrop { + background-color: rgba(0, 0, 0, 0.3) !important; +} +.buy-backdrop #afterpay__iframe-checkout-container { + width: 100%; +} +.buy-backdrop #afterpay__iframe-checkout-container .style_closeWrapper__lIHJe { + display: none; +} + +.buy-container-closer { + display: none; +} + +@keyframes rotate { + 0% { + transform: rotate(0); + } + 100% { + transform: rotate(360deg); + } +} +.creditCardSpecific { + display: none; +} diff --git a/public/scss/hop-styling.scss b/public/scss/hop-styling.scss new file mode 100644 index 00000000..a71e4aa4 --- /dev/null +++ b/public/scss/hop-styling.scss @@ -0,0 +1,224 @@ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +body { + font-family: Roboto; + font-weight: 400; + color: #000; + margin: 0; + background-color: white; + padding: 8px; + + .validation-info { + display: none; + } + + .italicSmall { + font-style: normal; + font-size: 14px; + } + + span { + line-height: 24px; + } + + a { + color: #1574A1; + text-decoration: none; + } + + .form-group { + display: flex; + flex-direction: column; + } + + .headerlinecontainer { + .headerline1 { + font-size: 20px; + line-height: 32px; + font-weight: 400; + margin: 24px 0; + text-align: center; + } + } + + input, + select { + margin-top: 4px; + } + + input, + select { + border-radius: 8px; + border: 1px solid #8E9292; + padding: 16px; + font-size: 16px; + color: #000; + background-color: #fff; + &:focus, + &:focus-visible { + box-shadow: 0 0 0 2.5px #1574a1; + border: 1px solid transparent; + outline: none; + } + &:disabled, + &.disabled { + background-color: #f5f5f5; + filter: grayscale(100%); + &:hover { + box-shadow: 0 0 0 4px transparent; + border: 1px solid #8e9292; + } + } + &:hover { + border: 1px solid #8e9292; + box-shadow: 0 0 0 4px #9fcee6; + } + } + + select { + border-radius: 8px; + border: 1px solid #8E9292; + padding: 16px; + width: 100%; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: 95% 50%; + background-size: 15px 9px; + appearance: none; + font-size: 16px; + color: #000; + } + + .creditCardSpecific { + div { + padding: 8px 0; + .headerlinecontainer { + display: none; + } + label { + font-weight: 500; + } + } + #infoRow2 { + margin-bottom: 0; + } + } + + #cardExpirationContainer { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0; + @media (min-width: 400px) { + flex-direction: row; + .card_expirationYear, + .card_expirationMonth { + display: flex; + flex-direction: column; + width: 100%; + } + .card_expirationYear { + margin-left: 16px; + } + } + } + + .optional-label { + span { + display: none; + } + &:after { + content: " (optional)"; + font-weight: 300; + font-style: normal; + font-size: 15px; + } + } + + .buttonContainer { + height: 48px; + button { + position: relative; + background: linear-gradient(270deg, #1574a1 0%, #003d58 100%); + border: none; + border-radius: 8px; + color: #fff; + justify-content: center; + font-weight: 500; + font-size: 16px; + height: 48px; + @media (hover: hover) { + background: linear-gradient(270deg, #1574a1 0%, #003d58 100%); + } + &:focus { + box-shadow: 0 0 0 3px, 0 0 0 5.5px #06577c; + } + &:focus, // Mouse, touch, stylus focus + &:focus-visible { + // Keyboard focus for accessibility + outline: none; + box-shadow: 0 0 0 3px, 0 0 0 5.5px #06577c; + color: #fff; + background: linear-gradient(270deg, rgba(6, 87, 124, 1) 0%, rgba(6, 87, 124, 1) 100%); + } + &:disabled { + background: #e3e4e4 !important; + background: linear-gradient(270deg, #e3e4e4 0%, #e3e4e4 100%) !important; + color: #4d5151 !important; + font-weight: 400; + height: 48px; + border: none; + border-radius: 8px; + cursor: pointer; + pointer-events: all; + } + &.has-loader { + color: #fff; + background: #06577c; + box-shadow: 0 0 0 3px, 0 0 0 5.5px #06577c; + pointer-events: auto; + } + &.delay { + // fixes flicker while transitioning between states + transition: background 0s 0s ease-in-out; + } + } + } + + //Error Styling + .has-error { + input, + select { + border: 1px solid #d4281c; + &:focus { + box-shadow: 0 0 0 2.5px #fcbfbb; + } + &:hover { + border: 1px solid #d4281c; + box-shadow: 0 0 0 4px #fcbfbb; + } + } + select { + border: 1px solid #d4281c; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: 95% 50%; + background-size: 15px 9px; + appearance: none; + } + small { + margin-top: 4px; + color: #d4281c; + } + } + + .cc-error-container { + #alert-message { + background-color: #fcbfbb; + border: 1px solid #d4281c; + } + } + +} diff --git a/public/scss/site-2020Styling.scss b/public/scss/site-2020Styling.scss new file mode 100644 index 00000000..3cd0839b --- /dev/null +++ b/public/scss/site-2020Styling.scss @@ -0,0 +1,118 @@ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ +/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ + +/* Style the modal when PayPal button is selected */ +#pageLoadingModal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.40); + display: flex; + + .modal-content { + margin: auto auto; + } + + p { + margin: 0; + text-transform: uppercase; + font-size: 14px; + } + + .modal { + display: none; + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + -webkit-overflow-scrolling: touch; + outline: 0; + } + + .modal-content { + position: relative; + background-color: #fff; + border-radius: 8px; + background-clip: padding-box; + outline: 0; + width: 176px; + height: 136px; + display: flex; + justify-content: center; + align-items: center; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 84 32'%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' fill='%23fff'/%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' fill='%23fff'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06.8c8.08 0 11.89.935 11.89.935 3.58.576 5.696 7.207 5.696 7.207h.727c0-2.427 1.302-2.397 2.341-2 .723.292 1.363.76 1.86 1.361 1.319 1.547.465 1.674.465 1.674h-5.067l3.846 3.01v12.016c0 2.41-2.029 2.31-2.029 2.31H22.338s-2.029.1-2.029-2.31V12.996l3.84-3.009H19.07s-.853-.127.466-1.674a4.693 4.693 0 0 1 1.86-1.361c1.032-.397 2.341-.427 2.341 2h.736s2.117-6.64 5.696-7.21c0 0 3.81-.942 11.89-.942Z' fill='%23fff' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06 8.847c7.924 0 14.685.511 14.685.511 0-2.585-2.38-6.011-2.38-6.011S50.4 2.519 42.06 2.519s-12.303.828-12.303.828-2.38 3.426-2.38 6.011c0 0 6.76-.51 14.683-.51Z' fill='%23DA291C' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M35.277 16.523a62.558 62.558 0 0 1 13.277 0m3.276-.439s2.384-2.257 8.608-2.257c0 0 1.179 2.657-2.54 3.37m-25.894-1.113s-2.387-2.257-8.611-2.257c0 0-1.16 2.579 2.543 3.37m-3.546 5.856s21.52 3.647 39.123 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: center center; + background-size: 96px; + &:after { + content: ''; + border-radius: 50%; + position: absolute; + width: 5.5rem; + height: 5.5rem; + border: 0.45em solid #d4281c; + border-right-color: transparent; + animation: spinnerrotate 0.75s linear infinite; + } + @keyframes spinnerrotate { + 0% { transform: rotate(0)} + 100% { transform: rotate(360deg)} + } + p { + display: none; + } + } + + .modal-backdrop { + position: absolute; + top: 0; + right: 0; + left: 0; + background-color: #000000; + &.fade { + opacity: 0; + filter: alpha(opacity=0); + } + } + + &.hide { + display: none; + } +} + +// AfterPay Styles + +.buy-backdrop { + background-color: rgba(0,0,0,.3)!important; + #afterpay__iframe-checkout-container { + width: 100%; + .style_closeWrapper__lIHJe { + display: none; + } + } +} + +.buy-container-closer { + display: none; +} + +// Modal Spinner + +@keyframes rotate { + 0% { transform: rotate(0)} + 100% { transform: rotate(360deg)} +} + +.creditCardSpecific { + display: none; +} diff --git a/src/constants/application-config.js b/src/constants/application-config.js index 06fd28b5..c0bcd8de 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-globals */ /** * @module applicationConfig * @author T-Wrecks Team @@ -16,7 +17,17 @@ const applicationConfig = Object.freeze({ CLIENTTAG_QUERYSTRING: 'ClientTag', GOOGLE_PLACES_API_KEY: process.env.VUE_APP_GOOGLE_PLACES_API_KEY, ISS_DEV_CMS_DOMAIN: 'https://digitalisscms.dev.safelite.io', - CASH_PARENT_ACCOUNT_NUMBER: 167132 + CASH_PARENT_ACCOUNT_NUMBER: 167132, + PAY_IN_ADVANCE_RESPONSE_URL: + `${location.protocol + }//${ + location.host + }/?issPage=payment-return&src=iss-nextgen`, + PAY_IN_ADVANCE_CANCEL_URL: + `${location.protocol + }//${ + location.host + }/?issPage=payment-method&src=iss-nextgen` }); export default applicationConfig; diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 60ef3ba8..7f797462 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -178,6 +178,10 @@ const endpoints = Object.freeze({ FinalDeductible: { url: '/coverage/api/v1/coverage/final-deductible', method: 'POST' + }, + GetPaymentSignature: { + url: '/order/api/v1/order/sign', + method: 'POST' } }); diff --git a/src/layouts/payment-page/payment-page.spec.js b/src/layouts/payment-page/payment-page.spec.js new file mode 100644 index 00000000..c7e66af8 --- /dev/null +++ b/src/layouts/payment-page/payment-page.spec.js @@ -0,0 +1,492 @@ +// Components +import payment from '@/layouts/payment-page/payment-page.vue'; + +// Supporting Files +import { shallowMount } from '@vue/test-utils'; +import { getMountOptions } from '@/helpers/unit-test-helper'; +import { useMainStore } from '@/store'; +import paymentMethods from '@/constants/payment-method-constants' + +// Constants +const parts = { + windshield: { + name: 'windshield', + canSafeliteRecalibrate: true, + childParts: [], + color: 'Green Tint', + description: + 'solar, soundproofing, lane keep assist, lane departure warning system, w/adaptive cruise control', + id: 'db22fd44-10dd-456f-979b-ff88cf68cca6', + partNumber: 'FW04896GTYN', + partType: 'WINDSHIELD', + recalibrationType: 'STATIC', + requiresCapabilityQuestions: false, + requiresRecalibration: true, + salesTax: 63.86, + sellingPrice: 791.46 + }, + frontWipers: { + name: 'front wipers', + partNumber: 'SBB16', + description: 'SAFELITE BEAM BLADE 16', + partType: 'FRONT WIPER', + price: 32.64 + }, + rearWipers: { + name: 'rear wipers', + partNumber: 'SBBR12A', + description: 'SAFELITE REAR BLADE 12A', + partType: 'REAR WIPER', + price: 24.48 + }, + rainDefense: { + name: 'rain defense', + partNumber: 'RAIN DEFENSE', + description: null, + partType: 'RAIN DEFENSE', + price: 35.5 + } +}; + +const pricedParts = { + windshield: { + ...parts.windshield, + kitPrice: 0, + laborAmount: 60, + salesTax: 0, + sellingPrice: 791.46 + }, + frontWipers: { + ...parts.frontWipers, + kitPrice: 0, + laborAmount: 20, + salesTax: 0, + sellingPrice: 32.64 + }, + rearWipers: { + ...parts.rearWipers, + kitPrice: 10, + laborAmount: 10, + salesTax: 0, + sellingPrice: 24.48 + }, + rainDefense: { + ...parts.rainDefense, + kitPrice: 20, + laborAmount: 0, + salesTax: 0, + sellingPrice: 35.5 + } +}; + +const taxedParts = { + windshield: { + ...pricedParts.windshield, + salesTax: 63.86, + subTotal: 100 + }, + frontWipers: { + ...pricedParts.frontWipers, + salesTax: 2.36, + subTotal: 100 + }, + rearWipers: { + ...pricedParts.rearWipers, + salesTax: 6.0, + subTotal: 100 + }, + rainDefense: { + ...pricedParts.rainDefense, + salesTax: 1.0, + subTotal: 100 + } +}; + +// Setup global mocks +let mockCmsContent = {}; + +jest.mock('@/mixins/base-mixin.js', () => ({ + methods: { + getAmountDue: jest.fn().mockImplementation(() => 5), + getDisplayAmountDue: jest.fn().mockImplementation(() => '$5.00') + } +})); + +jest.mock('@/helpers/cms-content-helper', () => ({ + fetchCmsContentForPage: () => Promise.resolve('content') +})); + +function setupMocks({ customMountOptions }) { + const mountOptions = getMountOptions({ + ...customMountOptions, + route: { query: { issPage: 'page-name' }, params: {} } + }); + + // set all mock stuff + mountOptions.mixins = [ + { + methods: { + getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => { + if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName]) { + return mockCmsContent[widgetName][fieldName]; + } + return undefined; + }), + setCmsContent: jest.fn() + } + } + ]; + + const wrapper = shallowMount(payment, mountOptions); + + // act on vm + + return wrapper; +} + +describe('payment-page.vue', () => { + beforeEach(() => { + useMainStore().order = + { + vehicle: { + year: '2020', + make: 'acura', + model: 'mdx', + style: '4-door sedan', + carId: 'dummyCarId', + category: 'dummyCategory', + vin: 'dummyVin' + }, + serviceLocation: { + address: 'add1', + address2: 'add2', + city: 'city', + state: 'state', + zipCode: 'zip', + zipCodeCtu: 'zipCtu', + appointmentType: 'IN_SHOP', + isVehicleProtected: true, + provider: { + providerNumber: 2, + address: { + streetAddress: 'add3', + city: 'city2', + state: 'state2', + zipCode: 'zip2', + zipCodeCtu: 'zipCtu2' + } + }, + techNotes: '' + }, + customer: { + firstName: 'first', + lastName: 'last', + emailAddress: 'builddigitaltest@safelite.com', + phoneNumber: '555-555-5555', + isSmsOptIn: false + }, + damage: { + isRepair: false, + numberOfChips: null, + glassToReplace: [{ location: 'windshield' }] + }, + lineItems: { + glassParts: [parts.windshield], + supportingItems: [], + vaps: [parts.frontWipers], + promos: [] + }, + payment: { + isInsurance: false, + insuranceCoverage: { + isVerified: null, + coverageStatus: null, + coverageVerificationType: null + }, + isPayInAdvance: true, + payInAdvanceType: 'Afterpay', + inactivePromos: [] + }, + schedule: { + date: 'date', + startTime: 'start', + endTime: 'end', + jobMinMinutes: '30', + jobMaxMinutes: '45' + } + }; + + mockCmsContent = {}; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('arePagePrerequisitesValid', () => { + test('Returns true in nominal conditions', () => { + // Arrange + // Store defaults are valid + const wrapper = setupMocks({}); + + // Act + const result = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(result).toBe(true); + }); + + describe('Payment method cases', () => { + test('Returns false if pia options are not valid', () => { + // Arrange + useMainStore().order.payment.isPayInAdvance = null; + useMainStore().order.payment.payInAdvanceType = null; + const wrapper = setupMocks({}); + + // Act + const result = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(result).toBe(false); + }); + + test('Returns false if pay at time of service', () => { + // Arrange + useMainStore().order.payment.isPayInAdvance = false; + useMainStore().order.payment.payInAdvanceType = null; + const wrapper = setupMocks({}); + + // Act + const result = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(result).toBe(false); + }); + }); + }); + + describe('beforeRouteEnter', () => { + test('Properly initializes fields and kicks off hop', async () => { + // Arrange + const vmMock = { + setCmsContent: jest.fn(), + $refs: { + cart: { + cartItems: [] + } + }, + getPiaLineItems: jest.fn(), + fetchSignatureInfo: jest.fn(), + setIFrameListener: jest.fn(), + + $nextTick: (f) => { + f(); + } + }; + + const nextF = (f) => { + f(vmMock); + }; + + // Act + await payment.beforeRouteEnter.call( + vmMock, + { query: { issPage: 'payment-page' } }, + undefined, + nextF + ); + + // Assert + expect(vmMock.setCmsContent).toBeCalled(); + expect(vmMock.fetchSignatureInfo).toBeCalled(); + expect(vmMock.setIFrameListener).toBeCalled(); + }); + }); + + describe('payment type mapping', () => { + describe('getPaymentType', () => { + test('Maps AFTERPAY -> ap', () => { + // Arrange + useMainStore().order.payment.payInAdvanceType = paymentMethods.AFTERPAY; + const wrapper = setupMocks({}); + + // Act + const mapped = wrapper.vm.getPaymentType(); + + // Assert + expect(mapped).toBe('ap'); + }); + + test('Maps CREDIT_CARD -> cc', () => { + // Arrange + useMainStore().order.payment.payInAdvanceType = paymentMethods.CREDIT_CARD; + const wrapper = setupMocks({}); + + // Act + const mapped = wrapper.vm.getPaymentType(); + + // Assert + expect(mapped).toBe('cc'); + }); + + test('Maps PAYPAL -> pp', () => { + // Arrange + useMainStore().order.payment.payInAdvanceType = paymentMethods.PAYPAL; + const wrapper = setupMocks({}); + + // Act + const mapped = wrapper.vm.getPaymentType(); + + // Assert + expect(mapped).toBe('pp'); + }); + + test('Maps other values to self', () => { + // Arrange + useMainStore().order.payment.payInAdvanceType = 'SomeOtherText'; + const wrapper = setupMocks({}); + + // Act + const mapped = wrapper.vm.getPaymentType(); + + // Assert + expect(mapped).toBe('SomeOtherText'); + }); + }); + + describe('isPaypal', () => { + test('Is responsive if initial data changes', async () => { + // Arrange + // note: begins with payment-type = afterpay + const wrapper = setupMocks({}); + + // Act + const preVal = wrapper.vm.isPaypal; + wrapper.vm.paymentType = 'pp'; + await wrapper.vm.$nextTick(); + + const postVal = wrapper.vm.isPaypal; + + // Assert + expect(preVal).toBe(false); + expect(postVal).toBe(true); + }); + }); + }); + + describe('hop form', () => { + describe('fetchSignatureInfo', () => { + test('Sets auth fields', async () => { + // Arrange + const signature = { + token: 'TOKEN', + signature: 'SIGNATURE', + startDate: 'STARTDATE' + }; + const wrapper = setupMocks({}); + + wrapper.vm.submitHopForm = jest.fn(); + + // Act + await wrapper.vm.fetchSignatureInfo(signature); + + // Assert + expect(wrapper.vm.authToken).toBe(signature.token); + expect(wrapper.vm.authSignature).toBe(signature.signature); + expect(wrapper.vm.authSignatureStart).toBe(signature.startDate); + + expect(wrapper.vm.submitHopForm).toBeCalled(); + }); + }); + + describe('submitHopForm', () => { + test('Submits form', async () => { + // Arrange + const wrapper = setupMocks({}); + + wrapper.vm.$refs.hopForm.submit = jest.fn(); + wrapper.vm.$refs.paymentFrame = null; + + // Act + wrapper.vm.submitHopForm(); + + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.$refs.hopForm.submit).toBeCalled(); + }); + }); + + describe('handleIFrameContentWindowMessage', () => { + test('Navigates back if afterpay is closed', () => { + // Arrange + const event = { + data: 'afterpayClosed' + }; + + const wrapper = setupMocks({}); + + wrapper.vm.backButtonAction = jest.fn(); + + // Act + wrapper.vm.handleIFrameContentWindowMessage(event); + + // Assert + expect(wrapper.vm.backButtonAction).toBeCalled(); + }); + + test('Blocks UI interaction if credit card is submitted', () => { + // Arrange + const event = { + data: 'creditCardSubmit' + }; + + const wrapper = setupMocks({}); + + // Act + wrapper.vm.handleIFrameContentWindowMessage(event); + + // Assert + expect(wrapper.vm.shouldBlockInteraction).toBe(true); + }); + }); + }); + + describe('UI Blocking', () => { + test('UI block appears when toggled', async () => { + // Arrange + const wrapper = setupMocks({}); + + // Act + wrapper.vm.setUIBlock(true); + + await wrapper.vm.$nextTick(); + + const uiBlockElements = wrapper.findAll('.ui-block'); + + // Assert + expect(uiBlockElements.length).toBeGreaterThan(0); + }); + + test("Don't propogate clicks from UI block", async () => { + // Arrange + const wrapper = setupMocks({}); + + const outerDiv = wrapper.find('.container-fluid'); + const clickFn = jest.fn(); + outerDiv.element.addEventListener('click', clickFn); + + // Act + wrapper.vm.setUIBlock(true); + + await wrapper.vm.$nextTick(); + + const uiBlockElement = wrapper.find('.ui-block'); + + await uiBlockElement.trigger('click'); + + // Assert + expect(clickFn).not.toBeCalled(); + }); + }); +}); diff --git a/src/layouts/payment-page/payment-page.vue b/src/layouts/payment-page/payment-page.vue index 6e8cfb30..86f9089d 100644 --- a/src/layouts/payment-page/payment-page.vue +++ b/src/layouts/payment-page/payment-page.vue @@ -8,17 +8,155 @@
-

Placeholder for payment page

+
+ +
+ + @backClicked="backButtonAction" />
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/router/router-constants/externalUrl-values.js b/src/router/router-constants/externalUrl-values.js new file mode 100644 index 00000000..4a44e9c0 --- /dev/null +++ b/src/router/router-constants/externalUrl-values.js @@ -0,0 +1,5 @@ +const externalUrls = { + SAFELITE_HOP: process.env.VUE_APP_SAFELITE_HOP +}; + +export default externalUrls; diff --git a/src/store/index.js b/src/store/index.js index c8623193..1b963902 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2144,6 +2144,13 @@ export const useMainStore = defineStore({ const isPayInAdvance = paymentMethod !== paymentMethods.PAY_AT_TIME_OF_SERVICE; this.order.payment.isPayInAdvance = isPayInAdvance; this.order.payment.payInAdvanceType = isPayInAdvance ? paymentMethod : null; + }, + + getPaymentSignature() { + return globalMethods.callHttpClient({ + method: endpoints.GetPaymentSignature.method, + endpoint: endpoints.GetPaymentSignature.url + }); } }, diff --git a/vue.config.js b/vue.config.js index dcc30a25..24f2d771 100644 --- a/vue.config.js +++ b/vue.config.js @@ -2,6 +2,7 @@ process.env.VUE_APP_CONSUMER_CF_DISTRO = 'https://digitalapi.dev.safelite.io'; process.env.VUE_APP_CURRENT_ENVIRONMENT = 'Localhost'; process.env.VUE_APP_GOOGLE_PLACES_API_KEY = 'AIzaSyCuLhQcDdZTTb4JzpUFms1OCch2dk5lHF0'; +process.env.VUE_APP_SAFELITE_HOP = 'https://sv2-safelitehop-sys.safelite.com/fmgCheckoutEmbedded.aspx'; // GA & GTM // NOTE: Using the old ISS site GTM Cotnainer ID for now, will create a new one soon.