From 046559da7fdba9b3f2e0fe1bc46ca189e0fa6eff Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 22 Aug 2024 09:39:50 +0200 Subject: [PATCH 01/24] Added CustomCard story --- .../lib/storybook/config/paymentsConfig.ts | 8 + .../stories/cards/CustomCard.stories.tsx | 48 +++ .../customCardHelpers/CustomCardContainer.tsx | 143 +++++++++ .../customCardHelpers/customCard.config.js | 299 ++++++++++++++++++ .../customCardHelpers/customCard.style.scss | 110 +++++++ 5 files changed, 608 insertions(+) create mode 100644 packages/lib/storybook/stories/cards/CustomCard.stories.tsx create mode 100644 packages/lib/storybook/stories/cards/customCardHelpers/CustomCardContainer.tsx create mode 100644 packages/lib/storybook/stories/cards/customCardHelpers/customCard.config.js create mode 100644 packages/lib/storybook/stories/cards/customCardHelpers/customCard.style.scss diff --git a/packages/lib/storybook/config/paymentsConfig.ts b/packages/lib/storybook/config/paymentsConfig.ts index 57c7390f00..4c10c9c768 100644 --- a/packages/lib/storybook/config/paymentsConfig.ts +++ b/packages/lib/storybook/config/paymentsConfig.ts @@ -14,6 +14,14 @@ const paymentsConfig = { // RequestedTestAcquirerResponseCode: 2, allow3DS2: true }, + // Ready for v69+ - lose any additionalData 3DS2 related lines e.g. allow3DS2: true + authenticationData: { + attemptAuthentication: 'always', + // To force MDFlow: comment out below, and just keep line above + threeDSRequestData: { + nativeThreeDS: 'preferred' + } + }, shopperEmail: 'test-shopper@storytel.com', shopperIP: '172.30.0.1', // threeDS2RequestData: { diff --git a/packages/lib/storybook/stories/cards/CustomCard.stories.tsx b/packages/lib/storybook/stories/cards/CustomCard.stories.tsx new file mode 100644 index 0000000000..b30449ef7b --- /dev/null +++ b/packages/lib/storybook/stories/cards/CustomCard.stories.tsx @@ -0,0 +1,48 @@ +import { MetaConfiguration, PaymentMethodStoryProps, StoryConfiguration } from '../types'; +import { getStoryContextCheckout } from '../../utils/get-story-context-checkout'; +import { CustomCardConfiguration } from '../../../src/components/CustomCard/types'; +import { CustomCard } from '../../../src'; +import { CustomCardContainer } from './customCardHelpers/CustomCardContainer'; +import './customCardHelpers/customCard.style.scss'; +import { styles, setFocus, onBrand, onConfigSuccess, onBinLookup, onChange, setCCErrors } from './customCardHelpers/customCard.config'; + +type customCardStory = StoryConfiguration; + +const meta: MetaConfiguration = { + title: 'Cards/Custom Card' +}; + +const createComponent = (args: PaymentMethodStoryProps, context) => { + const { componentConfiguration } = args; + const checkout = getStoryContextCheckout(context); + const customCard = new CustomCard(checkout, componentConfiguration); + + window['customCard'] = customCard; + + return ; +}; + +export const Default: customCardStory = { + render: createComponent, + args: { + componentConfiguration: { + styles, + onConfigSuccess, + onBrand, + // onBinValue: cbObj => { + // if (cbObj.encryptedBin) { + // console.log('onBinValue', cbObj); + // } + // }, + onFocus: setFocus, + onBinLookup, + onChange, + onValidationError: errors => { + errors.forEach(setCCErrors); + } + }, + useSessions: false + } +}; + +export default meta; diff --git a/packages/lib/storybook/stories/cards/customCardHelpers/CustomCardContainer.tsx b/packages/lib/storybook/stories/cards/customCardHelpers/CustomCardContainer.tsx new file mode 100644 index 0000000000..a33760ad78 --- /dev/null +++ b/packages/lib/storybook/stories/cards/customCardHelpers/CustomCardContainer.tsx @@ -0,0 +1,143 @@ +import { useEffect, useRef, useState } from 'preact/hooks'; +import { makePayment } from '../../../helpers/checkout-api-calls'; +import paymentsConfig from '../../../config/paymentsConfig'; +import getCurrency from '../../../utils/get-currency'; +import { handleFinalState } from '../../../helpers/checkout-handlers'; + +export const CustomCardContainer = ({ element, context }) => { + const container = useRef(null); + + const createPayButton = (parent, component, attribute) => { + const payBtn = document.createElement('button'); + + payBtn.textContent = 'Pay'; + payBtn.name = 'pay'; + payBtn.classList.add('adyen-checkout__button', 'js-components-button--one-click', `js-${attribute}`); + + payBtn.addEventListener('click', e => { + e.preventDefault(); + startPayment(component); + }); + + document.querySelector(parent).appendChild(payBtn); + + return payBtn; + }; + + const startPayment = component => { + if (!component.isValid) return component.showValidation(); + + const allow3DS2 = paymentsConfig.authenticationData.attemptAuthentication || 'never'; + + makePayment(component.data, { + amount: { value: context.args.amount, currency: getCurrency(context.args.countryCode) }, + authenticationData: { + attemptAuthentication: allow3DS2, + // comment out below if you want to force MDFlow + threeDSRequestData: { + nativeThreeDS: 'preferred' + } + } + }) + .then(result => { + handlePaymentResult(result, component); + }) + .catch(error => { + throw Error(error); + }); + }; + + const handlePaymentResult = (result, component) => { + console.log('Result: ', result); + + if (result.action) { + threeDS2(result, component); + } else { + clearSFMarkup(); + + switch (result.resultCode) { + case 'Authorised': + case 'Refused': + handleFinalState(result, component); + break; + default: + } + } + }; + + const threeDS2 = (result, component) => { + clearSFMarkup(); + + component.handleAction(result.action); + }; + + const clearSFMarkup = () => { + if (globalThis.customCard) { + const sfNode = container.current; // equivalent to: document.querySelector('.secured-fields'); + // Clear the contents of the .secured-fields div + while (sfNode.firstChild) { + sfNode.removeChild(sfNode.firstChild); + } + } + }; + + useEffect(() => { + if (!element) return; + + element.mount(container.current); + + globalThis.payBtn = createPayButton('.secured-fields', globalThis.customCard, 'customcard'); + }, [element]); + + return ( +
+
+ + card + + + + + + + + + +
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/packages/lib/storybook/stories/cards/customCardHelpers/customCard.config.js b/packages/lib/storybook/stories/cards/customCardHelpers/customCard.config.js new file mode 100644 index 0000000000..5d56999183 --- /dev/null +++ b/packages/lib/storybook/stories/cards/customCardHelpers/customCard.config.js @@ -0,0 +1,299 @@ +let hideCVC = false; +let optionalCVC = false; +let hideDate = false; +let optionalDate = false; +let isDualBranding = false; + +function setAttributes(el, attrs) { + for (const key in attrs) { + el.setAttribute(key, attrs[key]); + } +} + +function setLogosActive(rootNode, mode) { + const imageHolder = rootNode.querySelector('.pm-image'); + const dualBrandingImageHolder = rootNode.querySelector('.pm-image-dual'); + + switch (mode) { + case 'dualBranding_notValid': + Object.assign(imageHolder.style, { display: 'none' }); + Object.assign(dualBrandingImageHolder.style, { display: 'block', 'pointer-events': 'none', opacity: 0.5 }); + break; + + case 'dualBranding_valid': + Object.assign(imageHolder.style, { display: 'none' }); + Object.assign(dualBrandingImageHolder.style, { display: 'block', 'pointer-events': 'auto', opacity: 1 }); + break; + + default: + // reset + Object.assign(imageHolder.style, { display: 'block' }); + Object.assign(dualBrandingImageHolder.style, { display: 'none' }); + } +} + +export const styles = { + // base: { + // caretColor: 'red' + // }, + error: { + color: 'red' + }, + validated: { + color: 'green', + fontWeight: 'bold' + // background: 'blue' + }, + placeholder: { + color: '#d8d8d8' + } +}; + +export function onConfigSuccess(pCallbackObj) { + /** + * Set the UI to it's starting state + */ + document.querySelector('.card-input__spinner__holder').style.display = 'none'; + + pCallbackObj.rootNode.style.display = 'block'; + + pCallbackObj.rootNode.querySelector('.pm-image-dual').style.display = 'none'; + + setLogosActive(pCallbackObj.rootNode); + + /** + * Set focus on first element + */ + setTimeout(() => { + // Allow time for screen to redraw after spinner is hidden + window.customCard.setFocusOn('encryptedCardNumber'); + }, 100); + + // window.customCard.updateStyles({ + // base: { + // color: '#000', + //// fontSize: '18px', + //// lineHeight: '18px' + // }, + // error: { + // color: 'orange' + // }, + // validated: { + // color: 'blue', + // fontWeight: 'bold' + // }, + // placeholder: { + // color: 'green' + // } + // }); +} + +export function setCCErrors(pCallbackObj) { + if (!pCallbackObj.rootNode) return; + + const sfNode = pCallbackObj.rootNode.querySelector(`[data-cse="${pCallbackObj.fieldType}"]`); + const errorNode = sfNode.parentNode.querySelector('.pm-form-label__error-text'); + + if (errorNode.innerText === '' && pCallbackObj.error === '') return; + + if (pCallbackObj.error !== '') { + errorNode.style.display = 'block'; + errorNode.innerText = pCallbackObj.errorI18n; + + // Add error classes + setErrorClasses(sfNode, true); + return; + } + + // Else: error === '' + errorNode.style.display = 'none'; + errorNode.innerText = ''; + + // Remove error classes + setErrorClasses(sfNode, false); +} + +export function setFocus(pCallbackObj) { + const sfNode = pCallbackObj.rootNode.querySelector(`[data-cse="${pCallbackObj.fieldType}"]`); + setFocusClasses(sfNode, pCallbackObj.focus); +} + +export function onBrand(pCallbackObj) { + /** + * If not in dual branding mode - add card brand to first image element + */ + if (!isDualBranding) { + const brandLogo1 = pCallbackObj.rootNode.querySelector('.pm-image-1'); + setAttributes(brandLogo1, { + src: pCallbackObj.brandImageUrl, + alt: pCallbackObj.brand + }); + } + + /** + * Deal with showing/hiding CVC field + */ + const cvcNode = pCallbackObj.rootNode.querySelector('.pm-form-label--cvc'); + + if (pCallbackObj.cvcPolicy === 'hidden' && !hideCVC) { + hideCVC = true; + cvcNode.style.display = 'none'; + } + + if (hideCVC && pCallbackObj.cvcPolicy !== 'hidden') { + hideCVC = false; + cvcNode.style.display = 'block'; + } + + // Optional cvc fields + if (pCallbackObj.cvcPolicy === 'optional' && !optionalCVC) { + optionalCVC = true; + if (cvcNode) cvcNode.querySelector('.pm-form-label__text').innerText = 'CVV/CVC (optional):'; + } + + if (optionalCVC && pCallbackObj.cvcPolicy !== 'optional') { + optionalCVC = false; + if (cvcNode) cvcNode.querySelector('.pm-form-label__text').innerText = 'CVV/CVC'; + } + + /** + * Deal with showing/hiding date field(s) + */ + const dateNode = pCallbackObj.rootNode.querySelector('.pm-form-label--exp-date'); + const monthNode = pCallbackObj.rootNode.querySelector('.pm-form-label--exp-month'); + const yearNode = pCallbackObj.rootNode.querySelector('.pm-form-label--exp-year'); + + if (pCallbackObj.expiryDatePolicy === 'hidden' && !hideDate) { + hideDate = true; + if (dateNode) dateNode.style.display = 'none'; + if (monthNode) monthNode.style.display = 'none'; + if (yearNode) yearNode.style.display = 'none'; + } + + if (hideDate && pCallbackObj.expiryDatePolicy !== 'hidden') { + hideDate = false; + if (dateNode) dateNode.style.display = 'block'; + if (monthNode) monthNode.style.display = 'block'; + if (yearNode) yearNode.style.display = 'block'; + } + + // Optional date fields + if (pCallbackObj.expiryDatePolicy === 'optional' && !optionalDate) { + optionalDate = true; + if (dateNode) dateNode.querySelector('.pm-form-label__text').innerText = 'Expiry date (optional):'; + if (monthNode) monthNode.querySelector('.pm-form-label__text').innerText = 'Expiry month (optional):'; + if (yearNode) yearNode.querySelector('.pm-form-label__text').innerText = 'Expiry year (optional):'; + } + + if (optionalDate && pCallbackObj.expiryDatePolicy !== 'optional') { + optionalDate = false; + if (dateNode) dateNode.querySelector('.pm-form-label__text').innerText = 'Expiry date:'; + if (monthNode) monthNode.querySelector('.pm-form-label__text').innerText = 'Expiry month:'; + if (yearNode) yearNode.querySelector('.pm-form-label__text').innerText = 'Expiry year:'; + } +} + +function dualBrandListener(e) { + customCard.dualBrandingChangeHandler(e); +} + +function resetDualBranding(rootNode) { + isDualBranding = false; + + setLogosActive(rootNode); + + const brandLogo1 = rootNode.querySelector('.pm-image-dual-1'); + brandLogo1.removeEventListener('click', dualBrandListener); + + const brandLogo2 = rootNode.querySelector('.pm-image-dual-2'); + brandLogo2.removeEventListener('click', dualBrandListener); +} + +/** + * Implementing dual branding + */ +function onDualBrand(pCallbackObj) { + const brandLogo1 = pCallbackObj.rootNode.querySelector('.pm-image-dual-1'); + const brandLogo2 = pCallbackObj.rootNode.querySelector('.pm-image-dual-2'); + + isDualBranding = true; + + const supportedBrands = pCallbackObj.supportedBrandsRaw; + + /** + * Set first brand icon (and, importantly also add alt &/or data-value attrs); and add event listener + */ + setAttributes(brandLogo1, { + src: supportedBrands[0].brandImageUrl, + alt: supportedBrands[0].brand, + 'data-value': supportedBrands[0].brand + }); + + brandLogo1.addEventListener('click', dualBrandListener); + + /** + * Set second brand icon (and, importantly also add alt &/or data-value attrs); and add event listener + */ + setAttributes(brandLogo2, { + src: supportedBrands[1].brandImageUrl, + alt: supportedBrands[1].brand, + 'data-value': supportedBrands[1].brand + }); + brandLogo2.addEventListener('click', dualBrandListener); +} + +export function onBinLookup(pCallbackObj) { + /** + * Dual branded result... + */ + if (pCallbackObj.supportedBrandsRaw?.length > 1) { + onDualBrand(pCallbackObj); + return; + } + + /** + * ...else - binLookup 'reset' result or binLookup result with only one brand + */ + resetDualBranding(pCallbackObj.rootNode); +} + +export function onChange(state, component) { + /** + * If we're in a dual branding scenario & the number field becomes valid or is valid and become invalid + * - set the brand logos to the required 'state' + */ + if (isDualBranding) { + const mode = state.valid.encryptedCardNumber ? 'dualBranding_valid' : 'dualBranding_notValid'; + setLogosActive(document.querySelector('.secured-fields'), mode); + } +} + +const setErrorClasses = function (pNode, pSetErrors) { + if (pSetErrors) { + if (pNode.className.indexOf('pm-input-field--error') === -1) { + pNode.className += ' pm-input-field--error'; + } + return; + } + + // Remove errors + if (pNode.className.indexOf('pm-input-field--error') > -1) { + const newClassName = pNode.className.replace('pm-input-field--error', ''); + pNode.className = newClassName.trim(); + } +}; + +const setFocusClasses = function (pNode, pSetFocus) { + if (pSetFocus) { + if (pNode.className.indexOf('pm-input-field--focus') === -1) { + pNode.className += ' pm-input-field--focus'; + } + return; + } + + // Remove focus + if (pNode.className.indexOf('pm-input-field--focus') > -1) { + const newClassName = pNode.className.replace('pm-input-field--focus', ''); + pNode.className = newClassName.trim(); + } +}; diff --git a/packages/lib/storybook/stories/cards/customCardHelpers/customCard.style.scss b/packages/lib/storybook/stories/cards/customCardHelpers/customCard.style.scss new file mode 100644 index 0000000000..12c01508ae --- /dev/null +++ b/packages/lib/storybook/stories/cards/customCardHelpers/customCard.style.scss @@ -0,0 +1,110 @@ +.secured-fields { + position: relative; + font-family: 'Open Sans', sans-serif; + font-size: 14px; + padding: 24px; +} +.pm-image, .pm-image-dual { + background-color: #ffffff; + border-radius: 4px; + -moz-boder-radius: 4px; + -webkit-border-radius: 4px; + float: right; + line-height: 0; + position: relative; + overflow: hidden; +} +.pm-form-label { + float: left; + padding-bottom: 1em; + position: relative; + width: 100%; +} +.pm-form-label--exp-date { + width: 40%; +} +.pm-form-label--cvc { + float: right; + width: 40%; +} +.pm-form-label__text { + color: #00112c; + float: left; + font-size: 0.93333em; + padding-bottom: 6px; + position: relative; +} +.pm-input-field { + background: white; + border: 1px solid #d8d8d8; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + box-sizing: border-box; + clear: left; + font-size: 0.93333333333em; + float: left; + padding: 8px; + position: relative; + width: 100%; + height: 35px; +} + +.pm-form-label__error-text { + color: #ff7d00; + display: none; + float: left; + font-size: 13px; + padding-top: 0.4em; + position: relative; + width: 100%; +} + +/* Set dynamically */ +.pm-input-field--error, +.secured-fields-si.pm-input-field--error { + border: 1px solid #ff7d00; +} + +.pm-input-field--focus { + border: 1px solid #969696; + outline: none; +} +.pm-input-field--error.pm-input-field--focus { + border: 1px solid #ff7d00; +} + +.card-input__spinner__holder { + position: relative; + top: 40px; +} + +.card-input__spinner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + display: none; +} + +.card-input__spinner--active { + display: block; +} + +/*.valid-icon, +.error-icon { + display: none; + position: absolute; + top: 30px; + right: 16px; +} + +.valid-icon { + fill: #089A43; +} + +.error-icon { + fill: #C12424; +}*/ From fda7267a54953a80cc0b0e885466a0c944106f21 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 22 Aug 2024 09:48:17 +0200 Subject: [PATCH 02/24] Moved CustomCardContainer --- .../{cards/customCardHelpers => }/CustomCardContainer.tsx | 8 ++++---- .../lib/storybook/stories/cards/CustomCard.stories.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename packages/lib/storybook/stories/{cards/customCardHelpers => }/CustomCardContainer.tsx (95%) diff --git a/packages/lib/storybook/stories/cards/customCardHelpers/CustomCardContainer.tsx b/packages/lib/storybook/stories/CustomCardContainer.tsx similarity index 95% rename from packages/lib/storybook/stories/cards/customCardHelpers/CustomCardContainer.tsx rename to packages/lib/storybook/stories/CustomCardContainer.tsx index a33760ad78..dbfadbc09c 100644 --- a/packages/lib/storybook/stories/cards/customCardHelpers/CustomCardContainer.tsx +++ b/packages/lib/storybook/stories/CustomCardContainer.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'preact/hooks'; -import { makePayment } from '../../../helpers/checkout-api-calls'; -import paymentsConfig from '../../../config/paymentsConfig'; -import getCurrency from '../../../utils/get-currency'; -import { handleFinalState } from '../../../helpers/checkout-handlers'; +import { makePayment } from '../helpers/checkout-api-calls'; +import paymentsConfig from '../config/paymentsConfig'; +import getCurrency from '../utils/get-currency'; +import { handleFinalState } from '../helpers/checkout-handlers'; export const CustomCardContainer = ({ element, context }) => { const container = useRef(null); diff --git a/packages/lib/storybook/stories/cards/CustomCard.stories.tsx b/packages/lib/storybook/stories/cards/CustomCard.stories.tsx index b30449ef7b..bc7e5ee406 100644 --- a/packages/lib/storybook/stories/cards/CustomCard.stories.tsx +++ b/packages/lib/storybook/stories/cards/CustomCard.stories.tsx @@ -2,7 +2,7 @@ import { MetaConfiguration, PaymentMethodStoryProps, StoryConfiguration } from ' import { getStoryContextCheckout } from '../../utils/get-story-context-checkout'; import { CustomCardConfiguration } from '../../../src/components/CustomCard/types'; import { CustomCard } from '../../../src'; -import { CustomCardContainer } from './customCardHelpers/CustomCardContainer'; +import { CustomCardContainer } from '../CustomCardContainer'; import './customCardHelpers/customCard.style.scss'; import { styles, setFocus, onBrand, onConfigSuccess, onBinLookup, onChange, setCCErrors } from './customCardHelpers/customCard.config'; From 193a04932b0c802e687def1ec24435b598c297ea Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 22 Aug 2024 11:07:42 +0200 Subject: [PATCH 03/24] Created payment related utils file for the customCard --- .../storybook/stories/CustomCardContainer.tsx | 82 +---------------- .../stories/cards/CustomCard.stories.tsx | 2 +- .../customCardHelpers/customCard.utils.js | 87 +++++++++++++++++++ 3 files changed, 92 insertions(+), 79 deletions(-) create mode 100644 packages/lib/storybook/stories/cards/customCardHelpers/customCard.utils.js diff --git a/packages/lib/storybook/stories/CustomCardContainer.tsx b/packages/lib/storybook/stories/CustomCardContainer.tsx index dbfadbc09c..e8012ab837 100644 --- a/packages/lib/storybook/stories/CustomCardContainer.tsx +++ b/packages/lib/storybook/stories/CustomCardContainer.tsx @@ -1,91 +1,17 @@ import { useEffect, useRef, useState } from 'preact/hooks'; -import { makePayment } from '../helpers/checkout-api-calls'; -import paymentsConfig from '../config/paymentsConfig'; -import getCurrency from '../utils/get-currency'; -import { handleFinalState } from '../helpers/checkout-handlers'; + +import { setUpUtils, createPayButton } from './cards/customCardHelpers/customCard.utils'; export const CustomCardContainer = ({ element, context }) => { const container = useRef(null); - const createPayButton = (parent, component, attribute) => { - const payBtn = document.createElement('button'); - - payBtn.textContent = 'Pay'; - payBtn.name = 'pay'; - payBtn.classList.add('adyen-checkout__button', 'js-components-button--one-click', `js-${attribute}`); - - payBtn.addEventListener('click', e => { - e.preventDefault(); - startPayment(component); - }); - - document.querySelector(parent).appendChild(payBtn); - - return payBtn; - }; - - const startPayment = component => { - if (!component.isValid) return component.showValidation(); - - const allow3DS2 = paymentsConfig.authenticationData.attemptAuthentication || 'never'; - - makePayment(component.data, { - amount: { value: context.args.amount, currency: getCurrency(context.args.countryCode) }, - authenticationData: { - attemptAuthentication: allow3DS2, - // comment out below if you want to force MDFlow - threeDSRequestData: { - nativeThreeDS: 'preferred' - } - } - }) - .then(result => { - handlePaymentResult(result, component); - }) - .catch(error => { - throw Error(error); - }); - }; - - const handlePaymentResult = (result, component) => { - console.log('Result: ', result); - - if (result.action) { - threeDS2(result, component); - } else { - clearSFMarkup(); - - switch (result.resultCode) { - case 'Authorised': - case 'Refused': - handleFinalState(result, component); - break; - default: - } - } - }; - - const threeDS2 = (result, component) => { - clearSFMarkup(); - - component.handleAction(result.action); - }; - - const clearSFMarkup = () => { - if (globalThis.customCard) { - const sfNode = container.current; // equivalent to: document.querySelector('.secured-fields'); - // Clear the contents of the .secured-fields div - while (sfNode.firstChild) { - sfNode.removeChild(sfNode.firstChild); - } - } - }; - useEffect(() => { if (!element) return; element.mount(container.current); + setUpUtils(context, container); + globalThis.payBtn = createPayButton('.secured-fields', globalThis.customCard, 'customcard'); }, [element]); diff --git a/packages/lib/storybook/stories/cards/CustomCard.stories.tsx b/packages/lib/storybook/stories/cards/CustomCard.stories.tsx index bc7e5ee406..be9e194610 100644 --- a/packages/lib/storybook/stories/cards/CustomCard.stories.tsx +++ b/packages/lib/storybook/stories/cards/CustomCard.stories.tsx @@ -17,7 +17,7 @@ const createComponent = (args: PaymentMethodStoryProps, const checkout = getStoryContextCheckout(context); const customCard = new CustomCard(checkout, componentConfiguration); - window['customCard'] = customCard; + globalThis.customCard = customCard; return ; }; diff --git a/packages/lib/storybook/stories/cards/customCardHelpers/customCard.utils.js b/packages/lib/storybook/stories/cards/customCardHelpers/customCard.utils.js new file mode 100644 index 0000000000..62ddb7b52b --- /dev/null +++ b/packages/lib/storybook/stories/cards/customCardHelpers/customCard.utils.js @@ -0,0 +1,87 @@ +import paymentsConfig from '../../../config/paymentsConfig'; +import { makePayment } from '../../../helpers/checkout-api-calls'; +import getCurrency from '../../../utils/get-currency'; +import { handleFinalState } from '../../../helpers/checkout-handlers'; + +let context; +let container; + +// So the util functions can access the story's context and container +export const setUpUtils = (pContext, pContainer) => { + context = pContext; + container = pContainer; +}; + +export const createPayButton = (parent, component, attribute) => { + const payBtn = document.createElement('button'); + + payBtn.textContent = 'Pay'; + payBtn.name = 'pay'; + payBtn.classList.add('adyen-checkout__button', 'js-components-button--one-click', `js-${attribute}`); + + payBtn.addEventListener('click', e => { + e.preventDefault(); + startPayment(component); + }); + + document.querySelector(parent).appendChild(payBtn); + + return payBtn; +}; + +const startPayment = component => { + if (!component.isValid) return component.showValidation(); + + const allow3DS2 = paymentsConfig.authenticationData.attemptAuthentication || 'never'; + + makePayment(component.data, { + amount: { value: context.args.amount, currency: getCurrency(context.args.countryCode) }, + authenticationData: { + attemptAuthentication: allow3DS2, + // comment out below if you want to force MDFlow + threeDSRequestData: { + nativeThreeDS: 'preferred' + } + } + }) + .then(result => { + handlePaymentResult(result, component); + }) + .catch(error => { + throw Error(error); + }); +}; + +const handlePaymentResult = (result, component) => { + console.log('Result: ', result); + + if (result.action) { + threeDS2(result, component); + } else { + clearSFMarkup(); + + switch (result.resultCode) { + case 'Authorised': + case 'Refused': + handleFinalState(result, component); + break; + default: + } + } +}; + +const threeDS2 = (result, component) => { + clearSFMarkup(); + + component.handleAction(result.action); +}; + +const clearSFMarkup = () => { + if (globalThis.customCard) { + const sfNode = container.current; // equivalent to: document.querySelector('.secured-fields'); + // Clear the contents of the .secured-fields div + while (sfNode.firstChild) { + sfNode.removeChild(sfNode.firstChild); + } + } +}; From ff34bb6dabebf0d7be70c9c2717e9892425efcd1 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 22 Aug 2024 11:19:55 +0200 Subject: [PATCH 04/24] Fixing linting errors --- .../storybook/stories/CustomCardContainer.tsx | 20 +++++++++---------- .../customCardHelpers/customCard.config.js | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/lib/storybook/stories/CustomCardContainer.tsx b/packages/lib/storybook/stories/CustomCardContainer.tsx index e8012ab837..fcbc096043 100644 --- a/packages/lib/storybook/stories/CustomCardContainer.tsx +++ b/packages/lib/storybook/stories/CustomCardContainer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'preact/hooks'; +import { useEffect, useRef } from 'preact/hooks'; import { setUpUtils, createPayButton } from './cards/customCardHelpers/customCard.utils'; @@ -21,7 +21,7 @@ export const CustomCardContainer = ({ element, context }) => { ref={container} id="component-root" className="component-wrapper secured-fields" - // @ts-ignore + // @ts-ignore just hiding for better UX experience style={'display:none;'} > @@ -36,26 +36,26 @@ export const CustomCardContainer = ({ element, context }) => { -