diff --git a/enabler/.env b/enabler/.env new file mode 100644 index 00000000..639d4507 --- /dev/null +++ b/enabler/.env @@ -0,0 +1,13 @@ +VITE_ADMIN_CLIENT_ID=IYg3zsamDxEtBVhN0GXPfSyz +VITE_ADMIN_CLIENT_SECRET=Ti5_ciOUgU76hu0hq75shf0V3Fg4Oean +VITE_ADMIN_SCOPE=manage_project:commercetools-checkout +VITE_REGION=europe-west1.gcp +VITE_CART_ID=f49ea44e-a189-4f9f-b17f-b09f29da49c9 + + + +# VITE_ADMIN_CLIENT_ID=u3YNvD_OUzwpIx1HkVmDyz95 +# VITE_ADMIN_CLIENT_SECRET=EYmPN7oAmwkTXe29owPPCkys_4V4924B +# VITE_ADMIN_SCOPE=manage_project:juanjcampos +# VITE_REGION=europe-west1.gcp + diff --git a/enabler/decs.d.ts b/enabler/decs.d.ts index cbc82b18..d5cf927a 100644 --- a/enabler/decs.d.ts +++ b/enabler/decs.d.ts @@ -1,7 +1 @@ -declare module "@adyen/adyen-web" -declare module "@adyen/adyen-web/dist/types/components/ApplePay" -declare module "@adyen/adyen-web/dist/types/components/GooglePay" -declare module "@adyen/adyen-web/dist/types/core" -declare module "@adyen/adyen-web/dist/types/core/core" -declare module "@adyen/adyen-web/dist/types/components/Redirect/Redirect" -declare module "@adyen/adyen-web/dist/types/core/types" +declare module '*.scss'; diff --git a/enabler/index.html b/enabler/index.html index db21a6f2..98416a99 100644 --- a/enabler/index.html +++ b/enabler/index.html @@ -17,12 +17,13 @@
-
-
-
Adyen dropin component:
-
-
-
-
Adyen card component:
-
-
-
-
Ideal component:
-
-
-
-
Googlepay component:
-
-
-
+
diff --git a/enabler/package-lock.json b/enabler/package-lock.json index 8b3797ed..3add58ab 100644 --- a/enabler/package-lock.json +++ b/enabler/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@adyen/adyen-web": "5.51.0", "@types/node": "^20.11.0", + "sass": "^1.70.0", "typescript": "5.2.2", "vite": "5.0.12", "vite-plugin-css-injected-by-js": "3.3.0" @@ -637,6 +638,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/async": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", @@ -656,6 +670,27 @@ "node": ">= 0.8" } }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -684,6 +719,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -789,6 +851,18 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", @@ -844,6 +918,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -976,6 +1062,54 @@ "node": ">=0.10.0" } }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1034,6 +1168,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -1056,6 +1199,18 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -1121,6 +1276,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -1174,6 +1341,23 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sass": { + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", + "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/secure-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", @@ -1227,6 +1411,18 @@ "node": ">=8" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", diff --git a/enabler/package.json b/enabler/package.json index 14bbee1a..bbfd7d8c 100644 --- a/enabler/package.json +++ b/enabler/package.json @@ -15,8 +15,8 @@ "http-server": "^14.1.1" }, "devDependencies": { - "@adyen/adyen-web": "5.51.0", "@types/node": "^20.11.0", + "sass": "^1.70.0", "typescript": "5.2.2", "vite": "5.0.12", "vite-plugin-css-injected-by-js": "3.3.0" diff --git a/enabler/src/FakeSdk.ts b/enabler/src/FakeSdk.ts new file mode 100644 index 00000000..b1be1b93 --- /dev/null +++ b/enabler/src/FakeSdk.ts @@ -0,0 +1,10 @@ +export class FakeSdk { + private environment: string; + constructor({ environment }) { + this.environment = environment; + console.log('FakeSdk constructor', this.environment); + } + init(opts: any) { + console.log('FakeSdk init', opts); + } +} diff --git a/enabler/src/components/base.ts b/enabler/src/components/base.ts index 00aeb02c..658d5d55 100644 --- a/enabler/src/components/base.ts +++ b/enabler/src/components/base.ts @@ -1,75 +1,54 @@ -import Core from '@adyen/adyen-web/dist/types/core/core'; -import { ComponentOptions, PaymentResult } from '../payment-connector/paymentConnector'; -import ApplePay from '@adyen/adyen-web/dist/types/components/ApplePay'; -import GooglePay from '@adyen/adyen-web/dist/types/components/GooglePay'; -import RedirectElement from '@adyen/adyen-web/dist/types/components/Redirect/Redirect'; +import { FakeSdk } from '../FakeSdk'; +import { ComponentOptions, PaymentMethod, PaymentResult } from '../payment-connector/paymentConnector'; + export type ElementOptions = { - adyenPaymentMethod: 'card' | 'ideal' | 'googlepay' | 'dropin' | 'applepay' | string; + paymentMethod: PaymentMethod; isOffsite?: boolean; -}; +}; + +export type BaseOptions = { + sdk: FakeSdk; + connectorUrl: string; + sessionId: string; + environment: string; + config: any; + onComplete: (result: PaymentResult) => void; + onError: (error?: any) => void; +} /** - * Base Component + * Base Web Component */ -export class BaseComponent { - protected _paymentMethod: string; - protected _offsite: boolean = false; - protected _adyenCheckout: typeof Core; - protected _config: any; - - public component: typeof ApplePay | typeof GooglePay | typeof RedirectElement; - - constructor(opts: ElementOptions, componentOptions: ComponentOptions, adyenCheckout: typeof Core) { - this._paymentMethod = opts.adyenPaymentMethod; - this._offsite = !!opts.isOffsite; - this._adyenCheckout = adyenCheckout; - this.beforePay = componentOptions.beforePay || (() => Promise.resolve()); - this.onError = componentOptions.onError; - this.onComplete = componentOptions.onComplete; - this.component = this._create(); - } - - beforePay: () => Promise; - - protected _create(): typeof ApplePay | typeof GooglePay | typeof RedirectElement { - return this._adyenCheckout.create(this._paymentMethod, this._config); - } - - submit() { - this.beforePay().then(() => { - this.component.submit(); - }); - } - - hasSubmit() { - return true; - } - - isOffsite() { - return this._offsite; - } - - isAvailable() { - return 'isAvailable' in this.component ? this.component.isAvailable() : Promise.resolve(); - } - - isValid() { - return this.component.isValid; - } - - mount(selector: string) { - this - .isAvailable() - .then(() => { - this.component.mount(selector); - }) - .catch((e: unknown) => { - console.log(`${this._paymentMethod } is not available`, e); - }); - } - - onComplete: ((result: PaymentResult) => Promise) | undefined; - - onError: ((error: any) => Promise) | undefined; +export abstract class BaseComponent { + protected paymentMethod: ElementOptions['paymentMethod']; + protected sdk: FakeSdk; + protected connectorUrl: BaseOptions['connectorUrl']; + protected sessionId: BaseOptions['sessionId']; + protected environment: BaseOptions['environment']; + protected config: BaseOptions['config']; + protected showPayButton: boolean; + + constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { + this.sdk = baseOptions.sdk; + this.connectorUrl = baseOptions.connectorUrl; + this.sessionId = baseOptions.sessionId; + this.environment = baseOptions.environment; + this.config = baseOptions.config; + this.onComplete = baseOptions.onComplete; + this.onError = baseOptions.onError; + this.showPayButton = + 'showPayButton' in componentOptions.config ? !!componentOptions.config.showPayButton : + 'showPayButton' in baseOptions.config ? !!baseOptions.config.showPayButton : + true; + } + + async updated() {} + + abstract submit(): void; + + abstract mount(selector: string): void ; + + onComplete: (result: PaymentResult) => void; + onError: (error?: any) => void; } diff --git a/enabler/src/components/payment-methods/applepay.ts b/enabler/src/components/payment-methods/applepay.ts deleted file mode 100644 index e646f139..00000000 --- a/enabler/src/components/payment-methods/applepay.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { BaseComponent } from '../base'; -import ApplePay from '@adyen/adyen-web/dist/types/components/ApplePay'; -import Core from '@adyen/adyen-web/dist/types/core'; -import { ComponentOptions } from '../../payment-connector/paymentConnector'; - -/** - * Apple pay component - * - * Configuration options: - * https://docs.adyen.com/payment-methods/apple-pay/web-component/ - */ -export class Applepay extends BaseComponent { - constructor(componentOptions: ComponentOptions, adyenCheckout: typeof Core) { - super({ adyenPaymentMethod: 'applepay' }, componentOptions, adyenCheckout); - } - - protected _create() { - return this._adyenCheckout.create(this._paymentMethod, { - onClick: (resolve, reject) => { - this.beforePay() - .then(() => resolve()) - .catch(() => reject()); - }, - ...this._config, - }) as unknown as typeof ApplePay; - } - - hasSubmit() { - return false; - } -} diff --git a/enabler/src/components/payment-methods/card.ts b/enabler/src/components/payment-methods/card.ts deleted file mode 100644 index ab199acc..00000000 --- a/enabler/src/components/payment-methods/card.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BaseComponent } from '../base'; -import { ComponentOptions } from '../../payment-connector/paymentConnector'; -import Core from '@adyen/adyen-web/dist/types/core'; - -/** - * Credit card component - * - * Configuration options: - * https://docs.adyen.com/payment-methods/cards/web-component/ - */ - - -export class Card extends BaseComponent { - constructor(componentOptions: ComponentOptions, adyenCheckout: typeof Core) { - super({ adyenPaymentMethod: 'card' }, componentOptions, adyenCheckout); - } -} diff --git a/enabler/src/components/payment-methods/card/card.ts b/enabler/src/components/payment-methods/card/card.ts new file mode 100644 index 00000000..37a1744a --- /dev/null +++ b/enabler/src/components/payment-methods/card/card.ts @@ -0,0 +1,125 @@ +import { BaseComponent, BaseOptions } from '../../base'; +import { ComponentOptions } from '../../../payment-connector/paymentConnector'; +import styles from '../../../style/style.module.scss'; +import inputFieldStyles from '../../../style/inputField.module.scss'; +import buttonStyles from '../../../style/button.module.scss'; +import { addFormFieldsEventListeners, fieldIds, getInput, getCardBrand, validateAllFields } from './utils'; + +export class Card extends BaseComponent { + constructor(baseOptions: BaseOptions, componentOptions: ComponentOptions) { + super(baseOptions, componentOptions); + this.paymentMethod = 'card'; + } + + mount(selector: string) { + document.querySelector(selector).insertAdjacentHTML("afterbegin", this._getTemplate()); + if (this.showPayButton) { + document.querySelector('#creditCardForm-paymentButton').addEventListener('click', (e) => { + e.preventDefault(); + this.submit(); + }); + } + + addFormFieldsEventListeners(); + } + + async submit() { + // here we would call the SDK to submit the payment + this.sdk.init({ environment: this.environment }); + const isFormValid = validateAllFields(); + if (!isFormValid) { + return; + } + try { + const requestData = { + paymentMethod: { + type: this.paymentMethod, + cardNumber: getInput(fieldIds.cardNumber).value.replace(/\s/g, ''), + expiryMonth: getInput(fieldIds.expiryDate).value.split('/')[0], + expiryYear: getInput(fieldIds.expiryDate).value.split('/')[1], + cvc: getInput(fieldIds.cvv).value, + holderName: getInput(fieldIds.holderName).value, + }, + "paymentReference": "1234", + }; + const response = await fetch(this.connectorUrl + '/payments', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-Session-Id': this.sessionId }, + body: JSON.stringify(requestData), + }); + const data = await response.json(); + + if (data.outcome === 'Authorized' && data.paymentReference) { + this.onComplete && this.onComplete({ isSuccess: true, paymentReference: data.paymentReference }); + } else { + this.onComplete && this.onComplete({ isSuccess: false }); + } + } catch(e) { + this.onError('Some error occurred. Please try again.'); + } + } + + private _getTemplate() { + const payButton = this.showPayButton ? `` : ''; + return ` +
+
+
+ + + Invalid card number +
+ + + + +
+
+
+
+ + + Invalid expiry date +
+
+ + + Invalid CVV +
+
+
+ + + This field is required +
+ ${payButton} +
+
+ ` + } + + showValidation() { + validateAllFields(); + } + + isValid() { + return validateAllFields(); + } + + getState() { + return { + endDigits: getInput(fieldIds.cardNumber).value.slice(-4), + brand: getCardBrand(getInput(fieldIds.cardNumber).value), + expiryDate: getInput(fieldIds.expiryDate).value, + }; + } +} diff --git a/enabler/src/components/payment-methods/card/utils.ts b/enabler/src/components/payment-methods/card/utils.ts new file mode 100644 index 00000000..1211f26e --- /dev/null +++ b/enabler/src/components/payment-methods/card/utils.ts @@ -0,0 +1,180 @@ +import styles from '../../../style/style.module.scss'; +import inputFieldStyles from '../../../style/inputField.module.scss'; + +export const fieldIds = { + cardNumber: 'creditCardForm-cardNumber', + expiryDate: 'creditCardForm-expiryDate', + cvv: 'creditCardForm-cvv', + holderName: 'creditCardForm-holderNameLabel', +}; + +export const getInput = (field: string) => (document.querySelector(`#${field}`) as HTMLInputElement); + +const showErrorIfInvalid = (field: string) => { + if (!isFieldValid(field)) { + const input = getInput(field); + input.parentElement.classList.add(inputFieldStyles.error); + input.parentElement.querySelector(`#${field} + .${inputFieldStyles.errorField}`).classList.remove(styles.hidden); + } +} + +const hideErrorIfValid = (field: string) => { + if (isFieldValid(field)) { + const input = getInput(field); + input.parentElement.classList.remove(inputFieldStyles.error); + input.parentElement.querySelector(`#${field} + .${inputFieldStyles.errorField}`).classList.add(styles.hidden); + } +} + +export const validateAllFields = () => { + let isValid = true; + Object.values(fieldIds).forEach((field) => { + if (!isFieldValid(field)) { + isValid = false; + showErrorIfInvalid(field); + } + }); + return isValid; +} + +const handleFieldValidation = (field: string) => { + const input = getInput(field); + input.addEventListener('input', () => { + hideErrorIfValid(field); + }); + input.addEventListener('focusout', () => { + showErrorIfInvalid(field); + input.value.length > 0 ? input.parentElement.classList.add(inputFieldStyles.containValue) : input.parentElement.classList.remove(inputFieldStyles.containValue); + }); +} + +const isFieldValid = (field: string) => { + const input = getInput(field); + switch (field) { + case 'creditCardForm-cardNumber': + return input.value.replace(/\s/g, '').length === 16; + case 'creditCardForm-expiryDate': + return input.value.length === 5; + case 'creditCardForm-cvv': + return input.value.length === 3; + case 'creditCardForm-holderNameLabel': + return input.value.length > 0; + default: + return false; + } +} + +export const getCardBrand = (cardNumber: string) => { + if (cardNumber.startsWith('4')) { + return 'visa'; + } + if (cardNumber.startsWith('5')) { + return 'mastercard'; + } + if (cardNumber.startsWith('6')) { + return 'maestro'; + } + if (cardNumber.startsWith('3')) { + return 'amex'; + } + return 'unknown'; +} + +const dateFormatter = (): ((inputValue: string) => string) => { + let previousValue = ''; + return (inputValue: string): string => { + let output = inputValue; + let isInvalidValue = false; + const pattern = /[0-9/]/; + const lastCharacter = inputValue.slice(-1); + const inputLength = inputValue.length; + /*** + * should not allow any character other than 0-9 and / + * should convert eg: 1/ => 01/ + * eg: 12/3/ =>12/3 + * eg: 18 =>01/8 + * eg: 123 =>12/3 + * eg: 12(previous value = 1) =>12/ + * eg: 12(previous value = 12/) =>1 (delete behaviour) + * eg: 12/345 =>12/34 + */ + if (!pattern.test(lastCharacter)) { + isInvalidValue = true; + } else if (lastCharacter === '/') { + if (inputLength === 2) { + output = `0${inputValue}`; + } else if (inputLength !== 3) { + isInvalidValue = true; + } + } else if (inputLength === 2) { + if (previousValue !== `${output}/`) { + if (Number(inputValue) > 12) { + output = `0${inputValue[0]}/${inputValue[1]}`; + } else { + output = `${output}/`; + } + } else { + output = output[0]; + } + } else if (inputLength === 3 && lastCharacter != '/') { + output = `${inputValue[0]}${inputValue[1]}/${inputValue[2]}`; + } else if (inputLength > 5) { + isInvalidValue = true; + } + + if (isInvalidValue) { + output = inputValue.substring(0, inputLength - 1); + } + previousValue = output; + return output; + }; +}; + +const addCardNumberEventListeners = () => { + const cardNumber = getInput(fieldIds.cardNumber); + cardNumber.addEventListener('input', () => { + cardNumber.value = cardNumber.value.replace(/\D/g,'').replace(/(\d{4})/g, '$1 ').trim(); + const brand = getCardBrand(cardNumber.value); + const cardIcons = document.querySelectorAll(`.${styles.cardIcon}`); + cardIcons.forEach((icon) => { + icon.classList.add(styles.hidden); + }); + const cardIcon = document.querySelector(`#creditCardForm-${brand}`); + if (cardIcon) { + cardIcon.classList.remove(styles.hidden); + } + }); + handleFieldValidation(fieldIds.cardNumber); +} + +const addDateEventListeners = () => { + const expiryDate = getInput(fieldIds.expiryDate); + expiryDate.addEventListener('input', () => { + expiryDate.value = dateFormatter()(expiryDate.value); + }); + handleFieldValidation(fieldIds.expiryDate); +} + +const addCvvEventListeners = () => { + const cvv = getInput(fieldIds.cvv); + cvv.addEventListener('input', () => { + if (isNaN(Number(cvv.value))) { + cvv.value = cvv.value.slice(0, -1); + } + if (cvv.value.length > 3) { + cvv.value = cvv.value.slice(0, 3); + } + }); + handleFieldValidation(fieldIds.cvv); +} + +const addHolderNameEventListeners = () => { + handleFieldValidation(fieldIds.holderName); +} + +export const addFormFieldsEventListeners = () => { + addCardNumberEventListeners(); + addDateEventListeners(); + addCvvEventListeners(); + addHolderNameEventListeners(); +} diff --git a/enabler/src/components/payment-methods/dropin.ts b/enabler/src/components/payment-methods/dropin.ts deleted file mode 100644 index 269f4f92..00000000 --- a/enabler/src/components/payment-methods/dropin.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseComponent } from '../base'; -import Core from '@adyen/adyen-web/dist/types/core'; -import { ComponentOptions } from '../../payment-connector/paymentConnector'; - -/** - * Dropin component - * - * Configuration options: - * https://docs.adyen.com/payment-methods/ideal/web-component/ - */ -export class Dropin extends BaseComponent { - constructor(componentOptions: ComponentOptions, adyenCheckout: typeof Core) { - super({ - adyenPaymentMethod: 'dropin', - isOffsite: true, - }, componentOptions, adyenCheckout); - } -} diff --git a/enabler/src/components/payment-methods/googlepay.ts b/enabler/src/components/payment-methods/googlepay.ts deleted file mode 100644 index ae892097..00000000 --- a/enabler/src/components/payment-methods/googlepay.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { BaseComponent } from '../base'; -import GooglePay from '@adyen/adyen-web/dist/types/components/GooglePay'; -import Core from '@adyen/adyen-web/dist/types/core'; -import { ComponentOptions } from '../../payment-connector/paymentConnector'; - -/** - * Google pay component - * - * Configuration options: - * https://docs.adyen.com/payment-methods/google-pay/web-component/ - */ -export class Googlepay extends BaseComponent { - constructor(componentOptions: ComponentOptions, adyenCheckout: typeof Core) { - super({ - adyenPaymentMethod: 'googlepay', - }, componentOptions, adyenCheckout); - } - - protected _create() { - return this._adyenCheckout.create(this._paymentMethod, { - onClick: (resolve, reject) => { - this.beforePay() - .then(() => resolve()) - .catch(() => reject()); - }, - ...this._config, - }) as unknown as typeof GooglePay; - } - - hasSubmit() { - return false; - } -} diff --git a/enabler/src/components/payment-methods/ideal.ts b/enabler/src/components/payment-methods/ideal.ts deleted file mode 100644 index da817f63..00000000 --- a/enabler/src/components/payment-methods/ideal.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseComponent } from '../base'; -import Core from '@adyen/adyen-web/dist/types/core'; -import { ComponentOptions } from '../../payment-connector/paymentConnector'; - -/** - * Ideal component - * - * Configuration options: - * https://docs.adyen.com/payment-methods/ideal/web-component/ - */ -export class Ideal extends BaseComponent { - constructor(componentOptions: ComponentOptions, adyenCheckout: typeof Core) { - super({ - adyenPaymentMethod: 'ideal', - isOffsite: true, - }, componentOptions, adyenCheckout); - } -} diff --git a/enabler/src/main.ts b/enabler/src/main.ts index e4fdbe86..db70e883 100644 --- a/enabler/src/main.ts +++ b/enabler/src/main.ts @@ -1,4 +1,3 @@ -import '@adyen/adyen-web/dist/adyen.css'; -import { AdyenPaymentConnector } from './payment-connector/adyenPaymentConnector'; +import { MockPaymentConnector } from './payment-connector/mockPaymentConnector'; -export { AdyenPaymentConnector as Connector }; +export { MockPaymentConnector as Connector }; diff --git a/enabler/src/payment-connector/adyenPaymentConnector.ts b/enabler/src/payment-connector/adyenPaymentConnector.ts deleted file mode 100644 index 84ee48df..00000000 --- a/enabler/src/payment-connector/adyenPaymentConnector.ts +++ /dev/null @@ -1,162 +0,0 @@ -import AdyenCheckout from '@adyen/adyen-web'; -import Core from '@adyen/adyen-web/dist/types/core/core'; -import { CoreOptions } from '@adyen/adyen-web/dist/types/core/types'; - -import { ComponentOptions, PaymentConnector, PaymentError, PaymentResult } from './paymentConnector'; -import { Dropin } from '../components/payment-methods/dropin'; -import { Card } from '../components/payment-methods/card'; -import { Applepay } from '../components/payment-methods/applepay'; -import { Ideal } from '../components/payment-methods/ideal'; -import { Googlepay } from '../components/payment-methods/googlepay'; - -declare global { - interface ImportMeta { - env: any; - } -} - -type AdyenConnectorOptions = { - connectorUrl: string; - sessionId: string; - config: Omit; - beforePay?: () => Promise; - onComplete: (result: PaymentResult) => Promise; - onError?: (error: PaymentError) => Promise; -}; - -enum AdyenComponentType { - dropin = "dropin", - card = "card", - applepay = "applepay", - ideal = "ideal", - googlepay = "googlepay", -} - -export class AdyenPaymentConnector implements PaymentConnector { - adyenCheckout: typeof Core; - sessionId: AdyenConnectorOptions['sessionId']; - connectorUrl: AdyenConnectorOptions['connectorUrl']; - config: AdyenConnectorOptions['config']; - beforePay: AdyenConnectorOptions['beforePay']; - onComplete: AdyenConnectorOptions['onComplete']; - onError: AdyenConnectorOptions['onError']; - setupData: Promise<{ adyenCheckout: typeof Core }>; - - private constructor(props: AdyenConnectorOptions) { - this.sessionId = props.sessionId; - this.connectorUrl = props.connectorUrl; - this.config = props.config; - this.beforePay = props.beforePay; - this.onComplete = props.onComplete; - this.onError = props.onError; - this.setupData = AdyenPaymentConnector._Setup(this); - } - - private static _Setup = async (instance: AdyenPaymentConnector): Promise<{ adyenCheckout: typeof Core }> => { - const [sessionResponse, configResponse] = await Promise.all([ - fetch(instance.connectorUrl + '/payment-sessions', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'X-Session-Id': instance.sessionId }, - body: JSON.stringify({}), - }), - fetch(instance.connectorUrl + '/config', { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }), - ]); - - const [sessionJson, configJson] = await Promise.all([sessionResponse.json(), configResponse.json()]); - - const { sessionData: data, paymentReference } = sessionJson; - - const adyenCheckout = await AdyenCheckout({ - onPaymentCompleted: (result, component) => { - debugger; - console.info(result, component); - window.location.href = instance.connectorUrl + '/confirm'; - }, - onError: (error, component) => { - console.error(error.name, error.message, error.stack, component); - }, - onSubmit: async (state, component) => { - const reqData = { - ...state.data, - channel: 'Web', - paymentReference, - }; - const response = await fetch(instance.connectorUrl + '/payments', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'X-Session-Id': instance.sessionId }, - body: JSON.stringify(reqData), - }); - const data = await response.json(); - console.log('onSubmit', state, component, data) - if (data.action) { - component.handleAction(data.action); - } else { - if (data.resultCode === 'Authorised') { - component.setStatus('success'); - instance.onComplete && instance.onComplete({ isSuccess: true, paymentReference }); - } else { - instance.onComplete && instance.onComplete({ isSuccess: false, paymentReference }); - component.setStatus('error'); - } - } - }, - onAdditionalDetails: async (state, component) => { - console.log('onAdditionalDetails', state, component); - const requestData = { - ...state.data, - paymentReference, - }; - const response = await fetch(instance.connectorUrl + '/payments/confirmations', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'X-Session-Id': instance.sessionId }, - body: JSON.stringify(requestData), - }); - const data = await response.json(); - if (data.resultCode === 'Authorised') { - component?.setStatus('success'); - component?.onComplete({ isSuccess: true, paymentReference }); - } else { - component?.onComplete({ isSuccess: false, paymentReference }); - component?.setStatus('error'); - } - }, - analytics: { - enabled: true, - }, - // Above properties can be rewritten externally - - // Spread options passed as parameter - ...instance.config, - - // Below options are always overwritten - environment: configJson.environment, - clientKey: configJson.clientKey, - session: { - id: data.id, - sessionData: data.sessionData, - }, - }); - - return { adyenCheckout }; - } - - async createComponent(type: string, opts: ComponentOptions) { - const { adyenCheckout } = await this.setupData; - switch (type) { - case AdyenComponentType.dropin: - return new Dropin(opts, adyenCheckout); - case AdyenComponentType.card: - return new Card(opts, adyenCheckout); - case AdyenComponentType.applepay: - return new Applepay(opts, adyenCheckout); - case AdyenComponentType.ideal: - return new Ideal(opts, adyenCheckout); - case AdyenComponentType.googlepay: - return new Googlepay(opts, adyenCheckout); - } - throw new Error(`Component type not supported: ${type}. Supported types: ${Object.keys(AdyenComponentType).join(', ')}`); - } -} diff --git a/enabler/src/payment-connector/mockPaymentConnector.ts b/enabler/src/payment-connector/mockPaymentConnector.ts new file mode 100644 index 00000000..4315f56e --- /dev/null +++ b/enabler/src/payment-connector/mockPaymentConnector.ts @@ -0,0 +1,75 @@ +import { ComponentOptions, PaymentConnector, PaymentResult, PaymentMethod } from './paymentConnector'; +import { Card } from '../components/payment-methods/card/card'; +import { FakeSdk } from '../FakeSdk'; +import { BaseOptions } from '../components/base'; + +const SupportedMethods: PaymentMethod[] = ['card']; + +declare global { + interface ImportMeta { + env: any; + } +} + +type MockConnectorOptions = { + connectorUrl: string; + sessionId: string; + config?: { hidePayButton?: boolean }; + onComplete?: (result: PaymentResult) => void; + onError?: (error: any) => void; +}; + +export class MockPaymentConnector implements PaymentConnector { + sessionId: MockConnectorOptions['sessionId']; + connectorUrl: MockConnectorOptions['connectorUrl']; + config: MockConnectorOptions['config']; + onComplete: MockConnectorOptions['onComplete']; + onError: MockConnectorOptions['onError']; + setupData: Promise<{ baseOptions: BaseOptions }>; + + private constructor(props: MockConnectorOptions) { + this.sessionId = props.sessionId; + this.connectorUrl = props.connectorUrl; + this.config = props.config; + this.onComplete = props.onComplete; + this.onError = props.onError; + this.setupData = MockPaymentConnector._Setup(this); + } + + private static _Setup = async (props: MockConnectorOptions): Promise<{ baseOptions: BaseOptions }> => { + // Fetch the config from the connector and use it to initialize your SDK: + + // const configResponse = await fetch(instance.connectorUrl + '/config', { + // method: 'GET', + // headers: { 'Content-Type': 'application/json' }, + // }); + + // const configJson = await configResponse.json(); + + const sdkOptions = { + // environment: configJson.environment, + environment: 'test' + } + + return Promise.resolve({ baseOptions: { + sdk: new FakeSdk(sdkOptions), + connectorUrl: props.connectorUrl, + sessionId: props.sessionId, + environment: sdkOptions.environment, + config: props.config || {}, + onComplete: props.onComplete || (() => {}), + onError: props.onError || (() => {}), + } + }); + } + + async createComponent(type: string, componentOptions: ComponentOptions) { + const { baseOptions } = await this.setupData; + + switch (type) { + case 'card': + return new Card(baseOptions, componentOptions); + } + throw new Error(`Payment method not supported: ${type}. Supported methods: ${Object.keys(SupportedMethods).join(', ')}`); + } +} diff --git a/enabler/src/payment-connector/paymentConnector.ts b/enabler/src/payment-connector/paymentConnector.ts index 6586c7dc..954984a5 100644 --- a/enabler/src/payment-connector/paymentConnector.ts +++ b/enabler/src/payment-connector/paymentConnector.ts @@ -1,26 +1,21 @@ import { BaseComponent } from '../components/base'; +export type PaymentMethod = 'card'; + export type PaymentResult = { - isSuccess: boolean; + isSuccess: true; paymentReference: string; -}; - -export type PaymentError = { - error: string; - paymentReference?: string; -}; +} | { isSuccess: false }; export type ComponentOptions = { - id: string; - config: any; - beforePay?: () => Promise; - onComplete: (result: PaymentResult) => Promise; - onError?: (error: PaymentError) => Promise; + config: { + showPayButton?: boolean; + }; }; export interface PaymentConnector { /** * @throws {Error} */ - createComponent: (type: string, opts: ComponentOptions) => Promise + createComponent: (type: string, opts: ComponentOptions, sessionId?: string) => Promise } diff --git a/enabler/src/style/_a11y.scss b/enabler/src/style/_a11y.scss new file mode 100644 index 00000000..8f299215 --- /dev/null +++ b/enabler/src/style/_a11y.scss @@ -0,0 +1,13 @@ +.visuallyHidden { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.dNone { + display: none; +} diff --git a/enabler/src/style/_colors.scss b/enabler/src/style/_colors.scss new file mode 100644 index 00000000..7278d3c7 --- /dev/null +++ b/enabler/src/style/_colors.scss @@ -0,0 +1,74 @@ +/** +* From https://www.figma.com/file/B3AuN5HVBARkLRXuYUWVY0/CA_UI-library?node-id=10-17139&t=HIYfSjMPWF7Q8zEX-0 +*/ + +$color-style-black: #000; +$color-style-white: #fff; + +// Primary +$color-style-primary-active: #115190; +$color-style-primary-disabled-background: #e0e0e0; +$color-style-primary-disabled-stroke: #9d9d9d; +$color-style-primary-hover-background: #edf4fc; +$color-style-primary-hover: #115fac; +$color-style-primary-low-capacity: #5b9ede; +$color-style-primary-main: #186ec3; + +// Secondary +$color-style-secondary-card-background: #f2f2f2; +$color-style-secondary-disabled-background: #fcfcfc; +$color-style-secondary-disabled-stroke: #e0e0e0; +$color-style-secondary-main: #555557; + +// Error +$color-style-error-active: #ffc5c5; +$color-style-error-background: linear-gradient( + 0deg, + rgba(255, 255, 255, 0.9), + rgba(255, 255, 255, 0.9) + ), + #d32f2f; +$color-style-error-dark: #b52323; +$color-style-error-hover: #fbdada; +$color-style-error-main: #d32f2f; + +// Warning +$color-style-warning-active: #ffd0a9; +$color-style-warning-background: #ffebdb; +$color-style-warning-dark: #e16600; +$color-style-warning-hover: #ffd6b5; +$color-style-warning-main: #ed6c02; + +// Success +$color-style-success-active: #c8e4ca; +$color-style-success-background: #e8f5e9; +$color-style-success-dark: #2e7d32; +$color-style-success-hover: #dcecdd; +$color-style-success-main: #4caf50; + +// Info +$color-style-info-active: #cce0eb; +$color-style-info-background: #e8f3fa; +$color-style-info-dark: #006fac; +$color-style-info-hover: #d6e9f3; +$color-style-info-main: #0288d1; + +// Other +$color-style-other-border-dark: #4f4f4f; +$color-style-other-border-default: #949494; +$color-style-other-border-divider: #dee2e6; +$color-style-other-card-background: #f7f7f7; +$color-style-other-divider: rgba(0, 0, 0, 0.12); +$color-style-other-tag-background: #ededed; +$color-style-other-tag-background-discount: #ddf7de; +$color-style-other-tag-text-discount: #105e14; +$color-style-other-tooltip-background: rgba(97, 97, 97, 0.9); + +// Text +$color-style-text-black: #333333; +$color-style-text-disabled: #a2a3a4; +$color-style-text-helper: #5e6368; +$color-style-text-label: #4f4f4f; + +// Layout +$color-style-side-background: #f8f8f8; diff --git a/enabler/src/style/_variables.scss b/enabler/src/style/_variables.scss new file mode 100644 index 00000000..e13afd3e --- /dev/null +++ b/enabler/src/style/_variables.scss @@ -0,0 +1,102 @@ +$color-black: $color-style-black; +$color-white: $color-style-white; + +/** +* From https://www.figma.com/file/B3AuN5HVBARkLRXuYUWVY0/CA_UI-library?node-id=10-17139&t=HIYfSjMPWF7Q8zEX-0 +*/ + +// TEXT +$color-text-default: $color-style-text-black; +$color-text-disabled: $color-style-text-disabled; +$color-text-error: $color-style-error-main; +$color-text-helper: $color-style-text-helper; +$color-text-label-input-focused: $color-style-primary-main; +$color-text-label: $color-style-text-label; +$color-text-white: $color-style-white; + +// BORDER +$color-border-default: $color-style-other-border-default; +$color-border-divider: $color-style-other-border-divider; +$color-border-disabled: $color-style-secondary-disabled-stroke; +$color-border-error: $color-style-error-main; +$color-border-focus: $color-style-other-border-dark; + +// STATUS +$color-status-default: $color-style-primary-main; +$color-status-focus: $color-style-primary-active; +$color-status-hover: $color-style-primary-hover; + +// success +$color-status-successful-active: $color-style-success-active; +$color-status-successful-background: $color-style-success-background; +$color-status-successful-dark: $color-style-success-dark; +$color-status-successful-hover: $color-style-success-hover; +$color-status-successful: $color-style-success-main; + +// warning +$color-status-warning-active: $color-style-warning-active; +$color-status-warning-background: $color-style-warning-background; +$color-status-warning-dark: $color-style-warning-dark; +$color-status-warning-hover: $color-style-warning-hover; +$color-status-warning: $color-style-warning-main; + +// info +$color-status-info-active: $color-style-info-active; +$color-status-info-background: $color-style-info-background; +$color-status-info-dark: $color-style-info-dark; +$color-status-info-hover: $color-style-info-hover; +$color-status-info: $color-style-info-main; + +// error +$color-status-error-active: $color-style-error-active; +$color-status-error-background: $color-style-error-background; +$color-status-error-dark: $color-style-error-dark; +$color-status-error-hover: $color-style-error-hover; +$color-status-error: $color-style-error-main; + +// BUTTON +$color-button-disabled: $color-style-primary-disabled-background; +$color-button-focus: $color-style-primary-active; +$color-button-hover: $color-style-primary-hover; +$color-button-low-opacity: $color-style-primary-low-capacity; +$color-button: $color-style-primary-main; + +// OTHER COMPONENTS +$color-card-background: $color-style-other-card-background; +$color-divider: $color-style-other-divider; +$color-tag-background: $color-style-other-tag-background; +$color-tag-background-discount: $color-style-other-tag-background-discount; +$color-tooltip-background: $color-style-other-tooltip-background; + +// FONTS +$font-family: var(--ctc-font-family); +$font-weight-light: 300; +$font-weight-regular: 400; +$font-weight-medium: 500; + +// Radiuses +$border-radius: 0.25rem; + +// Shadows +$box-shadow: + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 3px 1px -2px rgba(0, 0, 0, 0.12), + 0 1px 5px 0 rgba(0, 0, 0, 0.2); + +// Z-Index +$z-index-1: 1; +$z-index-100: 100; +$z-index-200: 200; + +// Breakpoints +$breakpoints: ( + 'small': ( + min-width: 360px, + ), + 'medium': ( + min-width: 576px, + ), + 'large': ( + min-width: 1024px, + ), +) !default; diff --git a/enabler/src/style/_vx.scss b/enabler/src/style/_vx.scss new file mode 100644 index 00000000..23610123 --- /dev/null +++ b/enabler/src/style/_vx.scss @@ -0,0 +1,36 @@ +// BUTTONS +:root, +:host { + --ctc-button: #186ec3; + --ctc-button-hover: color-mix(in srgb, var(--ctc-button), black 15%); + --ctc-button-disabled: #e0e0e0; + --ctc-button-text: #fff; + --ctc-button-disabled-text: #a2a3a4; +} + +// RADIO +:root, +:host { + --ctc-radio: #186ec3; +} + +// CHECKBOX +:root, +:host { + --ctc-checkbox: #186ec3; +} + +// INPUT FIELDS +:root, +:host { + --ctc-input-field-focus: #186ec3; +} + +// FONTS +:root, +:host { + --ctc-font-family: 'Roboto', sans-serif; +} + +@import './colors'; +@import './variables'; diff --git a/enabler/src/style/button.module.scss b/enabler/src/style/button.module.scss new file mode 100644 index 00000000..ed8569c1 --- /dev/null +++ b/enabler/src/style/button.module.scss @@ -0,0 +1,157 @@ +@use './vx' as *; + +// legacy browser fallback +@supports not (background: color-mix(in srgb, red 50%, blue)) { + :root { + --ctc-button-hover: var(--ctc-button); + } +} + +.button { + color: var(--ctc-button-text); + padding: 0.5rem 1.375rem; + background-color: var(--ctc-button); + border: 0 none; + border-radius: $border-radius; + font-size: 0.9375rem; + font-weight: $font-weight-regular; + font-family: $font-family; + text-transform: uppercase; + line-height: 1.5rem; + letter-spacing: 0.43px; + box-shadow: $box-shadow; + background-position: center; + transition: background-color 0.8s; + cursor: pointer; + + &:hover { + background: var(--ctc-button-hover) + radial-gradient(circle, transparent 1%, var(--ctc-button-hover) 1%) center/15000%; + } + + &:active { + background-color: var(--ctc-button-hover); + background-size: 100%; + transition: background-color 0s; + } + + &:disabled { + color: var(--ctc-button-disabled-text); + background-color: var(--ctc-button-disabled); + pointer-events: none; + box-shadow: none; + + &:hover, + &:active { + background-color: var(--ctc-button-disabled); + cursor: not-allowed; + } + } +} + +.fullWidth { + width: 100%; +} + +.linkButton { + text-decoration: none; + text-transform: unset; + background: none; + border-bottom: 1.5px solid transparent; + box-shadow: none; + padding: 0; + border-radius: 0; + font-weight: $font-weight-light; + color: var(--ctc-button); + + font-size: 1rem; + font-weight: 700; + line-height: 1.188rem; + letter-spacing: 0.009rem; + + &:hover, + &:active { + cursor: pointer; + border-bottom: 1.5px solid var(--ctc-button); + background: none; + } + + &.disabled { + color: $color-style-text-disabled; + text-decoration: none; + cursor: not-allowed; + pointer-events: none; + background-color: transparent; + box-shadow: none; + + &:hover, + &:active { + border-bottom: none; + } + } +} + +// variants +.lowOpacityButton { + background-color: color-mix(in srgb, var(--ctc-button), transparent 30%); + + // legacy browser fallback + @supports not (background: color-mix(in srgb, red 50%, blue)) { + background-color: var(--ctc-button); + opacity: 60%; + } +} + +.textButton { + background-color: transparent; + color: var(--ctc-button); + box-shadow: none; + padding: 0.5rem 0.6875rem; + + $color-style-primary-hover-background: color-mix(in srgb, var(--ctc-button), transparent 90%); + + &:hover { + background: $color-style-primary-hover-background + radial-gradient(circle, transparent 1%, $color-style-primary-hover-background 1%) + center/15000%; + } + + &:active { + background-color: $color-style-primary-hover-background; + background-size: 100%; + transition: background-color 0s; + } + + &:focus { + background-color: $color-style-primary-hover-background; + } + + &:disabled { + background-color: transparent; + color: $color-button-disabled; + } +} + +.errorButton { + color: $color-status-error-dark; + background: transparent; + border: 1px solid transparent; + transition: none; + box-shadow: none; + outline: none; + transition: none; + + &:hover { + background: $color-status-error-hover; + } + + &:active { + border: 1px solid $color-status-error-dark; + background: $color-status-error-active; + } + + &:disabled { + background-color: transparent; + color: $color-button-disabled; + } +} diff --git a/enabler/src/style/inputField.module.scss b/enabler/src/style/inputField.module.scss new file mode 100644 index 00000000..511fa6e4 --- /dev/null +++ b/enabler/src/style/inputField.module.scss @@ -0,0 +1,196 @@ + +@use './vx' as *; + +.inputContainer { + position: relative; + width: 100%; + font-family: $font-family; + margin-bottom: 1.5rem; + + .inputLabel { + position: absolute; + width: 100%; + top: 1rem; + padding: 0 0.25rem 0 1rem; + + font-size: 1rem; + line-height: 1.5rem; + background-color: transparent; + color: $color-border-focus; + transition: 0.3s all; + pointer-events: none; + z-index: $z-index-1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .inputField { + width: 100%; + margin: 0; + padding: 1rem 0.75rem; + box-sizing: border-box; + font-size: 1rem; + line-height: 1rem; + border: 1px solid $color-border-default; + border-radius: 4px; + &::placeholder { + opacity: 0; + transition: 0.3s opacity; + } + } + + .inputField:hover { + border: 1px solid $color-border-focus; + cursor: pointer; + } + + .helperText { + color: $color-text-helper; + font-size: 0.75rem; + line-height: 0.75rem; + padding: 0.25rem 0.75rem 0.25rem 1rem; + } +} + +.inputContainer:not(.containValue) { + .inputField { + width: 100%; + margin: 0; + padding: 1rem 0.75rem; + box-sizing: border-box; + font-size: 1rem; + line-height: 1rem; + border: 1px solid $color-border-default; + border-radius: 4px; + &::placeholder { + opacity: 0; + transition: 0.3s opacity; + } + &:not(:focus)::-webkit-datetime-edit-year-field:not([aria-valuenow]), + &:not(:focus)::-webkit-datetime-edit-month-field:not([aria-valuenow]), + &:not(:focus)::-webkit-datetime-edit-day-field:not([aria-valuenow]) { + color: transparent; + } + &:not(:focus):in-range::-webkit-datetime-edit-year-field, + &:not(:focus):in-range::-webkit-datetime-edit-month-field, + &:not(:focus):in-range::-webkit-datetime-edit-day-field, + &:not(:focus):in-range::-webkit-datetime-edit-hour-field, + &:not(:focus):in-range::-webkit-datetime-edit-minute-field, + &:not(:focus):in-range::-webkit-datetime-edit-text { + color: transparent; + } + } +} + +.inputContainer:focus-within, +.disabledWithValue, +.inputContainer.containValue { + overflow: initial; + + .inputLabel { + transform: translateY(-1.375rem); + width: auto; + padding: 0 0.25rem; + left: 0.75rem; + color: var(--ctc-input-field-focus); + background-color: #fff; + font-size: 0.75rem; + line-height: 0.75rem; + } + + .inputField { + outline-color: var(--ctc-input-field-focus); + } + + .inputField::placeholder { + opacity: 1; + } +} + +.hasGreyBackground.inputContainer:focus-within { + .inputLabel { + background-color: $color-style-side-background; + } +} + +.containValue:not(:focus-within), +.disabledWithValue { + .inputLabel { + color: $color-border-default; + } +} + +.trailingIconContainer { + .inputField { + padding-right: 3.25rem; + } + .trailingIcon { + position: absolute; + right: 1rem; + top: 1rem; + width: 1.5rem; + height: 1.5rem; + cursor: pointer; + } +} + +.leadingIconContainer { + .inputField { + padding-left: 3rem; + } + .leadingIcon { + position: absolute; + left: 0.75rem; + top: 1rem; + width: 1.5rem; + height: 1.5rem; + cursor: pointer; + } + .inputLabel { + left: 3rem; + } +} + +.inputContainer.error { + margin-bottom: 1rem; + .inputField { + border: 2px solid $color-status-error; + outline: none; + } + .helperText { + color: $color-status-error; + } +} + +.inputContainer.error:focus-within, +.inputContainer.error.containValue { + .inputLabel { + color: $color-status-error; + max-width: calc(100% - 1rem); + } +} + +.inputContainer.disabled { + .inputField { + cursor: not-allowed; + border: 1px solid $color-border-default; + color: $color-border-default; + background-color: $color-text-white; + } + .inputLabel { + color: $color-border-default; + } +} + +.inputFieldTooltip { + position: absolute; + top: 1rem; + right: 0.35rem; +} + +.errorField { + color: $color-status-error; + font-size: 0.75rem; + padding: 0.25rem 0.75rem 0.25rem 1rem; +} diff --git a/enabler/src/style/style.module.scss b/enabler/src/style/style.module.scss new file mode 100644 index 00000000..2fcf0231 --- /dev/null +++ b/enabler/src/style/style.module.scss @@ -0,0 +1,196 @@ +@use './vx' as *; +@import './a11y'; + +// from mock + +.paymentForm { + margin-top: 1rem; +} + +.wrapper { + position: relative; + width: 100%; + margin-bottom: 1rem; +} + +.row { + display: flex; + flex-direction: row; + gap: 1rem; + position: relative; +} + +.subHeading { + color: $color-text-helper; + font-size: 0.8125rem; + margin-top: 0.125rem; + margin-bottom: 1.5rem; +} + + +// from worldpay + + +.wrapper { + * { + font-family: 'Roboto', sans-serif; + box-sizing: border-box; + } +} + + +.twoColumnLayout { + display: flex; + flex-flow: row wrap; + column-gap: 1rem; + + // render children as columns when each child has at least 150px of space + > * { + flex: 1 0 150px; + } +} + +.container { + margin-top: 1rem; + + iframe { + height: 3.317rem !important; + border: 1px solid $color-border-default !important; + border-radius: $border-radius; + padding-left: 1rem; + width: 100%; + float: none !important; + } + + .input { + display: inline-block; + position: relative; + width: 100%; + margin-bottom: 1rem; + } + + .error { + color: $color-status-error; + font-size: 0.75rem; + padding: 0 0.75rem 0.25rem 1rem; + position: rela2tive; + } +} + +/* input fields border colors */ +:global(.is-valid) { + & iframe { + border: 1px solid $color-border-default !important; + } +} + +:global(.is-onfocus) { + & iframe { + border: 2px solid $color-status-default !important; + } + ~ .error { + display: none; + } +} + +:global(.is-invalid), +.inputError :not(:global(.is-onfocus)), +.inputErrorEmptyText { + & iframe { + border: 2px solid $color-status-error !important; + } +} + +/* floting labels */ +.floatingLabel { + position: absolute; + pointer-events: none; + left: 0.8rem; + top: 1rem; + transition: 0.2s ease all; + background-color: white; + padding: 0 0.25rem; + width: 80%; +} + +.wrapper { + position: relative; + width: 100%; + margin-bottom: 1rem; +} + +.cardIcons { + display: none; +} + +:global(.is-empty) ~ .cardIcons { + display: block; +} + +:global(.is-empty) ~ .floatingCard { + display: none; +} + +:global(.is-onfocus) ~ .floatingLabel, +:global(.is-valid) ~ .floatingLabel, +:global(.is-invalid):not(.inputEmpty) ~ .floatingLabel { + top: -0.5rem; + font-size: 0.75rem; + width: auto; +} + +/* floating labels colors */ +:global(.is-valid) ~ .floatingLabel { + color: $color-border-default; +} + +:global(.is-onfocus) ~ .floatingLabel { + color: $color-status-default; +} + +:global(.is-invalid):not(.inputEmpty) ~ .floatingLabel { + color: $color-status-error; +} + +.inputErrorEmptyText ~ .floatingLabel { + color: black; +} + +/* floating card icon */ +.floatingCard { + position: absolute !important; + pointer-events: none; + right: 0; + top: 0; + transition: 0.2s ease all; + background-color: white; + padding: 0 0.25rem; +} + +.subHeading { + color: $color-text-helper; + font-size: 0.8125rem; + margin-top: 0.125rem; + margin-bottom: 1.5rem; +} + +.alert { + margin: 1em 0; +} + + + +.cardRow { + display: flex; + gap: 0.25rem; + margin-top: 0.5rem; +} + +.cardIcon { + border: 1px solid #d9d9d9; + border-radius: 0.156rem; +} + +.hidden { + display: none; +}