From 39b47d7c4c3a08ef876b67185d0e8fae4dd0603e Mon Sep 17 00:00:00 2001 From: sponglord Date: Mon, 3 Oct 2022 13:53:33 +0200 Subject: [PATCH] Feature/expanding core mount fny (#1772) * expanding core mount fny - so that second and subsequent calls to mount act as a "remount" * Adding mock React app that can mount and unmount the Card component * Comment out console.log --- packages/lib/src/components/BaseElement.ts | 26 +++--- packages/playground/src/Script.js | 43 ++++++++++ .../playground/src/pages/Cards/Cards.html | 9 ++ packages/playground/src/pages/Cards/Cards.js | 14 ++- .../src/pages/Cards/MockReactApp.js | 85 +++++++++++++++++++ 5 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 packages/playground/src/Script.js create mode 100644 packages/playground/src/pages/Cards/MockReactApp.js diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index 5847ce83a9..d6f3034f4d 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -78,7 +78,16 @@ class BaseElement

{ } if (this._node) { - throw new Error('Component is already mounted.'); + this.unmount(); // new, if this._node exists then we are "remounting" so we first need to unmount if it's not already been done + } else { + // Set up analytics, once + if (this.props.modules && this.props.modules.analytics && !this.props.isDropin) { + this.props.modules.analytics.send({ + containerWidth: this._node && this._node.offsetWidth, + component: this.constructor['analyticsType'] ?? this.constructor['type'], + flavor: 'components' + }); + } } this._node = node; @@ -86,14 +95,6 @@ class BaseElement

{ render(this._component, node); - if (this.props.modules && this.props.modules.analytics && !this.props.isDropin) { - this.props.modules.analytics.send({ - containerWidth: this._node && this._node.offsetWidth, - component: this.constructor['analyticsType'] ?? this.constructor['type'], - flavor: 'components' - }); - } - return this; } @@ -106,11 +107,13 @@ class BaseElement

{ this.props = this.formatProps({ ...this.props, ...props }); this.state = {}; - return this.unmount().remount(); + return this.unmount().mount(this._node); // for new mount fny } /** - * Unmounts an element and mounts it again on the same node + * Unmounts an element and mounts it again on the same node i.e. allows mount w/o having to pass a node. + * Should be "private" & undocumented (although being a public function is useful for testing). + * Left in for legacy reasons */ public remount(component?): this { if (!this._node) { @@ -137,6 +140,7 @@ class BaseElement

{ /** * Unmounts an element and removes it from the parent instance + * For "destroy" type cleanup - when you don't intend to use the component again */ public remove() { this.unmount(); diff --git a/packages/playground/src/Script.js b/packages/playground/src/Script.js new file mode 100644 index 0000000000..7d12c6f14b --- /dev/null +++ b/packages/playground/src/Script.js @@ -0,0 +1,43 @@ +class Script { + constructor(src, node = 'body', attributes = {}, dataAttributes = {}) { + this.script = document.createElement('script'); + this.src = src; + this.node = node; + this.attributes = attributes; + this.dataAttributes = dataAttributes; + } + + load = () => + new Promise((resolve, reject) => { + Object.assign(this.script, this.attributes); + Object.assign(this.script.dataset, this.dataAttributes); + + this.script.src = this.src; + this.script.async = true; + this.script.onload = event => { + this.script.setAttribute('data-script-loaded', 'true'); + resolve(event); + }; + this.script.onerror = () => { + this.remove(); + reject(new Error(`Unable to load script ${this.src}`)); + }; + + const container = document.querySelector(this.node); + const addedScript = container.querySelector(`script[src="${this.src}"]`); + + if (addedScript) { + const isLoaded = addedScript.getAttribute('data-script-loaded'); + if (isLoaded) resolve(true); + else addedScript.addEventListener('load', resolve); + } else { + container.appendChild(this.script); + } + }); + + remove = () => { + return this.script.parentNode && this.script.parentNode.removeChild(this.script); + }; +} + +export default Script; diff --git a/packages/playground/src/pages/Cards/Cards.html b/packages/playground/src/pages/Cards/Cards.html index 872b4cf0b6..c99e0a3c49 100644 --- a/packages/playground/src/pages/Cards/Cards.html +++ b/packages/playground/src/pages/Cards/Cards.html @@ -38,6 +38,15 @@

Card

+
+
+

Card in React

+
+
+
+
+
+

Bancontact

diff --git a/packages/playground/src/pages/Cards/Cards.js b/packages/playground/src/pages/Cards/Cards.js index a84670a77b..95601385bb 100644 --- a/packages/playground/src/pages/Cards/Cards.js +++ b/packages/playground/src/pages/Cards/Cards.js @@ -5,15 +5,17 @@ import { handleSubmit, handleAdditionalDetails, handleError, handleChange } from import { amount, shopperLocale } from '../../config/commonConfig'; import '../../../config/polyfills'; import '../../style.scss'; +import { MockReactApp } from './MockReactApp'; const showComps = { + clickToPay: true, storedCard: true, card: true, + cardInReact: true, bcmcCard: true, avsCard: true, avsPartialCard: true, - kcpCard: true, - clickToPay: true + kcpCard: true }; getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse => { @@ -70,6 +72,12 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = .mount('.card-field'); } + // Card mounted in a React app + if (showComps.cardInReact) { + window.cardReact = checkout.create('card', {}); + MockReactApp(window, 'cardReact', document.querySelector('.react-card-field'), false); + } + // Bancontact card if (showComps.bcmcCard) { window.bancontact = checkout.create('bcmc').mount('.bancontact-field'); @@ -134,6 +142,8 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = .create('card', { type: 'scheme', brands: ['mc', 'visa', 'amex', 'bcmc', 'maestro', 'korean_local_card'], + // Set koreanAuthenticationRequired AND countryCode so KCP fields show at start + // Just set koreanAuthenticationRequired if KCP fields should only show if korean_local_card entered configuration: { koreanAuthenticationRequired: true }, diff --git a/packages/playground/src/pages/Cards/MockReactApp.js b/packages/playground/src/pages/Cards/MockReactApp.js new file mode 100644 index 0000000000..d5e7da9790 --- /dev/null +++ b/packages/playground/src/pages/Cards/MockReactApp.js @@ -0,0 +1,85 @@ +import Script from '../../Script'; + +function MountButton(props) { + const e = React.createElement; + + const [isMounted, setIsMounted] = React.useState(props.mountAtStart); + return e( + 'button', + { + type: 'button', + onClick: () => { + setIsMounted(!isMounted); + props.onClick(); + }, + className: 'adyen-checkout__button adyen-checkout__button--secondary' + }, + isMounted ? 'Unmount' : 'Mount' + ); +} + +function MockReactComp(props) { + const e = React.createElement; + + const ref = React.createRef(); + + const [isMounted, setIsMounted] = React.useState(false); + + const listenerFn = React.useCallback( + e => { + // console.log('### MockReactComp:::: isMounted=', isMounted, 'so...', isMounted ? 'unmount it' : 'mount it'); + if (!isMounted) { + setIsMounted(true); + // New mount/unmount/mount fny + props.docWindow[props.type].mount(ref.current); + + // Old mount/unmount/remount fny + // if (!props.docWindow[props.type]._node) { + // console.log('### MockReactComp:::: first mount'); + // props.docWindow[props.type].mount(ref.current); + // } else { + // console.log('### MockReactComp:::: second & sub remount'); + // props.docWindow[props.type].remount(); + // } + } else { + setIsMounted(false); + props.docWindow[props.type].unmount(); + // props.docWindow[props.type]._node = null; + } + }, + [isMounted] + ); + + React.useEffect(() => { + if (props.mountAtStart) { + setIsMounted(true); + // New mount/unmount/mount fny + props.docWindow[props.type].mount(ref.current); // = window.card.mount(ref.current) + } + }, []); + + React.useEffect(() => { + // console.log('### MockReactComp:::: RENDERING isMounted=', isMounted); + }, [isMounted]); + + return [ + e(MountButton, { + key: 'mountBtn', + onClick: listenerFn, + mountAtStart: props.mountAtStart + }), + e('div', { ref, id: 'myReactDiv', key: 'holder', style: { marginTop: '20px' } }) + ]; +} + +export async function MockReactApp(docWindow, type, domContainer, mountAtStart = true) { + const script1 = new Script('https://unpkg.com/react@18/umd/react.development.js'); + await script1.load(); + + const script2 = new Script('https://unpkg.com/react-dom@18/umd/react-dom.development.js'); + await script2.load(); + + const e = React.createElement; + const root = ReactDOM.createRoot(domContainer); + root.render(e(MockReactComp, { docWindow, type, mountAtStart })); +}